1# This file is part of Gajim.
2#
3# Gajim is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published
5# by the Free Software Foundation; version 3 only.
6#
7# Gajim is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License
13# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
14
15# XEP-0030: Service Discovery
16
17import nbxmpp
18from nbxmpp.namespaces import Namespace
19from nbxmpp.structs import StanzaHandler
20from nbxmpp.errors import StanzaError
21from nbxmpp.errors import is_error
22
23from gajim.common import app
24from gajim.common.nec import NetworkIncomingEvent
25from gajim.common.nec import NetworkEvent
26from gajim.common.modules.util import as_task
27from gajim.common.modules.base import BaseModule
28
29
30class Discovery(BaseModule):
31
32    _nbxmpp_extends = 'Discovery'
33    _nbxmpp_methods = [
34        'disco_info',
35        'disco_items',
36    ]
37
38    def __init__(self, con):
39        BaseModule.__init__(self, con)
40
41        self.handlers = [
42            StanzaHandler(name='iq',
43                          callback=self._answer_disco_info,
44                          typ='get',
45                          ns=Namespace.DISCO_INFO),
46            StanzaHandler(name='iq',
47                          callback=self._answer_disco_items,
48                          typ='get',
49                          ns=Namespace.DISCO_ITEMS),
50        ]
51
52        self._account_info = None
53        self._server_info = None
54
55    @property
56    def account_info(self):
57        return self._account_info
58
59    @property
60    def server_info(self):
61        return self._server_info
62
63    def discover_server_items(self):
64        server = self._con.get_own_jid().domain
65        self.disco_items(server, callback=self._server_items_received)
66
67    def _server_items_received(self, task):
68        try:
69            result = task.finish()
70        except StanzaError as error:
71            self._log.warning('Server disco failed')
72            self._log.error(error)
73            return
74
75        self._log.info('Server items received')
76        self._log.debug(result)
77        for item in result.items:
78            if item.node is not None:
79                # Only disco components
80                continue
81            self.disco_info(item.jid, callback=self._server_items_info_received)
82
83    def _server_items_info_received(self, task):
84        try:
85            result = task.finish()
86        except StanzaError as error:
87            self._log.warning('Server item disco info failed')
88            self._log.warning(error)
89            return
90
91        self._log.info('Server item info received: %s', result.jid)
92        self._parse_transports(result)
93        try:
94            self._con.get_module('MUC').pass_disco(result)
95            self._con.get_module('HTTPUpload').pass_disco(result)
96            self._con.get_module('Bytestream').pass_disco(result)
97        except nbxmpp.NodeProcessed:
98            pass
99
100        app.nec.push_incoming_event(
101            NetworkIncomingEvent('server-disco-received'))
102
103    def discover_account_info(self):
104        own_jid = self._con.get_own_jid().bare
105        self.disco_info(own_jid, callback=self._account_info_received)
106
107    def _account_info_received(self, task):
108        try:
109            result = task.finish()
110        except StanzaError as error:
111            self._log.warning('Account disco info failed')
112            self._log.warning(error)
113            return
114
115        self._log.info('Account info received: %s', result.jid)
116
117        self._account_info = result
118
119        self._con.get_module('MAM').pass_disco(result)
120        self._con.get_module('PEP').pass_disco(result)
121        self._con.get_module('PubSub').pass_disco(result)
122        self._con.get_module('Bookmarks').pass_disco(result)
123        self._con.get_module('VCardAvatars').pass_disco(result)
124
125        self._con.get_module('Caps').update_caps()
126
127    def discover_server_info(self):
128        # Calling this method starts the connect_maschine()
129        server = self._con.get_own_jid().domain
130        self.disco_info(server, callback=self._server_info_received)
131
132    def _server_info_received(self, task):
133        try:
134            result = task.finish()
135        except StanzaError as error:
136            self._log.error('Server disco info failed')
137            self._log.error(error)
138            return
139
140        self._log.info('Server info received: %s', result.jid)
141
142        self._server_info = result
143
144        self._con.get_module('SecLabels').pass_disco(result)
145        self._con.get_module('Blocking').pass_disco(result)
146        self._con.get_module('VCardTemp').pass_disco(result)
147        self._con.get_module('Carbons').pass_disco(result)
148        self._con.get_module('HTTPUpload').pass_disco(result)
149        self._con.get_module('Register').pass_disco(result)
150
151        self._con.connect_machine(restart=True)
152
153    def _parse_transports(self, info):
154        for identity in info.identities:
155            if identity.category not in ('gateway', 'headline'):
156                continue
157
158            self._log.info('Found transport: %s %s %s',
159                           info.jid, identity.category, identity.type)
160
161            jid = str(info.jid)
162            if jid not in app.transport_type:
163                app.transport_type[jid] = identity.type
164
165            if identity.type in self._con.available_transports:
166                self._con.available_transports[identity.type].append(jid)
167            else:
168                self._con.available_transports[identity.type] = [jid]
169
170    def _answer_disco_items(self, _con, stanza, _properties):
171        from_ = stanza.getFrom()
172        self._log.info('Answer disco items to %s', from_)
173
174        if self._con.get_module('AdHocCommands').command_items_query(stanza):
175            raise nbxmpp.NodeProcessed
176
177        node = stanza.getTagAttr('query', 'node')
178        if node is None:
179            result = stanza.buildReply('result')
180            self._con.connection.send(result)
181            raise nbxmpp.NodeProcessed
182
183        if node == Namespace.COMMANDS:
184            self._con.get_module('AdHocCommands').command_list_query(stanza)
185            raise nbxmpp.NodeProcessed
186
187    def _answer_disco_info(self, _con, stanza, _properties):
188        from_ = stanza.getFrom()
189        self._log.info('Answer disco info %s', from_)
190        if str(from_).startswith('echo.'):
191            # Service that echos all stanzas, ignore it
192            raise nbxmpp.NodeProcessed
193
194        if self._con.get_module('AdHocCommands').command_info_query(stanza):
195            raise nbxmpp.NodeProcessed
196
197    @as_task
198    def disco_muc(self,
199                  jid,
200                  request_vcard=False,
201                  allow_redirect=False):
202
203        _task = yield
204
205        self._log.info('Request MUC info for %s', jid)
206
207        result = yield self._nbxmpp('MUC').request_info(
208            jid,
209            request_vcard=request_vcard,
210            allow_redirect=allow_redirect)
211
212        if is_error(result):
213            raise result
214
215        if result.redirected:
216            self._log.info('MUC info received after redirect: %s -> %s',
217                           jid, result.info.jid)
218        else:
219            self._log.info('MUC info received: %s', result.info.jid)
220
221        app.storage.cache.set_last_disco_info(result.info.jid, result.info)
222
223        if result.vcard is not None:
224            avatar, avatar_sha = result.vcard.get_avatar()
225            if avatar is not None:
226                if not app.interface.avatar_exists(avatar_sha):
227                    app.interface.save_avatar(avatar)
228
229                app.storage.cache.set_muc_avatar_sha(result.info.jid,
230                                                     avatar_sha)
231                app.interface.avatar_storage.invalidate_cache(result.info.jid)
232
233        self._con.get_module('VCardAvatars').muc_disco_info_update(result.info)
234        app.nec.push_incoming_event(NetworkEvent(
235            'muc-disco-update',
236            account=self._account,
237            room_jid=result.info.jid))
238
239        yield result
240
241    @as_task
242    def disco_contact(self, contact):
243        _task = yield
244
245        fjid = contact.get_full_jid()
246
247        result = yield self.disco_info(fjid)
248        if is_error(result):
249            raise result
250
251        self._log.info('Disco Info received: %s', fjid)
252
253        app.storage.cache.set_last_disco_info(result.jid,
254                                              result,
255                                              cache_only=True)
256
257        app.nec.push_incoming_event(
258            NetworkEvent('caps-update',
259                         account=self._account,
260                         fjid=fjid,
261                         jid=contact.jid))
262
263
264def get_instance(*args, **kwargs):
265    return Discovery(*args, **kwargs), 'Discovery'
266