root/branches/gajim_0.11.1/src/common/helpers.py

Revision 8699, 27.4 kB (checked in by asterix, 15 months ago)

Handle malformed timestamps (which would cause an unhandled exception and hork the connection) and XEP-82 (dashes in timestamps).

  • Property svn:eol-style set to LF
Line 
1##      common/helpers.py
2##
3## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
4## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
5## Copyright (C) 2005
6##                    Dimitur Kirov <dkirov@gmail.com>
7##                    Travis Shirk <travis@pobox.com>
8##
9## This program is free software; you can redistribute it and/or modify
10## it under the terms of the GNU General Public License as published
11## by the Free Software Foundation; version 2 only.
12##
13## This program is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16## GNU General Public License for more details.
17##
18
19import re
20import locale
21import os
22import subprocess
23import urllib
24import errno
25import select
26import sha
27from encodings.punycode import punycode_encode
28from encodings import idna
29
30import gajim
31from i18n import Q_
32from i18n import ngettext
33from xmpp_stringprep import nodeprep, resourceprep, nameprep
34
35
36try:
37        import winsound # windows-only built-in module for playing wav
38        import win32api
39        import win32con
40except:
41        pass
42
43special_groups = (_('Transports'), _('Not in Roster'), _('Observers'))
44
45class InvalidFormat(Exception):
46        pass
47
48def decompose_jid(jidstring):
49        user = None
50        server = None
51        resource = None
52
53        # Search for delimiters
54        user_sep = jidstring.find('@')
55        res_sep = jidstring.find('/')
56
57        if user_sep == -1:
58                if res_sep == -1:
59                        # host
60                        server = jidstring
61                else:
62                        # host/resource
63                        server = jidstring[0:res_sep]
64                        resource = jidstring[res_sep + 1:] or None
65        else:
66                if res_sep == -1:
67                        # user@host
68                        user = jidstring[0:user_sep] or None
69                        server = jidstring[user_sep + 1:]
70                else:
71                        if user_sep < res_sep:
72                                # user@host/resource
73                                user = jidstring[0:user_sep] or None
74                                server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
75                                resource = jidstring[res_sep + 1:] or None
76                        else:
77                                # server/resource (with an @ in resource)
78                                server = jidstring[0:res_sep]
79                                resource = jidstring[res_sep + 1:] or None
80        return user, server, resource
81
82def parse_jid(jidstring):
83        '''Perform stringprep on all JID fragments from a string
84        and return the full jid'''
85        # This function comes from http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
86
87        return prep(*decompose_jid(jidstring))
88
89def idn_to_ascii(host):
90        '''convert IDN (Internationalized Domain Names) to ACE (ASCII-compatible encoding)'''
91        labels = idna.dots.split(host)
92        converted_labels = []
93        for label in labels:
94                converted_labels.append(idna.ToASCII(label))
95        return ".".join(converted_labels)
96
97def parse_resource(resource):
98        '''Perform stringprep on resource and return it'''
99        if resource:
100                try:
101                        return resourceprep.prepare(unicode(resource))
102                except UnicodeError:
103                        raise InvalidFormat, 'Invalid character in resource.'
104
105def prep(user, server, resource):
106        '''Perform stringprep on all JID fragments and return the full jid'''
107        # This function comes from
108        #http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
109
110        if user:
111                try:
112                        user = nodeprep.prepare(unicode(user))
113                except UnicodeError:
114                        raise InvalidFormat, _('Invalid character in username.')
115        else:
116                user = None
117
118        if not server:
119                raise InvalidFormat, _('Server address required.')
120        else:
121                try:
122                        server = nameprep.prepare(unicode(server))
123                except UnicodeError:
124                        raise InvalidFormat, _('Invalid character in hostname.')
125
126        if resource:
127                try:
128                        resource = resourceprep.prepare(unicode(resource))
129                except UnicodeError:
130                        raise InvalidFormat, _('Invalid character in resource.')
131        else:
132                resource = None
133
134        if user:
135                if resource:
136                        return '%s@%s/%s' % (user, server, resource)
137                else:
138                        return '%s@%s' % (user, server)
139        else:
140                if resource:
141                        return '%s/%s' % (server, resource)
142                else:
143                        return server
144
145def temp_failure_retry(func, *args, **kwargs):
146        while True:
147                try:
148                        return func(*args, **kwargs)
149                except (os.error, IOError, select.error), ex:
150                        if ex.errno == errno.EINTR:
151                                continue
152                        else:
153                                raise
154
155def convert_bytes(string):
156        suffix = ''
157        # IEC standard says KiB = 1024 bytes KB = 1000 bytes
158        # but do we use the standard?
159        use_kib_mib = gajim.config.get('use_kib_mib')
160        align = 1024.
161        bytes = float(string)
162        if bytes >= align:
163                bytes = round(bytes/align, 1)
164                if bytes >= align:
165                        bytes = round(bytes/align, 1)
166                        if bytes >= align:
167                                bytes = round(bytes/align, 1)
168                                if use_kib_mib:
169                                        #GiB means gibibyte
170                                        suffix = _('%s GiB')
171                                else:
172                                        #GB means gigabyte
173                                        suffix = _('%s GB')
174                        else:
175                                if use_kib_mib:
176                                        #MiB means mibibyte
177                                        suffix = _('%s MiB')
178                                else:
179                                        #MB means megabyte
180                                        suffix = _('%s MB')
181                else:
182                        if use_kib_mib:
183                                        #KiB means kibibyte
184                                        suffix = _('%s KiB')
185                        else:
186                                #KB means kilo bytes
187                                suffix = _('%s KB')
188        else:
189                #B means bytes
190                suffix = _('%s B')
191        return suffix % unicode(bytes)
192
193
194def get_contact_dict_for_account(account):
195        ''' create a dict of jid, nick -> contact with all contacts of account.
196        Can be used for completion lists'''
197        contacts_dict = {}
198        for jid in gajim.contacts.get_jid_list(account):
199                contact = gajim.contacts.get_contact_with_highest_priority(account,
200                                jid)
201                contacts_dict[jid] = contact
202                name = contact.name
203                if contacts_dict.has_key(name):
204                        contact1 = contacts_dict[name]
205                        del contacts_dict[name]
206                        contacts_dict['%s (%s)' % (name, contact1.jid)] = contact1
207                        contacts_dict['%s (%s)' % (name, jid)] = contact
208                else:
209                        if contact.name == gajim.get_nick_from_jid(jid):
210                                del contacts_dict[jid]
211                        contacts_dict[name] = contact
212        return contacts_dict
213
214def get_uf_show(show, use_mnemonic = False):
215        '''returns a userfriendly string for dnd/xa/chat
216        and makes all strings translatable
217        if use_mnemonic is True, it adds _ so GUI should call with True
218        for accessibility issues'''
219        if show == 'dnd':
220                if use_mnemonic:
221                        uf_show = _('_Busy')
222                else:
223                        uf_show = _('Busy')
224        elif show == 'xa':
225                if use_mnemonic:
226                        uf_show = _('_Not Available')
227                else:
228                        uf_show = _('Not Available')
229        elif show == 'chat':
230                if use_mnemonic:
231                        uf_show = _('_Free for Chat')
232                else:
233                        uf_show = _('Free for Chat')
234        elif show == 'online':
235                if use_mnemonic:
236                        uf_show = _('_Available')
237                else:
238                        uf_show = _('Available')
239        elif show == 'connecting':
240                        uf_show = _('Connecting')
241        elif show == 'away':
242                if use_mnemonic:
243                        uf_show = _('A_way')
244                else:
245                        uf_show = _('Away')
246        elif show == 'offline':
247                if use_mnemonic:
248                        uf_show = _('_Offline')
249                else:
250                        uf_show = _('Offline')
251        elif show == 'invisible':
252                if use_mnemonic:
253                        uf_show = _('_Invisible')
254                else:
255                        uf_show = _('Invisible')
256        elif show == 'not in roster':
257                uf_show = _('Not in Roster')
258        elif show == 'requested':
259                uf_show = Q_('?contact has status:Unknown')
260        else:
261                uf_show = Q_('?contact has status:Has errors')
262        return unicode(uf_show)
263
264def get_uf_sub(sub):
265        if sub == 'none':
266                uf_sub = Q_('?Subscription we already have:None')
267        elif sub == 'to':
268                uf_sub = _('To')
269        elif sub == 'from':
270                uf_sub = _('From')
271        elif sub == 'both':
272                uf_sub = _('Both')
273        else:
274                uf_sub = sub
275
276        return unicode(uf_sub)
277
278def get_uf_ask(ask):
279        if ask is None:
280                uf_ask = Q_('?Ask (for Subscription):None')
281        elif ask == 'subscribe':
282                uf_ask = _('Subscribe')
283        else:
284                uf_ask = ask
285
286        return unicode(uf_ask)
287
288def get_uf_role(role, plural = False):
289        ''' plural determines if you get Moderators or Moderator'''
290        if role == 'none':
291                role_name = Q_('?Group Chat Contact Role:None')
292        elif role == 'moderator':
293                if plural:
294                        role_name = _('Moderators')
295                else:
296                        role_name = _('Moderator')
297        elif role == 'participant':
298                if plural:
299                        role_name = _('Participants')
300                else:
301                        role_name = _('Participant')
302        elif role == 'visitor':
303                if plural:
304                        role_name = _('Visitors')
305                else:
306                        role_name = _('Visitor')
307        return role_name
308       
309def get_uf_affiliation(affiliation):
310        '''Get a nice and translated affilition for muc'''
311        if affiliation == 'none': 
312                affiliation_name = Q_('?Group Chat Contact Affiliation:None')
313        elif affiliation == 'owner':
314                affiliation_name = _('Owner')
315        elif affiliation == 'admin':
316                affiliation_name = _('Administrator')
317        elif affiliation == 'member':
318                affiliation_name = _('Member')
319        else: # Argl ! An unknown affiliation !
320                affiliation_name = affiliation.capitalize()
321        return affiliation_name
322
323
324def get_sorted_keys(adict):
325        keys = adict.keys()
326        keys.sort()
327        return keys
328
329def to_one_line(msg):
330        msg = msg.replace('\\', '\\\\')
331        msg = msg.replace('\n', '\\n')
332        # s1 = 'test\ntest\\ntest'
333        # s11 = s1.replace('\\', '\\\\')
334        # s12 = s11.replace('\n', '\\n')
335        # s12
336        # 'test\\ntest\\\\ntest'
337        return msg
338
339def from_one_line(msg):
340        # (?<!\\) is a lookbehind assertion which asks anything but '\'
341        # to match the regexp that follows it
342
343        # So here match '\\n' but not if you have a '\' before that
344        expr = re.compile(r'(?<!\\)\\n')
345        msg = expr.sub('\n', msg)
346        msg = msg.replace('\\\\', '\\')
347        # s12 = 'test\\ntest\\\\ntest'
348        # s13 = re.sub('\n', s12)
349        # s14 s13.replace('\\\\', '\\')
350        # s14
351        # 'test\ntest\\ntest'
352        return msg
353
354def get_uf_chatstate(chatstate):
355        '''removes chatstate jargon and returns user friendly messages'''
356        if chatstate == 'active':
357                return _('is paying attention to the conversation')
358        elif chatstate == 'inactive':
359                return _('is doing something else')
360        elif chatstate == 'composing':
361                return _('is composing a message...')
362        elif chatstate == 'paused':
363                #paused means he or she was composing but has stopped for a while
364                return _('paused composing a message')
365        elif chatstate == 'gone':
366                return _('has closed the chat window or tab')
367        return ''
368
369def is_in_path(name_of_command, return_abs_path = False):
370        # if return_abs_path is True absolute path will be returned
371        # for name_of_command
372        # on failures False is returned
373        is_in_dir = False
374        found_in_which_dir = None
375        path = os.getenv('PATH').split(':')
376        for path_to_directory in path:
377                try:
378                        contents = os.listdir(path_to_directory)
379                except OSError: # user can have something in PATH that is not a dir
380                        pass
381                else:
382                        is_in_dir = name_of_command in contents
383                if is_in_dir:
384                        if return_abs_path:
385                                found_in_which_dir = path_to_directory
386                        break
387
388        if found_in_which_dir:
389                abs_path = os.path.join(path_to_directory, name_of_command)
390                return abs_path
391        else:
392                return is_in_dir
393
394def exec_command(command):
395        subprocess.Popen(command, shell = True)
396
397def build_command(executable, parameter):
398        # we add to the parameter (can hold path with spaces)
399        # "" so we have good parsing from shell
400        parameter = parameter.replace('"', '\\"') # but first escape "
401        command = '%s "%s"' % (executable, parameter)
402        return command
403
404def launch_browser_mailer(kind, uri):
405        #kind = 'url' or 'mail'
406        if os.name == 'nt':
407                try:
408                        os.startfile(uri) # if pywin32 is installed we open
409                except:
410                        pass
411
412        else:
413                if kind == 'mail' and not uri.startswith('mailto:'):
414                        uri = 'mailto:' + uri
415
416                if gajim.config.get('openwith') == 'gnome-open':
417                        command = 'gnome-open'
418                elif gajim.config.get('openwith') == 'kfmclient exec':
419                        command = 'kfmclient exec'
420                elif gajim.config.get('openwith') == 'exo-open':
421                        command = 'exo-open'
422                elif gajim.config.get('openwith') == 'custom':
423                        if kind == 'url':
424                                command = gajim.config.get('custombrowser')
425                        if kind == 'mail':
426                                command = gajim.config.get('custommailapp')
427                        if command == '': # if no app is configured
428                                return
429
430                command = build_command(command, uri)
431                try:
432                        exec_command(command)
433                except:
434                        pass
435
436def launch_file_manager(path_to_open):
437        if os.name == 'nt':
438                try:
439                        os.startfile(path_to_open) # if pywin32 is installed we open
440                except:
441                        pass
442        else:
443                if gajim.config.get('openwith') == 'gnome-open':
444                        command = 'gnome-open'
445                elif gajim.config.get('openwith') == 'kfmclient exec':
446                        command = 'kfmclient exec'
447                elif gajim.config.get('openwith') == 'exo-open':
448                        command = 'exo-open'
449                elif gajim.config.get('openwith') == 'custom':
450                        command = gajim.config.get('custom_file_manager')
451                if command == '': # if no app is configured
452                        return
453                command = build_command(command, path_to_open)
454                try:
455                        exec_command(command)
456                except:
457                        pass
458
459def play_sound(event):
460        if not gajim.config.get('sounds_on'):
461                return
462        path_to_soundfile = gajim.config.get_per('soundevents', event, 'path')
463        play_sound_file(path_to_soundfile)
464
465def play_sound_file(path_to_soundfile):
466        if path_to_soundfile == 'beep':
467                exec_command('beep')
468                return
469        if path_to_soundfile is None or not os.path.exists(path_to_soundfile):
470                return
471        if os.name == 'nt':
472                try:
473                        winsound.PlaySound(path_to_soundfile,
474                                winsound.SND_FILENAME|winsound.SND_ASYNC)
475                except:
476                        pass
477        elif os.name == 'posix':
478                if gajim.config.get('soundplayer') == '':
479                        return
480                player = gajim.config.get('soundplayer')
481                command = build_command(player, path_to_soundfile)
482                exec_command(command)
483
484def get_file_path_from_dnd_dropped_uri(uri):
485        path = urllib.url2pathname(uri) # escape special chars
486        path = path.strip('\r\n\x00') # remove \r\n and NULL
487        # get the path to file
488        if path.startswith('file:\\\\\\'): # windows
489                path = path[8:] # 8 is len('file:///')
490        elif path.startswith('file://'): # nautilus, rox
491                path = path[7:] # 7 is len('file://')
492        elif path.startswith('file:'): # xffm
493                path = path[5:] # 5 is len('file:')
494        return path
495
496def from_xs_boolean_to_python_boolean(value):
497        # this is xs:boolean so 'true', 'false', '1', '0'
498        # convert those to True/False (python booleans)
499        if value in ('1', 'true'):
500                val = True
501        else: # '0', 'false' or anything else
502                val = False
503
504        return val
505
506def get_xmpp_show(show):
507        if show in ('online', 'offline'):
508                return None
509        return show
510
511def get_output_of_command(command):
512        try:
513                child_stdin, child_stdout = os.popen2(command)
514        except ValueError:
515                return None
516
517        output = child_stdout.readlines()
518        child_stdout.close()
519        child_stdin.close()
520
521        return output
522
523def get_global_show():
524        maxi = 0
525        for account in gajim.connections:
526                if not gajim.config.get_per('accounts', account,
527                        'sync_with_global_status'):
528                        continue
529                connected = gajim.connections[account].connected
530                if connected > maxi:
531                        maxi = connected
532        return gajim.SHOW_LIST[maxi]
533
534def get_global_status():
535        maxi = 0
536        for account in gajim.connections:
537                if not gajim.config.get_per('accounts', account,
538                        'sync_with_global_status'):
539                        continue
540                connected = gajim.connections[account].connected
541                if connected > maxi:
542                        maxi = connected
543                        status = gajim.connections[account].status
544        return status
545
546def get_icon_name_to_show(contact, account = None):
547        '''Get the icon name to show in online, away, requested, ...'''
548        if account and gajim.events.get_nb_roster_events(account, contact.jid):
549                return 'message'
550        if account and gajim.events.get_nb_roster_events(account,
551        contact.get_full_jid()):
552                return 'message'
553        if contact.jid.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent
554                return contact.show
555        if contact.sub in ('both', 'to'):
556                return contact.show
557        if contact.ask == 'subscribe':
558                return 'requested'
559        transport = gajim.get_transport_name_from_jid(contact.jid)
560        if transport:
561                return contact.show
562        return 'not in roster'
563
564def decode_string(string):
565        '''try to decode (to make it Unicode instance) given string'''
566        if isinstance(string, unicode):
567                return string
568        # by the time we go to iso15 it better be the one else we show bad characters
569        encodings = (locale.getpreferredencoding(), 'utf-8', 'iso-8859-15')
570        for encoding in encodings:
571                try:
572                        string = string.decode(encoding)
573                except UnicodeError:
574                        continue
575                break
576
577        return string
578
579def ensure_utf8_string(string):
580        '''make sure string is in UTF-8'''
581        try:
582                string = decode_string(string).encode('utf-8')
583        except:
584                pass
585        return string
586
587def get_windows_reg_env(varname, default=''):
588        '''asks for paths commonly used but not exposed as ENVs
589        in english Windows 2003 those are:
590        'AppData' = %USERPROFILE%\Application Data (also an ENV)
591        'Desktop' = %USERPROFILE%\Desktop
592        'Favorites' = %USERPROFILE%\Favorites
593        'NetHood' = %USERPROFILE%\NetHood
594        'Personal' = D:\My Documents (PATH TO MY DOCUMENTS)
595        'PrintHood' = %USERPROFILE%\PrintHood
596        'Programs' = %USERPROFILE%\Start Menu\Programs
597        'Recent' = %USERPROFILE%\Recent
598        'SendTo' = %USERPROFILE%\SendTo
599        'Start Menu' = %USERPROFILE%\Start Menu
600        'Startup' = %USERPROFILE%\Start Menu\Programs\Startup
601        'Templates' = %USERPROFILE%\Templates
602        'My Pictures' = D:\My Documents\My Pictures
603        'Local Settings' = %USERPROFILE%\Local Settings
604        'Local AppData' = %USERPROFILE%\Local Settings\Application Data
605        'Cache' = %USERPROFILE%\Local Settings\Temporary Internet Files
606        'Cookies' = %USERPROFILE%\Cookies
607        'History' = %USERPROFILE%\Local Settings\History
608        '''
609
610        if os.name != 'nt':
611                return ''
612
613        val = default
614        try:
615                rkey = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,
616r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders')
617                try:
618                        val = str(win32api.RegQueryValueEx(rkey, varname)[0])
619                        val = win32api.ExpandEnvironmentStrings(val) # expand using environ
620                except:
621                        pass
622        finally:
623                win32api.RegCloseKey(rkey)
624        return val
625
626def get_my_pictures_path():
627        '''windows-only atm. [Unix lives in the past]'''
628        return get_windows_reg_env('My Pictures')
629
630def get_desktop_path():
631        if os.name == 'nt':
632                path = get_windows_reg_env('Desktop')
633        else:
634                path = os.path.join(os.path.expanduser('~'), 'Desktop')
635        return path
636
637def get_documents_path():
638        if os.name == 'nt':
639                path = get_windows_reg_env('Personal')
640        else:
641                path = os.path.expanduser('~')
642        return path
643
644def get_full_jid_from_iq(iq_obj):
645        '''return the full jid (with resource) from an iq as unicode'''
646        return parse_jid(str(iq_obj.getFrom()))
647
648def get_jid_from_iq(iq_obj):
649        '''return the jid (without resource) from an iq as unicode'''
650        jid = get_full_jid_from_iq(iq_obj)
651        return gajim.get_jid_without_resource(jid)
652
653def get_auth_sha(sid, initiator, target):
654        ''' return sha of sid + initiator + target used for proxy auth'''
655        return sha.new("%s%s%s" % (sid, initiator, target)).hexdigest()
656
657
658distro_info = {
659        'Arch Linux': '/etc/arch-release',
660        'Aurox Linux': '/etc/aurox-release',
661        'Conectiva Linux': '/etc/conectiva-release',
662        'CRUX': '/usr/bin/crux',
663        'Debian GNU/Linux': '/etc/debian_release',
664        'Debian GNU/Linux': '/etc/debian_version',
665        'Fedora Linux': '/etc/fedora-release',
666        'Gentoo Linux': '/etc/gentoo-release',
667        'Linux from Scratch': '/etc/lfs-release',
668        'Mandrake Linux': '/etc/mandrake-release',
669        'Slackware Linux': '/etc/slackware-release',
670        'Slackware Linux': '/etc/slackware-version',
671        'Solaris/Sparc': '/etc/release',
672        'Source Mage': '/etc/sourcemage_version',
673        'SUSE Linux': '/etc/SuSE-release',
674        'Sun JDS': '/etc/sun-release',
675        'PLD Linux': '/etc/pld-release',
676        'Yellow Dog Linux': '/etc/yellowdog-release',
677        # many distros use the /etc/redhat-release for compatibility
678        # so Redhat is the last
679        'Redhat Linux': '/etc/redhat-release'
680}
681
682def get_random_string_16():
683        ''' create random string of length 16'''
684        rng = range(65, 90)
685        rng.extend(range(48, 57))
686        char_sequence = map(lambda e:chr(e), rng)
687        from random import sample
688        return reduce(lambda e1, e2: e1 + e2, 
689                        sample(char_sequence, 16))
690       
691def get_os_info():
692        if os.name == 'nt':
693                ver = os.sys.getwindowsversion()
694                ver_format = ver[3], ver[0], ver[1]
695                win_version = {
696                        (1, 4, 0): '95',
697                        (1, 4, 10): '98',
698                        (1, 4, 90): 'ME',
699                        (2, 4, 0): 'NT',
700                        (2, 5, 0): '2000',
701                        (2, 5, 1): 'XP',
702                        (2, 5, 2): '2003',
703                    Â