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

Revision 6407, 54.4 kB (checked in by dkirov, 2 years ago)

r6265, r6266, r6267, r6269, r6350, r6366

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
55import pango
56
57import dialogs
58import tooltips
59import gtkgui_helpers
60
61from common import gajim
62from common import xmpp
63from common import i18n
64
65_ = i18n._
66APP = i18n.APP
67gtk.glade.bindtextdomain (APP, i18n.DIR)
68gtk.glade.textdomain (APP)
69
70# Dictionary mapping category, type pairs to browser class, image pairs.
71# This is a function, so we can call it after the classes are declared.
72# For the browser class, None means that the service will only be browsable
73# when it advertises disco as it's feature, False means it's never browsable.
74def _gen_agent_type_info():
75        return {
76                # Defaults
77                (0, 0):                                                 (None, None),
78
79                # Jabber server
80                ('server', 'im'):                               (ToplevelAgentBrowser, 'jabber.png'),
81                ('services', 'jabber'):         (ToplevelAgentBrowser, 'jabber.png'),
82
83                # Services
84                ('conference', 'text'):         (MucBrowser, 'conference.png'),
85                ('headline', 'rss'):                    (AgentBrowser, 'rss.png'),
86                ('headline', 'weather'):        (False, 'weather.png'),
87                ('gateway', 'weather'):         (False, 'weather.png'),
88                ('_jid', 'weather'):                    (False, 'weather.png'),
89                ('gateway', 'sip'):                     (False, 'sip.png'),
90                ('directory', 'user'):          (None, 'jud.png'),
91                ('pubsub', 'generic'):          (None, 'pubsub.png'),
92                ('proxy', 'bytestreams'):       (None, 'bytestreams.png'), # Socks5 FT proxy
93
94                # Transports
95                ('conference', 'irc'):          (False, 'irc.png'),
96                ('_jid', 'irc'):                                (False, 'irc.png'),
97                ('gateway', 'aim'):                     (False, 'aim.png'),
98                ('_jid', 'aim'):                                (False, 'aim.png'),
99                ('gateway', 'gadu-gadu'):       (False, 'gadu-gadu.png'),
100                ('_jid', 'gadugadu'):           (False, 'gadu-gadu.png'),
101                ('gateway', 'http-ws'):         (False, 'http-ws.png'),
102                ('gateway', 'icq'):                     (False, 'icq.png'),
103                ('_jid', 'icq'):                                (False, 'icq.png'),
104                ('gateway', 'msn'):                     (False, 'msn.png'),
105                ('_jid', 'msn'):                                (False, 'msn.png'),
106                ('gateway', 'sms'):                     (False, 'sms.png'),
107                ('_jid', 'sms'):                                (False, 'sms.png'),
108                ('gateway', 'smtp'):                    (False, 'mail.png'),
109                ('gateway', 'yahoo'):           (False, 'yahoo.png'),
110                ('_jid', 'yahoo'):                      (False, 'yahoo.png'),
111        }
112
113# Category type to "human-readable" description string, and sort priority
114_cat_to_descr = {
115        'other':                        (_('Others'),   2),
116        'gateway':              (_('Transports'),       0),
117        '_jid':                 (_('Transports'),       0),
118        #conference is a category for listing mostly groupchats in service discovery
119        'conference':   (_('Conference'),       1),
120}
121
122
123class CacheDictionary:
124        '''A dictionary that keeps items around for only a specific time.
125        Lifetime is in minutes. Getrefresh specifies whether to refresh when
126        an item is merely accessed instead of set aswell.'''
127        def __init__(self, lifetime, getrefresh = True):
128                self.lifetime = lifetime * 1000 * 60
129                self.getrefresh = getrefresh
130                self.cache = {}
131
132        class CacheItem:
133                '''An object to store cache items and their timeouts.'''
134                def __init__(self, value):
135                        self.value = value
136                        self.source = None
137
138                def __call__(self):
139                        return self.value
140
141        def _expire_timeout(self, key):
142                '''The timeout has expired, remove the object.'''
143                del self.cache[key]
144                return False
145
146        def _refresh_timeout(self, key):
147                '''The object was accessed, refresh the timeout.'''
148                item = self.cache[key]
149                if item.source:
150                        gobject.source_remove(item.source)
151                source = gobject.timeout_add(self.lifetime, self._expire_timeout, key)
152                item.source = source
153
154        def __getitem__(self, key):
155                item = self.cache[key]
156                if self.getrefresh:
157                        self._refresh_timeout(key)
158                return item()
159
160        def __setitem__(self, key, value):
161                item = self.CacheItem(value)
162                self.cache[key] = item
163                self._refresh_timeout(key)
164
165        def __delitem__(self, key):
166                item = self.cache[key]
167                if item.source:
168                        gobject.source_remove(item.source)
169                del self.cache[key]
170
171        def __contains__(self, key):
172                return key in self.cache
173        has_key = __contains__
174
175_icon_cache = CacheDictionary(15)
176
177def get_agent_address(jid, node = None):
178        '''Returns an agent's address for displaying in the GUI.'''
179        if node:
180                return '%s@%s' % (node, str(jid))
181        else:
182                return str(jid)
183
184class Closure(object):
185        '''A weak reference to a callback with arguments as an object.
186
187        Weak references to methods immediatly die, even if the object is still
188        alive. Besides a handy way to store a callback, this provides a workaround
189        that keeps a reference to the object instead.
190
191        Userargs and removeargs must be tuples.'''
192        def __init__(self, cb, userargs = (), remove = None, removeargs = ()):
193                self.userargs = userargs
194                self.remove = remove
195                self.removeargs = removeargs
196                if inspect.ismethod(cb):
197                        self.meth_self = weakref.ref(cb.im_self, self._remove)
198                        self.meth_name = cb.func_name
199                elif callable(cb):
200                        self.meth_self = None
201                        self.cb = weakref.ref(cb, self._remove)
202                else:
203                        raise TypeError('Object is not callable')
204
205        def _remove(self, ref):
206                if self.remove:
207                        self.remove(self, *self.removeargs)
208
209        def __call__(self, *args, **kwargs):
210                if self.meth_self:
211                        obj = self.meth_self()
212                        cb = getattr(obj, self.meth_name)
213                else:
214                        cb = self.cb()
215                args = args + self.userargs
216                return cb(*args, **kwargs)
217
218
219class ServicesCache:
220        '''Class that caches our query results. Each connection will have it's own
221        ServiceCache instance.'''
222        def __init__(self, account):
223                self.account = account
224                self._items = CacheDictionary(15, getrefresh = False)
225                self._info = CacheDictionary(15, getrefresh = False)
226                self._cbs = {}
227
228        def _clean_closure(self, cb, type, addr):
229                # A closure died, clean up
230                cbkey = (type, addr)
231                try:
232                        self._cbs[cbkey].remove(cb)
233                except KeyError:
234                        return
235                except ValueError:
236                        return
237                # Clean an empty list
238                if not self._cbs[cbkey]:
239                        del self._cbs[cbkey]
240
241        def get_icon(self, identities = []):
242                '''Return the icon for an agent.'''
243                # Grab the first identity with an icon
244                for identity in identities:
245                        try:
246                                cat, type = identity['category'], identity['type']
247                                info = _agent_type_info[(cat, type)]
248                        except KeyError:
249                                continue
250                        filename = info[1]
251                        if filename:
252                                break
253                else:
254                        # Loop fell through, default to unknown
255                        cat = type = 0
256                        info = _agent_type_info[(0, 0)]
257                        filename = info[1]
258                if not filename: # we don't have an image to show for this type
259                        return
260                # Use the cache if possible
261                if filename in _icon_cache:
262                        return _icon_cache[filename]
263                # Or load it
264                filepath = os.path.join(gajim.DATA_DIR, 'pixmaps', 'agents', filename)
265                pix = gtk.gdk.pixbuf_new_from_file(filepath)
266                # Store in cache
267                _icon_cache[filename] = pix
268                return pix
269
270        def get_browser(self, identities = [], features = []):
271                '''Return the browser class for an agent.'''
272                # Grab the first identity with a browser
273                browser = None
274                for identity in identities:
275                        try:
276                                cat, type = identity['category'], identity['type']
277                                info = _agent_type_info[(cat, type)]
278                        except KeyError:
279                                continue
280                        browser = info[0]
281                        if browser is not None:
282                                break
283                # Note: possible outcome here is browser=False
284                if browser is None:
285                        # NS_BROWSE is deprecated, but we check for it anyways.
286                        # Some services list it in features and respond to
287                        # NS_DISCO_ITEMS anyways.
288                        # Allow browsing for unknown types aswell.
289                        if (not features and not identities) or\
290                                        xmpp.NS_DISCO_ITEMS in features or\
291                                        xmpp.NS_BROWSE in features:
292                                browser = AgentBrowser
293                return browser
294
295        def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()):
296                '''Get info for an agent.'''
297                addr = get_agent_address(jid, node)
298                # Check the cache
299                if self._info.has_key(addr):
300                        args = self._info[addr] + args
301                        cb(jid, node, *args)
302                        return
303                if nofetch:
304                        return
305
306                # Create a closure object
307                cbkey = ('info', addr)
308                cb = Closure(cb, userargs = args, remove = self._clean_closure,
309                                removeargs = cbkey)
310                # Are we already fetching this?
311                if self._cbs.has_key(cbkey):
312                        self._cbs[cbkey].append(cb)
313                else:
314                        self._cbs[cbkey] = [cb]
315                        gajim.connections[self.account].discoverInfo(jid, node)
316
317        def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()):
318                '''Get a list of items in an agent.'''
319                addr = get_agent_address(jid, node)
320                # Check the cache
321                if self._items.has_key(addr):
322                        args = (self._items[addr],) + args
323                        cb(jid, node, *args)
324                        return
325                if nofetch:
326                        return
327
328                # Create a closure object
329                cbkey = ('items', addr)
330                cb = Closure(cb, userargs = args, remove = self._clean_closure,
331                                removeargs = cbkey)
332                # Are we already fetching this?
333                if self._cbs.has_key(cbkey):
334                        self._cbs[cbkey].append(cb)
335                else:
336                        self._cbs[cbkey] = [cb]
337                        gajim.connections[self.account].discoverItems(jid, node)
338
339        def agent_info(self, jid, node, identities, features, data):
340                '''Callback for when we receive an agent's info.'''
341                addr = get_agent_address(jid, node)
342
343                # Store in cache
344                self._info[addr] = (identities, features, data)
345
346                # Call callbacks
347                cbkey = ('info', addr)
348                if self._cbs.has_key(cbkey):
349                        for cb in self._cbs[cbkey]:
350                                cb(jid, node, identities, features, data)
351                        # clean_closure may have beaten us to it
352                        if self._cbs.has_key(cbkey):
353                                del self._cbs[cbkey]
354
355        def agent_items(self, jid, node, items):
356                '''Callback for when we receive an agent's items.'''
357                addr = get_agent_address(jid, node)
358
359                # Store in cache
360                self._items[addr] = items
361
362                # Call callbacks
363                cbkey = ('items', addr)
364                if self._cbs.has_key(cbkey):
365                        for cb in self._cbs[cbkey]:
366                                cb(jid, node, items)
367                        # clean_closure may have beaten us to it
368                        if self._cbs.has_key(cbkey):
369                                del self._cbs[cbkey]
370
371        def agent_info_error(self, jid):
372                '''Callback for when a query fails. (even after the browse and agents
373                namespaces)'''
374                addr = get_agent_address(jid)
375
376                # Call callbacks
377                cbkey = ('info', addr)
378                if self._cbs.has_key(cbkey):
379                        for cb in self._cbs[cbkey]:
380                                cb(jid, '', 0, 0, 0)
381                        # clean_closure may have beaten us to it
382                        if self._cbs.has_key(cbkey):
383                                del self._cbs[cbkey]
384
385        def agent_items_error(self, jid):
386                '''Callback for when a query fails. (even after the browse and agents
387                namespaces)'''
388                addr = get_agent_address(jid)
389
390                # Call callbacks
391                cbkey = ('items', addr)
392                if self._cbs.has_key(cbkey):
393                        for cb in self._cbs[cbkey]:
394                                cb(jid, '', 0)
395                        # clean_closure may have beaten us to it
396                        if self._cbs.has_key(cbkey):
397                                del self._cbs[cbkey]
398
399# object is needed so that property() works
400class ServiceDiscoveryWindow(object):
401        '''Class that represents the Services Discovery window.'''
402        def __init__(self, account, jid = '', node = '',
403                        address_entry = False, parent = None):
404                self._account = account
405                self.parent = parent
406                if not jid:
407                        jid = gajim.config.get_per('accounts', account, 'hostname')
408                        node = ''
409
410                self.jid = None
411                self.browser = None
412                self.children = []
413                self.dying = False
414
415                # Check connection
416                if gajim.connections[account].connected < 2:
417                        dialogs.ErrorDialog(_('You are not connected to the server'),
418_('Without a connection, you can not browse available services'))
419                        raise RuntimeError, 'You must be connected to browse services'
420
421                # Get a ServicesCache object.
422                try:
423                        self.cache = gajim.connections[account].services_cache
424                except AttributeError:
425                        self.cache = ServicesCache(account)
426                        gajim.connections[account].services_cache = self.cache
427
428                self.xml = gtkgui_helpers.get_glade('service_discovery_window.glade')
429                self.window = self.xml.get_widget('service_discovery_window')
430                self.services_treeview = self.xml.get_widget('services_treeview')
431                # This is more reliable than the cursor-changed signal.
432                selection = self.services_treeview.get_selection()
433                selection.connect_after('changed',
434                        self.on_services_treeview_selection_changed)
435                self.services_scrollwin = self.xml.get_widget('services_scrollwin')
436                self.progressbar = self.xml.get_widget('services_progressbar')
437                self.progressbar.set_no_show_all(True)
438                self.progressbar.hide()
439                self.banner = self.xml.get_widget('banner_agent_label')
440                self.banner_icon = self.xml.get_widget('banner_agent_icon')
441                self.banner_eventbox = self.xml.get_widget('banner_agent_eventbox')
442                self.style_event_id = 0
443                self.banner.realize()
444                self.paint_banner()
445                self.filter_hbox = self.xml.get_widget('filter_hbox')
446                self.filter_hbox.set_no_show_all(True)
447                self.filter_hbox.hide()
448                self.action_buttonbox = self.xml.get_widget('action_buttonbox')
449
450                # Address combobox
451                self.address_comboboxentry = None
452                address_hbox = self.xml.get_widget('address_hbox')
453                if address_entry:
454                        self.address_comboboxentry = self.xml.get_widget(
455                                'address_comboboxentry')
456                        self.address_comboboxentry_entry = self.address_comboboxentry.child
457                        self.address_comboboxentry_entry.set_activates_default(True)
458
459                        liststore = gtk.ListStore(str)
460                        self.address_comboboxentry.set_model(liststore)
461                        self.address_comboboxentry.set_text_column(0)
462                        self.latest_addresses = gajim.config.get(
463                                'latest_disco_addresses').split()
464                        jid = gajim.get_hostname_from_account(self.account)
465                        if jid in self.latest_addresses:
466                                self.latest_addresses.remove(jid)
467                        self.latest_addresses.insert(0, jid)
468                        if len(self.latest_addresses) > 10:
469                                self.latest_addresses = self.latest_addresses[0:10]
470                        for j in self.latest_addresses:
471                                self.address_comboboxentry.append_text(j)
472                        self.address_comboboxentry.child.set_text(jid)
473                else:
474                        # Don't show it at all if we didn't ask for it
475                        address_hbox.set_no_show_all(True)
476                        address_hbox.hide()
477
478                self._initial_state()
479                self.xml.signal_autoconnect(self)
480                self.travel(jid, node)
481                self.window.show_all()
482
483        def _get_account(self):
484                return self._account
485
486        def _set_account(self, value):
487                self._account = value
488                self.cache.account = value
489                if self.browser:
490                        self.browser.account = value
491        account = property(_get_account, _set_account)
492
493        def _initial_state(self):
494                '''Set some initial state on the window. Separated in a method because
495                it's handy to use within browser's cleanup method.'''
496                self.progressbar.hide()
497                self.window.set_title(_('Service Discovery using account %s') % self.account)
498                self._set_window_banner_text(_('Service Discovery'))
499                # FIXME: use self.banner_icon.clear() when we switch to GTK 2.8
500                self.banner_icon.set_from_file(None)
501                self.banner_icon.hide()         # Just clearing it doesn't work
502
503        def _set_window_banner_text(self, text, text_after = None):
504                theme = gajim.config.get('roster_theme')
505                bannerfont = gajim.config.get_per('themes', theme, 'bannerfont')
506                bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs')
507               
508                if bannerfont:
509                        font = pango.FontDescription(bannerfont)
510                else:
511                        font = pango.FontDescription('Normal')
512                if bannerfontattrs:
513                        # B is attribute set by default
514                        if 'B' in bannerfontattrs:
515                                font.set_weight(pango.WEIGHT_HEAVY)
516                        if 'I' in bannerfontattrs:
517                                font.set_style(pango.STYLE_ITALIC)
518               
519                font_attrs = 'font_desc="%s"' % font.to_string()
520                font_size = font.get_size()
521               
522                # in case there is no font specified we use x-large font size
523                if font_size == 0:
524                        font_attrs = '%s size="large"' % font_attrs
525                markup = '<span %s>%s</span>' % (font_attrs, text)
526                if text_after:
527                        font.set_weight(pango.WEIGHT_NORMAL)
528                        markup = '%s\n<span font_desc="%s" size="small">%s</span>' % \
529                                                                        (markup, font.to_string(), text_after)
530                self.banner.set_markup(markup)
531       
532        def paint_banner(self):
533                '''Repaint the banner with theme color'''
534                theme = gajim.config.get('roster_theme')
535                bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor')
536                textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor')
537                self.disconnect_style_event()
538                if bgcolor:
539                        color = gtk.gdk.color_parse(bgcolor)
540                        self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, color)
541                        default_bg = False
542                else:
543                        default_bg = True
544               
545                if textcolor:
546                        color = gtk.gdk.color_parse(textcolor)
547                        self.banner.modify_fg(gtk.STATE_NORMAL, color)
548                        default_fg = False
549                else:
550                        default_fg = True
551                if default_fg or default_bg:
552                        self._on_style_set_event(self.banner, None, default_fg, default_bg)
553                if self.browser:
554                        self.browser.update_theme()
555       
556        def disconnect_style_event(self):
557                if self.style_event_id:
558                        self.banner.disconnect(self.style_event_id)
559                        self.style_event_id = 0
560       
561        def connect_style_event(self, set_fg = False, set_bg = False):
562                self.disconnect_style_event()
563                self.style_event_id = self.banner.connect('style-set', 
564                                        self._on_style_set_event, set_fg, set_bg)
565       
566        def _on_style_set_event(self, widget, style, *opts):
567                ''' set style of widget from style class *.Frame.Eventbox
568                        opts[0] == True -> set fg color
569                        opts[1] == True -> set bg color '''
570               
571                self.disconnect_style_event()
572                if opts[1]:
573                        bg_color = widget.style.bg[gtk.STATE_SELECTED]
574                        self.banner_eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
575                if opts[0]:
576                        fg_color = widget.style.fg[gtk.STATE_SELECTED]
577                        self.banner.modify_fg(gtk.STATE_NORMAL, fg_color)
578                self.banner.ensure_style()
579                self.connect_style_event(opts[0], opts[1])
580       
581        def destroy(self, chain = False):
582                '''Close the browser. This can optionally close it's children and
583                propagate to the parent. This should happen on actions like register,
584                or join to kill off the entire browser chain.'''
585                if self.dying:
586                        return
587                self.dying = True
588
589                # self.browser._get_agent_address() would break when no browser.
590                addr = get_agent_address(self.jid, self.node)
591                del gajim.interface.instances[self.account]['disco'][addr]
592
593                if self.browser:
594                        self.window.hide()
595                        self.browser.cleanup()
596                        self.browser = None
597                self.window.destroy()
598
599                for child in self.children[:]:
600                        child.parent = None
601                        if chain:
602                                child.destroy(chain = chain)
603                                self.children.remove(child)
604                if self.parent:
605                        self.parent.children.remove(self)
606                        if chain and not self.parent.children:
607                                self.parent.destroy(chain = chain)
608                                self.parent = None
609
610        def travel(self