root/branches/gajim_0.9/src/disco.py

Revision 4806, 52.4 kB (checked in by asterix, 3 years ago)

revert my previous fix, and just don't take into account the click on go button if jid hasn't changed

Line 
1# -*- coding: utf-8 -*-
2##      config.py
3##
4## Contributors for this file:
5##      - Yann Le Boulanger <asterix@lagaule.org>
6##      - Nikos Kouremenos <kourem@gmail.com>
7##      - Stéphan Kochen <stephan@kochen.nl>
8##
9## Copyright (C) 2003-2004 Yann Le Boulanger <asterix@lagaule.org>
10##                         Vincent Hanquez <tab@snarc.org>
11## Copyright (C) 2005 Yann Le Boulanger <asterix@lagaule.org>
12##                    Vincent Hanquez <tab@snarc.org>
13##                    Nikos Kouremenos <nkour@jabber.org>
14##                    Dimitur Kirov <dkirov@gmail.com>
15##                    Travis Shirk <travis@pobox.com>
16##                    Norman Rasmussen <norman@rasmussen.co.za>
17##
18## This program is free software; you can redistribute it and/or modify
19## it under the terms of the GNU General Public License as published
20## by the Free Software Foundation; version 2 only.
21##
22## This program is distributed in the hope that it will be useful,
23## but WITHOUT ANY WARRANTY; without even the implied warranty of
24## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25## GNU General Public License for more details.
26##
27
28# The appearance of the treeview, and parts of the dialog, are controlled by
29# AgentBrowser (sub-)classes. Methods that probably should be overridden when
30# subclassing are: (look at the docstrings and source for additional info)
31# - def cleanup(self) *
32# - def _create_treemodel(self) *
33# - def _add_actions(self)
34# - def _clean_actions(self)
35# - def update_theme(self) *
36# - def update_actions(self)
37# - def default_action(self)
38# - def _find_item(self, jid, node)
39# - def _add_item(self, model, jid, node, item, force)
40# - def _update_item(self, model, iter, jid, node, item)
41# - def _update_info(self, model, iter, jid, node, identities, features, data)
42# - def _update_error(self, model, iter, jid, node)
43#
44# * Should call the super class for this method.
45# All others do not have to call back to the super class. (but can if they want
46# the functionality)
47# There are more methods, of course, but this is a basic set.
48
49import os
50import inspect
51import weakref
52import gobject
53import gtk
54import gtk.glade
55
56import dialogs
57import tooltips
58
59from gajim import Contact
60from common import helpers
61from common import gajim
62from common import xmpp
63from common import connection
64from common import i18n
65
66_ = i18n._
67APP = i18n.APP
68gtk.glade.bindtextdomain (APP, i18n.DIR)
69gtk.glade.textdomain (APP)
70
71GTKGUI_GLADE = 'gtkgui.glade'
72
73
74# Dictionary mapping category, type pairs to browser class, image pairs.
75# This is a function, so we can call it after the classes are declared.
76# For the browser class, None means that the service will only be browsable
77# when it advertises disco as it's feature, False means it's never browsable.
78def _gen_agent_type_info():
79        return {
80                # Defaults
81                (0, 0):                                                 (None, None),
82
83                # Jabber server
84                ('server', 'im'):                               (ToplevelAgentBrowser, 'jabber.png'),
85                ('services', 'jabber'):         (ToplevelAgentBrowser, 'jabber.png'),
86
87                # Services
88                ('conference', 'text'):         (MucBrowser, 'conference.png'),
89                ('headline', 'rss'):                    (AgentBrowser, 'rss.png'),
90                ('headline', 'weather'):        (False, 'weather.png'),
91                ('gateway', 'weather'):         (False, 'weather.png'),
92                ('_jid', 'weather'):                    (False, 'weather.png'),
93                ('gateway', 'sip'):                     (False, 'sip.png'),
94                ('directory', 'user'):          (None, 'jud.png'),
95                ('pubsub', 'generic'):          (None, 'pubsub.png'),
96                ('proxy', 'bytestreams'):       (None, 'bytestreams.png'), # Socks5 FT proxy
97
98                # Transports
99                ('conference', 'irc'):          (False, 'irc.png'),
100                ('_jid', 'irc'):                                (False, 'irc.png'),
101                ('gateway', 'aim'):                     (False, 'aim.png'),
102                ('_jid', 'aim'):                                (False, 'aim.png'),
103                ('gateway', 'gadu-gadu'):       (False, 'gadu-gadu.png'),
104                ('_jid', 'gadugadu'):           (False, 'gadu-gadu.png'),
105                ('gateway', 'http-ws'):         (False, 'http-ws.png'),
106                ('gateway', 'icq'):                     (False, 'icq.png'),
107                ('_jid', 'icq'):                                (False, 'icq.png'),
108                ('gateway', 'msn'):                     (False, 'msn.png'),
109                ('_jid', 'msn'):                                (False, 'msn.png'),
110                ('gateway', 'sms'):                     (False, 'sms.png'),
111                ('_jid', 'sms'):                                (False, 'sms.png'),
112                ('gateway', 'smtp'):                    (False, 'mail.png'),
113                ('gateway', 'yahoo'):           (False, 'yahoo.png'),
114                ('_jid', 'yahoo'):                      (False, 'yahoo.png'),
115        }
116
117# Category type to "human-readable" description string, and sort priority
118_cat_to_descr = {
119        'other':                        (_('Others'),   2),
120        'gateway':              (_('Transports'),       0),
121        '_jid':                 (_('Transports'),       0),
122        #conference is a category for listing mostly groupchats in service discovery
123        'conference':   (_('Conference'),       1),
124}
125
126
127class CacheDictionary:
128        '''A dictionary that keeps items around for only a specific time.
129        Lifetime is in minutes. Getrefresh specifies whether to refresh when
130        an item is merely accessed instead of set aswell.'''
131        def __init__(self, lifetime, getrefresh = True):
132                self.lifetime = lifetime * 1000 * 60
133                self.getrefresh = getrefresh
134                self.cache = {}
135
136        class CacheItem:
137                '''An object to store cache items and their timeouts.'''
138                def __init__(self, value):
139                        self.value = value
140                        self.source = None
141
142                def __call__(self):
143                        return self.value
144
145        def _expire_timeout(self, key):
146                '''The timeout has expired, remove the object.'''
147                del self.cache[key]
148                return False
149
150        def _refresh_timeout(self, key):
151                '''The object was accessed, refresh the timeout.'''
152                item = self.cache[key]
153                if item.source:
154                        gobject.source_remove(item.source)
155                source = gobject.timeout_add(self.lifetime, self._expire_timeout, key)
156                item.source = source
157
158        def __getitem__(self, key):
159                item = self.cache[key]
160                if self.getrefresh:
161                        self._refresh_timeout(key)
162                return item()
163
164        def __setitem__(self, key, value):
165                item = self.CacheItem(value)
166                self.cache[key] = item
167                self._refresh_timeout(key)
168
169        def __delitem__(self, key):
170                item = self.cache[key]
171                if item.source:
172                        gobject.source_remove(item.source)
173                del self.cache[key]
174
175        def __contains__(self, key):
176                return key in self.cache
177        has_key = __contains__
178
179_icon_cache = CacheDictionary(15)
180
181def get_agent_address(jid, node = None):
182        '''Returns an agent's address for displaying in the GUI.'''
183        if node:
184                return '%s@%s' % (node, str(jid))
185        else:
186                return str(jid)
187
188class Closure(object):
189        '''A weak reference to a callback with arguments as an object.
190
191        Weak references to methods immediatly die, even if the object is still
192        alive. Besides a handy way to store a callback, this provides a workaround
193        that keeps a reference to the object instead.
194
195        Userargs and removeargs must be tuples.'''
196        def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
197                self.userargs = userargs
198                self.remove = remove
199                self.removeargs = removeargs
200                if inspect.ismethod(cb):
201                        self.meth_self = weakref.ref(cb.im_self, self._remove)
202                        self.meth_name = cb.func_name
203                elif callable(cb):
204                        self.meth_self = None
205                        self.cb = weakref.ref(cb, self._remove)
206                else:
207                        raise TypeError('Object is not callable')
208
209        def _remove(self, ref):
210                if self.remove:
211                        self.remove(self, *self.removeargs)
212
213        def __call__(self, *args, **kwargs):
214                if self.meth_self:
215                        obj = self.meth_self()
216                        cb = getattr(obj, self.meth_name)
217                else:
218                        cb = self.cb()
219                args = args + self.userargs
220                return cb(*args, **kwargs)
221
222
223class ServicesCache:
224        '''Class that caches our query results. Each connection will have it's own
225        ServiceCache instance.'''
226        def __init__(self, account):
227                self.account = account
228                self._items = CacheDictionary(15, getrefresh = False)
229                self._info = CacheDictionary(15, getrefresh = False)
230                self._cbs = {}
231
232        def _clean_closure(self, cb, type, addr):
233                # A closure died, clean up
234                cbkey = (type, addr)
235                try:
236                        self._cbs[cbkey].remove(cb)
237                except KeyError:
238                        return
239                except ValueError:
240                        return
241                # Clean an empty list
242                if not self._cbs[cbkey]:
243                        del self._cbs[cbkey]
244
245        def get_icon(self, identities = []):
246                '''Return the icon for an agent.'''
247                # Grab the first identity with an icon
248                for identity in identities:
249                        try:
250                                cat, type = identity['category'], identity['type']
251                                info = _agent_type_info[(cat, type)]
252                        except KeyError:
253                                continue
254                        filename = info[1]
255                        if filename:
256                                break
257                else:
258                        # Loop fell through, default to unknown
259                        cat = type = 0
260                        info = _agent_type_info[(0, 0)]
261                        filename = info[1]
262                if not filename: # we don't have an image to show for this type
263                        return
264                # Use the cache if possible
265                if filename in _icon_cache:
266                        return _icon_cache[filename]
267                # Or load it
268                filepath = os.path.join(gajim.DATA_DIR, 'pixmaps', 'agents', filename)
269                pix = gtk.gdk.pixbuf_new_from_file(filepath)
270                # Store in cache
271                _icon_cache[filename] = pix
272                return pix
273
274        def get_browser(self, identities = [], features = []):
275                '''Return the browser class for an agent.'''
276                # Grab the first identity with a browser
277                browser = None
278                for identity in identities:
279                        try:
280                                cat, type = identity['category'], identity['type']
281                                info = _agent_type_info[(cat, type)]
282                        except KeyError:
283                                continue
284                        browser = info[0]
285                        if browser is not None:
286                                break
287                # Note: possible outcome here is browser=False
288                if browser is None:
289                        # NS_BROWSE is deprecated, but we check for it anyways.
290                        # Some services list it in features and respond to
291                        # NS_DISCO_ITEMS anyways.
292                        # Allow browsing for unknown types aswell.
293                        if (not features and not identities) or\
294                                        xmpp.NS_DISCO_ITEMS in features or\
295                                        xmpp.NS_BROWSE in features:
296                                browser = AgentBrowser
297                return browser
298
299        def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()):
300                '''Get info for an agent.'''
301                addr = get_agent_address(jid, node)
302                # Check the cache
303                if self._info.has_key(addr):
304                        args = self._info[addr] + args
305                        cb(jid, node, *args)
306                        return
307                if nofetch:
308                        return
309
310                # Create a closure object
311                cbkey = ('info', addr)
312                cb = Closure(cb, userargs = args, remove = self._clean_closure,
313                                removeargs = cbkey)
314                # Are we already fetching this?
315                if self._cbs.has_key(cbkey):
316                        self._cbs[cbkey].append(cb)
317                else:
318                        self._cbs[cbkey] = [cb]
319                        gajim.connections[self.account].discoverInfo(jid, node)
320
321        def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()):
322                '''Get a list of items in an agent.'''
323                addr = get_agent_address(jid, node)
324                # Check the cache
325                if self._items.has_key(addr):
326                        args = (self._items[addr],) + args
327                        cb(jid, node, *args)
328                        return
329                if nofetch:
330                        return
331
332                # Create a closure object
333                cbkey = ('items', addr)
334                cb = Closure(cb, userargs = args, remove = self._clean_closure,
335                                removeargs = cbkey)
336                # Are we already fetching this?
337                if self._cbs.has_key(cbkey):
338                        self._cbs[cbkey].append(cb)
339                else:
340                        self._cbs[cbkey] = [cb]
341                        gajim.connections[self.account].discoverItems(jid, node)
342
343        def agent_info(self, jid, node, identities, features, data):
344                '''Callback for when we receive an agent's info.'''
345                addr = get_agent_address(jid, node)
346
347                # Store in cache
348                self._info[addr] = (identities, features, data)
349
350                # Call callbacks
351                cbkey = ('info', addr)
352                if self._cbs.has_key(cbkey):
353                        for cb in self._cbs[cbkey]:
354                                cb(jid, node, identities, features, data)
355                        # clean_closure may have beaten us to it
356                        if self._cbs.has_key(cbkey):
357                                del self._cbs[cbkey]
358
359        def agent_items(self, jid, node, items):
360                '''Callback for when we receive an agent's items.'''
361                addr = get_agent_address(jid, node)
362
363                # Store in cache
364                self._items[addr] = items
365
366                # Call callbacks
367                cbkey = ('items', addr)
368                if self._cbs.has_key(cbkey):
369                        for cb in self._cbs[cbkey]:
370                                cb(jid, node, items)
371                        # clean_closure may have beaten us to it
372                        if self._cbs.has_key(cbkey):
373                                del self._cbs[cbkey]
374
375        def agent_info_error(self, jid):
376                '''Callback for when a query fails. (even after the browse and agents
377                namespaces)'''
378                addr = get_agent_address(jid)
379
380                # Call callbacks
381                cbkey = ('info', addr)
382                if self._cbs.has_key(cbkey):
383                        for cb in self._cbs[cbkey]:
384                                cb(jid, '', 0, 0, 0)
385                        # clean_closure may have beaten us to it
386                        if self._cbs.has_key(cbkey):
387                                del self._cbs[cbkey]
388
389        def agent_items_error(self, jid):
390                '''Callback for when a query fails. (even after the browse and agents
391                namespaces)'''
392                addr = get_agent_address(jid)
393
394                # Call callbacks
395                cbkey = ('items', addr)
396                if self._cbs.has_key(cbkey):
397                        for cb in self._cbs[cbkey]:
398                                cb(jid, '', 0)
399                        # clean_closure may have beaten us to it
400                        if self._cbs.has_key(cbkey):
401                                del self._cbs[cbkey]
402
403
404class ServiceDiscoveryWindow:
405        '''Class that represents the Services Discovery window.'''
406        def __init__(self, account, jid = '', node = '',
407                        address_entry = False, parent = None):
408                self._account = account
409                self.parent = parent
410                if not jid:
411                        jid = gajim.config.get_per('accounts', account, 'hostname')
412                        node = ''
413
414                self.jid = None
415                self.browser = None
416                self.children = []
417                self.dying = False
418
419                # Check connection
420                if gajim.connections[account].connected < 2:
421                        dialogs.ErrorDialog(_('You are not connected to the server'),
422_('Without a connection, you can not browse available services')).get_response()
423                        raise RuntimeError, 'You must be connected to browse services'
424
425                # Get a ServicesCache object.
426                try:
427                        self.cache = gajim.connections[account].services_cache
428                except AttributeError:
429                        self.cache = ServicesCache(account)
430                        gajim.connections[account].services_cache = self.cache
431
432                self.xml = gtk.glade.XML(GTKGUI_GLADE, 'service_discovery_window', APP)
433                self.window = self.xml.get_widget('service_discovery_window')
434                self.services_treeview = self.xml.get_widget('services_treeview')
435                # This is more reliable than the cursor-changed signal.
436                selection = self.services_treeview.get_selection()
437                selection.connect_after('changed',
438                        self.on_services_treeview_selection_changed)
439                self.services_scrollwin = self.xml.get_widget('services_scrollwin')
440                self.progressbar = self.xml.get_widget('services_progressbar')
441                self.progressbar.set_no_show_all(True)
442                self.progressbar.hide()
443                self.banner = self.xml.get_widget('banner_agent_label')
444                self.banner_icon = self.xml.get_widget('banner_agent_icon')
445                self.banner_eventbox = self.xml.get_widget('banner_agent_eventbox')
446                self.paint_banner()
447                self.filter_hbox = self.xml.get_widget('filter_hbox')
448                self.filter_hbox.set_no_show_all(True)
449                self.filter_hbox.hide()
450                self.action_buttonbox = self.xml.get_widget('action_buttonbox')
451
452                # Address combobox
453                self.address_comboboxentry = None
454                address_hbox = self.xml.get_widget('address_hbox')
455                if address_entry:
456                        self.address_comboboxentry = self.xml.get_widget(
457                                'address_comboboxentry')
458                        self.address_comboboxentry_entry = self.address_comboboxentry.child
459                        self.address_comboboxentry_entry.set_activates_default(True)
460
461                        liststore = gtk.ListStore(str)
462                        self.address_comboboxentry.set_model(liststore)
463                        self.address_comboboxentry.set_text_column(0)
464                        self.latest_addresses = gajim.config.get(
465                                'latest_disco_addresses').split()
466                        jid = gajim.get_hostname_from_account(self.account, use_srv = True)
467                        if jid in self.latest_addresses:
468                                self.latest_addresses.remove(jid)
469                        self.latest_addresses.insert(0, jid)
470                        if len(self.latest_addresses) > 10:
471                                self.latest_addresses = self.latest_addresses[0:10]
472                        for j in self.latest_addresses:
473                                self.address_comboboxentry.append_text(j)
474                        self.address_comboboxentry.child.set_text(jid)
475                else:
476                        # Don't show it at all if we didn't ask for it
477                        address_hbox.set_no_show_all(True)
478                        address_hbox.hide()
479
480                self._initial_state()
481                self.xml.signal_autoconnect(self)
482                self.travel(jid, node)
483                self.window.show_all()
484
485        def _get_account(self):
486                return self._account
487
488        def _set_account(self, value):
489                self._account = value
490                self.cache.account = value
491                if self.browser:
492                        self.browser.account = value
493        account = property(_get_account, _set_account)
494
495        def _initial_state(self):
496                '''Set some initial state on the window. Separated in a method because
497                it's handy to use within browser's cleanup method.'''
498                self.progressbar.hide()
499                self.window.set_title(_('Service Discovery'))
500                self.banner.set_markup('<span weight="heavy" size="large">'\
501                        '%s</span>\n' % _('Service Discovery'))
502                # FIXME: use self.banner_icon.clear() when we switch to GTK 2.8
503                self.banner_icon.set_from_file(None)
504                self.banner_icon.hide()         # Just clearing it doesn't work
505
506        def paint_banner(self):
507                '''Repaint the banner with theme color'''
508                theme = gajim.config.get('roster_theme')
509                bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
510                textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
511                if bgcolor:
512                        color = gtk.gdk.color_parse(bgcolor)
513                else:
514                        color = None
515                self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, color)
516
517                if textcolor:
518                        color = gtk.gdk.color_parse(textcolor)
519                else:
520                        color = None
521                self.banner.modify_fg(gtk.STATE_NORMAL, color)
522                if self.browser:
523                        self.browser.update_theme()
524
525        def destroy(self, chain = False):
526                '''Close the browser. This can optionally close it's children and
527                propagate to the parent. This should happen on actions like register,
528                or join to kill off the entire browser chain.'''
529                if self.dying:
530                        return
531                self.dying = True
532
533                # self.browser._get_agent_address() would break when no browser.
534                addr = get_agent_address(self.jid, self.node)
535                del gajim.interface.instances[self.account]['disco'][addr]
536
537                if self.browser:
538                        self.window.hide()
539                        self.browser.cleanup()
540                        self.browser = None
541                self.window.destroy()
542
543                for child in self.children[:]:
544                        child.parent = None
545                        if chain:
546                                child.destroy(chain = chain)
547                                self.children.remove(child)
548                if self.parent:
549                        self.parent.children.remove(self)
550                        if chain and not self.parent.children:
551                                self.parent.destroy(chain = chain)
552                                self.parent = None
553
554        def travel(self, jid, node):
555                '''Travel to an agent within the current services window.'''
556                if self.browser:
557                        self.browser.cleanup()
558                        self.browser = None
559                # Update the window list
560                if self.jid:
561                        old_addr = get_agent_address(self.jid, self.node)
562                        if gajim.interface.instances[self.account]['disco'].has_key(old_addr):
563                                del gajim.interface.instances[self.account]['disco'][old_addr]
564                addr = get_agent_address(jid, node)
565                gajim.interface.instances[self.account]['disco'][addr] = self
566                # We need to store these, self.browser is not always available.
567                self.jid = jid
568                self.node = node
569                self.cache.get_info(jid, node, self._travel)
570
571        def _travel(self, jid, node, identities, features, data):
572                '''Continuation of travel.'''
573                if self.dying or jid != self.jid or node != self.node:
574                        return
575                if not identities:
576                        if not self.address_comboboxentry:
577                                # We can't travel anywhere else.
578                                self.destroy()
579                        dialogs.ErrorDialog(_('The service could not be found'),
580_('There is no service at the address you entered, or it is not responding. Check the address and try again.')).get_response()
581                        return
582                klass = self.cache.get_browser(identities, features)
583                if not klass:
584                        dialogs.ErrorDialog(_('The service is not browsable'),
585_('This type of service does not contain any items to browse.')).get_response()
586                        return
587                elif klass is None:
588                        klass = AgentBrowser
589                self.browser = klass(self.account, jid, node)
590                self.browser.prepare_window(self)
591                self.browser.browse()
592
593        def open(self, jid, node):
594                '''Open an agent. By default, this happens in a new window.'''
595                try:
596                        win = gajim.interface.instances[self.account]['disco']\
597                                [get_agent_address(jid, node)]
598                        win.window.present()
599                        return
600                except KeyError:
601                        pass
602                try:
603                        win = ServiceDiscoveryWindow(self.account, jid, node, parent=self)
604                except RuntimeError:
605                        # Disconnected, perhaps
606                        return
607                self.children.append(win)
608
609        def on_service_discovery_window_destroy(self, widget):
610                self.destroy()
611
612        def on_close_button_clicked(self, widget):
613                self.destroy()
614
615        def on_address_comboboxentry_changed(self, widget):
616                if self.address_comboboxentry.get_active() != -1:
617