use webbrowser module to open uri instead of using popen. Fixes #5751
[gajim.git] / src / common / helpers.py
blobb95a331cba2e1fcf0b5c6ff4f500a1f0dc57aef9
1 # -*- coding:utf-8 -*-
2 ## src/common/helpers.py
3 ##
4 ## Copyright (C) 2003-2010 Yann Leboulanger <asterix AT lagaule.org>
5 ## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
6 ## Nikos Kouremenos <kourem AT gmail.com>
7 ## Copyright (C) 2006 Alex Mauer <hawke AT hawkesnest.net>
8 ## Copyright (C) 2006-2007 Travis Shirk <travis AT pobox.com>
9 ## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
10 ## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
11 ## James Newton <redshodan AT gmail.com>
12 ## Julien Pivotto <roidelapluie AT gmail.com>
13 ## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
14 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
15 ## Jonathan Schleifer <js-gajim AT webkeks.org>
17 ## This file is part of Gajim.
19 ## Gajim is free software; you can redistribute it and/or modify
20 ## it under the terms of the GNU General Public License as published
21 ## by the Free Software Foundation; version 3 only.
23 ## Gajim is distributed in the hope that it will be useful,
24 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
25 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 ## GNU General Public License for more details.
28 ## You should have received a copy of the GNU General Public License
29 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
32 import sys
33 import re
34 import locale
35 import os
36 import subprocess
37 import urllib
38 import webbrowser
39 import errno
40 import select
41 import base64
42 import hashlib
43 import caps_cache
45 from encodings.punycode import punycode_encode
46 from string import Template
48 from i18n import Q_
49 from i18n import ngettext
51 try:
52 import winsound # windows-only built-in module for playing wav
53 import win32api
54 import win32con
55 import wave # posix-only fallback wav playback
56 import ossaudiodev as oss
57 except Exception:
58 pass
60 special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
62 class InvalidFormat(Exception):
63 pass
65 def decompose_jid(jidstring):
66 user = None
67 server = None
68 resource = None
70 # Search for delimiters
71 user_sep = jidstring.find('@')
72 res_sep = jidstring.find('/')
74 if user_sep == -1:
75 if res_sep == -1:
76 # host
77 server = jidstring
78 else:
79 # host/resource
80 server = jidstring[0:res_sep]
81 resource = jidstring[res_sep + 1:]
82 else:
83 if res_sep == -1:
84 # user@host
85 user = jidstring[0:user_sep]
86 server = jidstring[user_sep + 1:]
87 else:
88 if user_sep < res_sep:
89 # user@host/resource
90 user = jidstring[0:user_sep]
91 server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
92 resource = jidstring[res_sep + 1:]
93 else:
94 # server/resource (with an @ in resource)
95 server = jidstring[0:res_sep]
96 resource = jidstring[res_sep + 1:]
97 return user, server, resource
99 def parse_jid(jidstring):
101 Perform stringprep on all JID fragments from a string and return the full
104 # This function comes from http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
106 return prep(*decompose_jid(jidstring))
108 def idn_to_ascii(host):
110 Convert IDN (Internationalized Domain Names) to ACE (ASCII-compatible
111 encoding)
113 from encodings import idna
114 labels = idna.dots.split(host)
115 converted_labels = []
116 for label in labels:
117 converted_labels.append(idna.ToASCII(label))
118 return ".".join(converted_labels)
120 def ascii_to_idn(host):
122 Convert ACE (ASCII-compatible encoding) to IDN (Internationalized Domain
123 Names)
125 from encodings import idna
126 labels = idna.dots.split(host)
127 converted_labels = []
128 for label in labels:
129 converted_labels.append(idna.ToUnicode(label))
130 return ".".join(converted_labels)
132 def parse_resource(resource):
134 Perform stringprep on resource and return it
136 if resource:
137 try:
138 from xmpp.stringprepare import resourceprep
139 return resourceprep.prepare(unicode(resource))
140 except UnicodeError:
141 raise InvalidFormat, 'Invalid character in resource.'
143 def prep(user, server, resource):
145 Perform stringprep on all JID fragments and return the full jid
147 # This function comes from
148 #http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
149 if user is not None:
150 if len(user) < 1 or len(user) > 1023:
151 raise InvalidFormat, _('Username must be between 1 and 1023 chars')
152 try:
153 from xmpp.stringprepare import nodeprep
154 user = nodeprep.prepare(unicode(user))
155 except UnicodeError:
156 raise InvalidFormat, _('Invalid character in username.')
157 else:
158 user = None
160 if server is not None:
161 if len(server) < 1 or len(server) > 1023:
162 raise InvalidFormat, _('Server must be between 1 and 1023 chars')
163 try:
164 from xmpp.stringprepare import nameprep
165 server = nameprep.prepare(unicode(server))
166 except UnicodeError:
167 raise InvalidFormat, _('Invalid character in hostname.')
168 else:
169 raise InvalidFormat, _('Server address required.')
171 if resource is not None:
172 if len(resource) < 1 or len(resource) > 1023:
173 raise InvalidFormat, _('Resource must be between 1 and 1023 chars')
174 try:
175 from xmpp.stringprepare import resourceprep
176 resource = resourceprep.prepare(unicode(resource))
177 except UnicodeError:
178 raise InvalidFormat, _('Invalid character in resource.')
179 else:
180 resource = None
182 if user:
183 if resource:
184 return '%s@%s/%s' % (user, server, resource)
185 else:
186 return '%s@%s' % (user, server)
187 else:
188 if resource:
189 return '%s/%s' % (server, resource)
190 else:
191 return server
193 def windowsify(s):
194 if os.name == 'nt':
195 return s.capitalize()
196 return s
198 def temp_failure_retry(func, *args, **kwargs):
199 while True:
200 try:
201 return func(*args, **kwargs)
202 except (os.error, IOError, select.error), ex:
203 if ex.errno == errno.EINTR:
204 continue
205 else:
206 raise
208 def get_uf_show(show, use_mnemonic = False):
210 Return a userfriendly string for dnd/xa/chat and make all strings
211 translatable
213 If use_mnemonic is True, it adds _ so GUI should call with True for
214 accessibility issues
216 if show == 'dnd':
217 if use_mnemonic:
218 uf_show = _('_Busy')
219 else:
220 uf_show = _('Busy')
221 elif show == 'xa':
222 if use_mnemonic:
223 uf_show = _('_Not Available')
224 else:
225 uf_show = _('Not Available')
226 elif show == 'chat':
227 if use_mnemonic:
228 uf_show = _('_Free for Chat')
229 else:
230 uf_show = _('Free for Chat')
231 elif show == 'online':
232 if use_mnemonic:
233 uf_show = Q_('?user status:_Available')
234 else:
235 uf_show = Q_('?user status:Available')
236 elif show == 'connecting':
237 uf_show = _('Connecting')
238 elif show == 'away':
239 if use_mnemonic:
240 uf_show = _('A_way')
241 else:
242 uf_show = _('Away')
243 elif show == 'offline':
244 if use_mnemonic:
245 uf_show = _('_Offline')
246 else:
247 uf_show = _('Offline')
248 elif show == 'invisible':
249 if use_mnemonic:
250 uf_show = _('_Invisible')
251 else:
252 uf_show = _('Invisible')
253 elif show == 'not in roster':
254 uf_show = _('Not in Roster')
255 elif show == 'requested':
256 uf_show = Q_('?contact has status:Unknown')
257 else:
258 uf_show = Q_('?contact has status:Has errors')
259 return unicode(uf_show)
261 def get_uf_sub(sub):
262 if sub == 'none':
263 uf_sub = Q_('?Subscription we already have:None')
264 elif sub == 'to':
265 uf_sub = _('To')
266 elif sub == 'from':
267 uf_sub = _('From')
268 elif sub == 'both':
269 uf_sub = _('Both')
270 else:
271 uf_sub = sub
273 return unicode(uf_sub)
275 def get_uf_ask(ask):
276 if ask is None:
277 uf_ask = Q_('?Ask (for Subscription):None')
278 elif ask == 'subscribe':
279 uf_ask = _('Subscribe')
280 else:
281 uf_ask = ask
283 return unicode(uf_ask)
285 def get_uf_role(role, plural = False):
286 ''' plural determines if you get Moderators or Moderator'''
287 if role == 'none':
288 role_name = Q_('?Group Chat Contact Role:None')
289 elif role == 'moderator':
290 if plural:
291 role_name = _('Moderators')
292 else:
293 role_name = _('Moderator')
294 elif role == 'participant':
295 if plural:
296 role_name = _('Participants')
297 else:
298 role_name = _('Participant')
299 elif role == 'visitor':
300 if plural:
301 role_name = _('Visitors')
302 else:
303 role_name = _('Visitor')
304 return role_name
306 def get_uf_affiliation(affiliation):
307 '''Get a nice and translated affilition for muc'''
308 if affiliation == 'none':
309 affiliation_name = Q_('?Group Chat Contact Affiliation:None')
310 elif affiliation == 'owner':
311 affiliation_name = _('Owner')
312 elif affiliation == 'admin':
313 affiliation_name = _('Administrator')
314 elif affiliation == 'member':
315 affiliation_name = _('Member')
316 else: # Argl ! An unknown affiliation !
317 affiliation_name = affiliation.capitalize()
318 return affiliation_name
320 def get_sorted_keys(adict):
321 keys = sorted(adict.keys())
322 return keys
324 def to_one_line(msg):
325 msg = msg.replace('\\', '\\\\')
326 msg = msg.replace('\n', '\\n')
327 # s1 = 'test\ntest\\ntest'
328 # s11 = s1.replace('\\', '\\\\')
329 # s12 = s11.replace('\n', '\\n')
330 # s12
331 # 'test\\ntest\\\\ntest'
332 return msg
334 def from_one_line(msg):
335 # (?<!\\) is a lookbehind assertion which asks anything but '\'
336 # to match the regexp that follows it
338 # So here match '\\n' but not if you have a '\' before that
339 expr = re.compile(r'(?<!\\)\\n')
340 msg = expr.sub('\n', msg)
341 msg = msg.replace('\\\\', '\\')
342 # s12 = 'test\\ntest\\\\ntest'
343 # s13 = re.sub('\n', s12)
344 # s14 s13.replace('\\\\', '\\')
345 # s14
346 # 'test\ntest\\ntest'
347 return msg
349 def get_uf_chatstate(chatstate):
351 Remove chatstate jargon and returns user friendly messages
353 if chatstate == 'active':
354 return _('is paying attention to the conversation')
355 elif chatstate == 'inactive':
356 return _('is doing something else')
357 elif chatstate == 'composing':
358 return _('is composing a message...')
359 elif chatstate == 'paused':
360 #paused means he or she was composing but has stopped for a while
361 return _('paused composing a message')
362 elif chatstate == 'gone':
363 return _('has closed the chat window or tab')
364 return ''
366 def is_in_path(command, return_abs_path=False):
368 Return True if 'command' is found in one of the directories in the user's
369 path. If 'return_abs_path' is True, return the absolute path of the first
370 found command instead. Return False otherwise and on errors
372 for directory in os.getenv('PATH').split(os.pathsep):
373 try:
374 if command in os.listdir(directory):
375 if return_abs_path:
376 return os.path.join(directory, command)
377 else:
378 return True
379 except OSError:
380 # If the user has non directories in his path
381 pass
382 return False
384 def exec_command(command):
385 subprocess.Popen('%s &' % command, shell=True).wait()
387 def build_command(executable, parameter):
388 # we add to the parameter (can hold path with spaces)
389 # "" so we have good parsing from shell
390 parameter = parameter.replace('"', '\\"') # but first escape "
391 command = '%s "%s"' % (executable, parameter)
392 return command
394 def get_file_path_from_dnd_dropped_uri(uri):
395 path = urllib.unquote(uri) # escape special chars
396 path = path.strip('\r\n\x00') # remove \r\n and NULL
397 # get the path to file
398 if re.match('^file:///[a-zA-Z]:/', path): # windows
399 path = path[8:] # 8 is len('file:///')
400 elif path.startswith('file://'): # nautilus, rox
401 path = path[7:] # 7 is len('file://')
402 elif path.startswith('file:'): # xffm
403 path = path[5:] # 5 is len('file:')
404 return path
406 def from_xs_boolean_to_python_boolean(value):
407 # this is xs:boolean so 'true', 'false', '1', '0'
408 # convert those to True/False (python booleans)
409 if value in ('1', 'true'):
410 val = True
411 else: # '0', 'false' or anything else
412 val = False
414 return val
416 def get_xmpp_show(show):
417 if show in ('online', 'offline'):
418 return None
419 return show
421 def get_output_of_command(command):
422 try:
423 child_stdin, child_stdout = os.popen2(command)
424 except ValueError:
425 return None
427 output = child_stdout.readlines()
428 child_stdout.close()
429 child_stdin.close()
431 return output
433 def decode_string(string):
435 Try to decode (to make it Unicode instance) given string
437 if isinstance(string, unicode):
438 return string
439 # by the time we go to iso15 it better be the one else we show bad characters
440 encodings = (locale.getpreferredencoding(), 'utf-8', 'iso-8859-15')
441 for encoding in encodings:
442 try:
443 string = string.decode(encoding)
444 except UnicodeError:
445 continue
446 break
448 return string
450 def ensure_utf8_string(string):
452 Make sure string is in UTF-8
454 try:
455 string = decode_string(string).encode('utf-8')
456 except Exception:
457 pass
458 return string
460 def get_windows_reg_env(varname, default=''):
462 Ask for paths commonly used but not exposed as ENVs in english Windows 2003
463 those are:
464 'AppData' = %USERPROFILE%\Application Data (also an ENV)
465 'Desktop' = %USERPROFILE%\Desktop
466 'Favorites' = %USERPROFILE%\Favorites
467 'NetHood' = %USERPROFILE%\NetHood
468 'Personal' = D:\My Documents (PATH TO MY DOCUMENTS)
469 'PrintHood' = %USERPROFILE%\PrintHood
470 'Programs' = %USERPROFILE%\Start Menu\Programs
471 'Recent' = %USERPROFILE%\Recent
472 'SendTo' = %USERPROFILE%\SendTo
473 'Start Menu' = %USERPROFILE%\Start Menu
474 'Startup' = %USERPROFILE%\Start Menu\Programs\Startup
475 'Templates' = %USERPROFILE%\Templates
476 'My Pictures' = D:\My Documents\My Pictures
477 'Local Settings' = %USERPROFILE%\Local Settings
478 'Local AppData' = %USERPROFILE%\Local Settings\Application Data
479 'Cache' = %USERPROFILE%\Local Settings\Temporary Internet Files
480 'Cookies' = %USERPROFILE%\Cookies
481 'History' = %USERPROFILE%\Local Settings\History
483 if os.name != 'nt':
484 return ''
486 val = default
487 try:
488 rkey = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,
489 r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders')
490 try:
491 val = str(win32api.RegQueryValueEx(rkey, varname)[0])
492 val = win32api.ExpandEnvironmentStrings(val) # expand using environ
493 except Exception:
494 pass
495 finally:
496 win32api.RegCloseKey(rkey)
497 return val
499 def get_my_pictures_path():
501 Windows-only atm
503 return get_windows_reg_env('My Pictures')
505 def get_desktop_path():
506 if os.name == 'nt':
507 path = get_windows_reg_env('Desktop')
508 else:
509 path = os.path.join(os.path.expanduser('~'), 'Desktop')
510 return path
512 def get_documents_path():
513 if os.name == 'nt':
514 path = get_windows_reg_env('Personal')
515 else:
516 path = os.path.expanduser('~')
517 return path
519 def sanitize_filename(filename):
521 Make sure the filename we will write does contain only acceptable and latin
522 characters, and is not too long (in that case hash it)
524 # 48 is the limit
525 if len(filename) > 48:
526 hash = hashlib.md5(filename)
527 filename = base64.b64encode(hash.digest())
529 filename = punycode_encode(filename) # make it latin chars only
530 filename = filename.replace('/', '_')
531 if os.name == 'nt':
532 filename = filename.replace('?', '_').replace(':', '_')\
533 .replace('\\', '_').replace('"', "'").replace('|', '_')\
534 .replace('*', '_').replace('<', '_').replace('>', '_')
536 return filename
538 def reduce_chars_newlines(text, max_chars = 0, max_lines = 0):
540 Cut the chars after 'max_chars' on each line and show only the first
541 'max_lines'
543 If any of the params is not present (None or 0) the action on it is not
544 performed
546 def _cut_if_long(string):
547 if len(string) > max_chars:
548 string = string[:max_chars - 3] + '...'
549 return string
551 if isinstance(text, str):
552 text = text.decode('utf-8')
554 if max_lines == 0:
555 lines = text.split('\n')
556 else:
557 lines = text.split('\n', max_lines)[:max_lines]
558 if max_chars > 0:
559 if lines:
560 lines = [_cut_if_long(e) for e in lines]
561 if lines:
562 reduced_text = '\n'.join(lines)
563 if reduced_text != text:
564 reduced_text += '...'
565 else:
566 reduced_text = ''
567 return reduced_text
569 def get_account_status(account):
570 status = reduce_chars_newlines(account['status_line'], 100, 1)
571 return status
573 def get_avatar_path(prefix):
575 Return the filename of the avatar, distinguishes between user- and contact-
576 provided one. Return None if no avatar was found at all. prefix is the path
577 to the requested avatar just before the ".png" or ".jpeg"
579 # First, scan for a local, user-set avatar
580 for type_ in ('jpeg', 'png'):
581 file_ = prefix + '_local.' + type_
582 if os.path.exists(file_):
583 return file_
584 # If none available, scan for a contact-provided avatar
585 for type_ in ('jpeg', 'png'):
586 file_ = prefix + '.' + type_
587 if os.path.exists(file_):
588 return file_
589 return None
591 def datetime_tuple(timestamp):
593 Convert timestamp using strptime and the format: %Y%m%dT%H:%M:%S
595 Because of various datetime formats are used the following exceptions
596 are handled:
597 - Optional milliseconds appened to the string are removed
598 - Optional Z (that means UTC) appened to the string are removed
599 - XEP-082 datetime strings have all '-' cahrs removed to meet
600 the above format.
602 timestamp = timestamp.split('.')[0]
603 timestamp = timestamp.replace('-', '')
604 timestamp = timestamp.replace('z', '')
605 timestamp = timestamp.replace('Z', '')
606 from time import strptime
607 return strptime(timestamp, '%Y%m%dT%H:%M:%S')
609 # import gajim only when needed (after decode_string is defined) see #4764
611 import gajim
613 def convert_bytes(string):
614 suffix = ''
615 # IEC standard says KiB = 1024 bytes KB = 1000 bytes
616 # but do we use the standard?
617 use_kib_mib = gajim.config.get('use_kib_mib')
618 align = 1024.
619 bytes = float(string)
620 if bytes >= align:
621 bytes = round(bytes/align, 1)
622 if bytes >= align:
623 bytes = round(bytes/align, 1)
624 if bytes >= align:
625 bytes = round(bytes/align, 1)
626 if use_kib_mib:
627 #GiB means gibibyte
628 suffix = _('%s GiB')
629 else:
630 #GB means gigabyte
631 suffix = _('%s GB')
632 else:
633 if use_kib_mib:
634 #MiB means mibibyte
635 suffix = _('%s MiB')
636 else:
637 #MB means megabyte
638 suffix = _('%s MB')
639 else:
640 if use_kib_mib:
641 #KiB means kibibyte
642 suffix = _('%s KiB')
643 else:
644 #KB means kilo bytes
645 suffix = _('%s KB')
646 else:
647 #B means bytes
648 suffix = _('%s B')
649 return suffix % unicode(bytes)
651 def get_contact_dict_for_account(account):
653 Create a dict of jid, nick -> contact with all contacts of account.
655 Can be used for completion lists
657 contacts_dict = {}
658 for jid in gajim.contacts.get_jid_list(account):
659 contact = gajim.contacts.get_contact_with_highest_priority(account,
660 jid)
661 contacts_dict[jid] = contact
662 name = contact.name
663 if name in contacts_dict:
664 contact1 = contacts_dict[name]
665 del contacts_dict[name]
666 contacts_dict['%s (%s)' % (name, contact1.jid)] = contact1
667 contacts_dict['%s (%s)' % (name, jid)] = contact
668 else:
669 if contact.name == gajim.get_nick_from_jid(jid):
670 del contacts_dict[jid]
671 contacts_dict[name] = contact
672 return contacts_dict
674 def launch_browser_mailer(kind, uri):
675 # kind = 'url' or 'mail'
676 if kind in ('mail', 'sth_at_sth') and not uri.startswith('mailto:'):
677 uri = 'mailto:' + uri
679 if kind == 'url' and uri.startswith('www.'):
680 uri = 'http://' + uri
682 if not gajim.config.get('autodetect_browser_mailer'):
683 if kind == 'url':
684 command = gajim.config.get('custombrowser')
685 elif kind in ('mail', 'sth_at_sth'):
686 command = gajim.config.get('custommailapp')
687 if command == '': # if no app is configured
688 return
690 command = build_command(command, uri)
691 try:
692 exec_command(command)
693 except Exception:
694 pass
696 else:
697 webbrowser.open(uri)
700 def launch_file_manager(path_to_open):
701 uri = 'file://' + path_to_open
702 if not gajim.config.get('autodetect_browser_mailer'):
703 command = gajim.config.get('custom_file_manager')
704 if command == '': # if no app is configured
705 return
706 command = build_command(command, path_to_open)
707 try:
708 exec_command(command)
709 except Exception:
710 pass
711 else:
712 webbrowser.open(uri)
714 def play_sound(event):
715 if not gajim.config.get('sounds_on'):
716 return
717 path_to_soundfile = gajim.config.get_per('soundevents', event, 'path')
718 play_sound_file(path_to_soundfile)
720 def check_soundfile_path(file, dirs=(gajim.gajimpaths.data_root,
721 gajim.DATA_DIR)):
723 Check if the sound file exists
725 :param file: the file to check, absolute or relative to 'dirs' path
726 :param dirs: list of knows paths to fallback if the file doesn't exists
727 (eg: ~/.gajim/sounds/, DATADIR/sounds...).
728 :return the path to file or None if it doesn't exists.
730 if not file:
731 return None
732 elif os.path.exists(file):
733 return file
735 for d in dirs:
736 d = os.path.join(d, 'sounds', file)
737 if os.path.exists(d):
738 return d
739 return None
741 def strip_soundfile_path(file, dirs=(gajim.gajimpaths.data_root,
742 gajim.DATA_DIR), abs=True):
744 Remove knowns paths from a sound file
746 Filechooser returns absolute path. If path is a known fallback path, we remove it.
747 So config have no hardcoded path to DATA_DIR and text in textfield is shorther.
748 param: file: the filename to strip.
749 param: dirs: list of knowns paths from which the filename should be stripped.
750 param: abs: force absolute path on dirs
752 if not file:
753 return None
755 name = os.path.basename(file)
756 for d in dirs:
757 d = os.path.join(d, 'sounds', name)
758 if abs:
759 d = os.path.abspath(d)
760 if file == d:
761 return name
762 return file
764 def play_sound_file(path_to_soundfile):
765 if path_to_soundfile == 'beep':
766 exec_command('beep')
767 return
768 path_to_soundfile = check_soundfile_path(path_to_soundfile)
769 if path_to_soundfile is None:
770 return
771 elif os.name == 'nt':
772 try:
773 winsound.PlaySound(path_to_soundfile,
774 winsound.SND_FILENAME|winsound.SND_ASYNC)
775 except Exception:
776 pass
777 elif os.name == 'posix':
778 if gajim.config.get('soundplayer') == '':
779 def _oss_play():
780 sndfile = wave.open(path_to_soundfile, 'rb')
781 (nc, sw, fr, nf, comptype, compname) = sndfile.getparams()
782 dev = oss.open('/dev/dsp', 'w')
783 dev.setparameters(sw * 8, nc, fr)
784 dev.write(sndfile.readframes(nf))
785 sndfile.close()
786 dev.close()
787 gajim.thread_interface(_oss_play)
788 return
789 player = gajim.config.get('soundplayer')
790 command = build_command(player, path_to_soundfile)
791 exec_command(command)
793 def get_global_show():
794 maxi = 0
795 for account in gajim.connections:
796 if not gajim.config.get_per('accounts', account,
797 'sync_with_global_status'):
798 continue
799 connected = gajim.connections[account].connected
800 if connected > maxi:
801 maxi = connected
802 return gajim.SHOW_LIST[maxi]
804 def get_global_status():
805 maxi = 0
806 for account in gajim.connections:
807 if not gajim.config.get_per('accounts', account,
808 'sync_with_global_status'):
809 continue
810 connected = gajim.connections[account].connected
811 if connected > maxi:
812 maxi = connected
813 status = gajim.connections[account].status
814 return status
817 def statuses_unified():
819 Test if all statuses are the same
821 reference = None
822 for account in gajim.connections:
823 if not gajim.config.get_per('accounts', account,
824 'sync_with_global_status'):
825 continue
826 if reference is None:
827 reference = gajim.connections[account].connected
828 elif reference != gajim.connections[account].connected:
829 return False
830 return True
832 def get_icon_name_to_show(contact, account = None):
834 Get the icon name to show in online, away, requested, etc
836 if account and gajim.events.get_nb_roster_events(account, contact.jid):
837 return 'event'
838 if account and gajim.events.get_nb_roster_events(account,
839 contact.get_full_jid()):
840 return 'event'
841 if account and account in gajim.interface.minimized_controls and \
842 contact.jid in gajim.interface.minimized_controls[account] and gajim.interface.\
843 minimized_controls[account][contact.jid].get_nb_unread_pm() > 0:
844 return 'event'
845 if account and contact.jid in gajim.gc_connected[account]:
846 if gajim.gc_connected[account][contact.jid]:
847 return 'muc_active'
848 else:
849 return 'muc_inactive'
850 if contact.jid.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent
851 return contact.show
852 if contact.sub in ('both', 'to'):
853 return contact.show
854 if contact.ask == 'subscribe':
855 return 'requested'
856 transport = gajim.get_transport_name_from_jid(contact.jid)
857 if transport:
858 return contact.show
859 if contact.show in gajim.SHOW_LIST:
860 return contact.show
861 return 'not in roster'
863 def get_full_jid_from_iq(iq_obj):
865 Return the full jid (with resource) from an iq as unicode
867 return parse_jid(str(iq_obj.getFrom()))
869 def get_jid_from_iq(iq_obj):
871 Return the jid (without resource) from an iq as unicode
873 jid = get_full_jid_from_iq(iq_obj)
874 return gajim.get_jid_without_resource(jid)
876 def get_auth_sha(sid, initiator, target):
878 Return sha of sid + initiator + target used for proxy auth
880 return hashlib.sha1("%s%s%s" % (sid, initiator, target)).hexdigest()
882 def remove_invalid_xml_chars(string):
883 if string:
884 string = re.sub(gajim.interface.invalid_XML_chars_re, '', string)
885 return string
887 distro_info = {
888 'Arch Linux': '/etc/arch-release',
889 'Aurox Linux': '/etc/aurox-release',
890 'Conectiva Linux': '/etc/conectiva-release',
891 'CRUX': '/usr/bin/crux',
892 'Debian GNU/Linux': '/etc/debian_release',
893 'Debian GNU/Linux': '/etc/debian_version',
894 'Fedora Linux': '/etc/fedora-release',
895 'Gentoo Linux': '/etc/gentoo-release',
896 'Linux from Scratch': '/etc/lfs-release',
897 'Mandrake Linux': '/etc/mandrake-release',
898 'Slackware Linux': '/etc/slackware-release',
899 'Slackware Linux': '/etc/slackware-version',
900 'Solaris/Sparc': '/etc/release',
901 'Source Mage': '/etc/sourcemage_version',
902 'SUSE Linux': '/etc/SuSE-release',
903 'Sun JDS': '/etc/sun-release',
904 'PLD Linux': '/etc/pld-release',
905 'Yellow Dog Linux': '/etc/yellowdog-release',
906 'AgiliaLinux': '/etc/agilialinux-version',
907 # many distros use the /etc/redhat-release for compatibility
908 # so Redhat is the last
909 'Redhat Linux': '/etc/redhat-release'
912 def get_random_string_16():
914 Create random string of length 16
916 rng = range(65, 90)
917 rng.extend(range(48, 57))
918 char_sequence = [chr(e) for e in rng]
919 from random import sample
920 return ''.join(sample(char_sequence, 16))
922 def get_os_info():
923 if gajim.os_info:
924 return gajim.os_info
925 if os.name == 'nt':
926 # platform.release() seems to return the name of the windows
927 ver = sys.getwindowsversion()
928 ver_format = ver[3], ver[0], ver[1]
929 win_version = {
930 (1, 4, 0): '95',
931 (1, 4, 10): '98',
932 (1, 4, 90): 'ME',
933 (2, 4, 0): 'NT',
934 (2, 5, 0): '2000',
935 (2, 5, 1): 'XP',
936 (2, 5, 2): '2003',
937 (2, 6, 0): 'Vista',
938 (2, 6, 1): '7',
940 if ver_format in win_version:
941 os_info = 'Windows' + ' ' + win_version[ver_format]
942 else:
943 os_info = 'Windows'
944 gajim.os_info = os_info
945 return os_info
946 elif os.name == 'posix':
947 executable = 'lsb_release'
948 params = ' --description --codename --release --short'
949 full_path_to_executable = is_in_path(executable, return_abs_path = True)
950 if full_path_to_executable:
951 command = executable + params
952 p = subprocess.Popen([command], shell=True, stdin=subprocess.PIPE,
953 stdout=subprocess.PIPE, close_fds=True)
954 p.wait()
955 output = temp_failure_retry(p.stdout.readline).strip()
956 # some distros put n/a in places, so remove those
957 output = output.replace('n/a', '').replace('N/A', '')
958 gajim.os_info = output
959 return output
961 # lsb_release executable not available, so parse files
962 for distro_name in distro_info:
963 path_to_file = distro_info[distro_name]
964 if os.path.exists(path_to_file):
965 if os.access(path_to_file, os.X_OK):
966 # the file is executable (f.e. CRUX)
967 # yes, then run it and get the first line of output.
968 text = get_output_of_command(path_to_file)[0]
969 else:
970 fd = open(path_to_file)
971 text = fd.readline().strip() # get only first line
972 fd.close()
973 if path_to_file.endswith('version'):
974 # sourcemage_version and slackware-version files
975 # have all the info we need (name and version of distro)
976 if not os.path.basename(path_to_file).startswith(
977 'sourcemage') or not\
978 os.path.basename(path_to_file).startswith('slackware'):
979 text = distro_name + ' ' + text
980 elif path_to_file.endswith('aurox-release') or \
981 path_to_file.endswith('arch-release'):
982 # file doesn't have version
983 text = distro_name
984 elif path_to_file.endswith('lfs-release'):
985 # file just has version
986 text = distro_name + ' ' + text
987 os_info = text.replace('\n', '')
988 gajim.os_info = os_info
989 return os_info
991 # our last chance, ask uname and strip it
992 uname_output = get_output_of_command('uname -sr')
993 if uname_output is not None:
994 os_info = uname_output[0] # only first line
995 gajim.os_info = os_info
996 return os_info
997 os_info = 'N/A'
998 gajim.os_info = os_info
999 return os_info
1002 def allow_showing_notification(account, type_ = 'notify_on_new_message',
1003 advanced_notif_num = None, is_first_message = True):
1005 Is it allowed to show nofication?
1007 Check OUR status and if we allow notifications for that status type is the
1008 option that need to be True e.g.: notify_on_signing is_first_message: set it
1009 to false when it's not the first message
1011 if advanced_notif_num is not None:
1012 popup = gajim.config.get_per('notifications', str(advanced_notif_num),
1013 'popup')
1014 if popup == 'yes':
1015 return True
1016 if popup == 'no':
1017 return False
1018 if type_ and (not gajim.config.get(type_) or not is_first_message):
1019 return False
1020 if gajim.config.get('autopopupaway'): # always show notification
1021 return True
1022 if gajim.connections[account].connected in (2, 3): # we're online or chat
1023 return True
1024 return False
1026 def allow_popup_window(account, advanced_notif_num = None):
1028 Is it allowed to popup windows?
1030 if advanced_notif_num is not None:
1031 popup = gajim.config.get_per('notifications', str(advanced_notif_num),
1032 'auto_open')
1033 if popup == 'yes':
1034 return True
1035 if popup == 'no':
1036 return False
1037 autopopup = gajim.config.get('autopopup')
1038 autopopupaway = gajim.config.get('autopopupaway')
1039 if autopopup and (autopopupaway or \
1040 gajim.connections[account].connected in (2, 3)): # we're online or chat
1041 return True
1042 return False
1044 def allow_sound_notification(account, sound_event, advanced_notif_num=None):
1045 if advanced_notif_num is not None:
1046 sound = gajim.config.get_per('notifications', str(advanced_notif_num),
1047 'sound')
1048 if sound == 'yes':
1049 return True
1050 if sound == 'no':
1051 return False
1052 if gajim.config.get('sounddnd') or gajim.connections[account].connected != \
1053 gajim.SHOW_LIST.index('dnd') and gajim.config.get_per('soundevents',
1054 sound_event, 'enabled'):
1055 return True
1056 return False
1058 def get_chat_control(account, contact):
1059 full_jid_with_resource = contact.jid
1060 if contact.resource:
1061 full_jid_with_resource += '/' + contact.resource
1062 highest_contact = gajim.contacts.get_contact_with_highest_priority(
1063 account, contact.jid)
1065 # Look for a chat control that has the given resource, or default to
1066 # one without resource
1067 ctrl = gajim.interface.msg_win_mgr.get_control(full_jid_with_resource,
1068 account)
1070 if ctrl:
1071 return ctrl
1072 elif highest_contact and highest_contact.resource and \
1073 contact.resource != highest_contact.resource:
1074 return None
1075 else:
1076 # unknown contact or offline message
1077 return gajim.interface.msg_win_mgr.get_control(contact.jid, account)
1079 def get_notification_icon_tooltip_dict():
1081 Return a dict of the form {acct: {'show': show, 'message': message,
1082 'event_lines': [list of text lines to show in tooltip]}
1084 # How many events must there be before they're shown summarized, not per-user
1085 max_ungrouped_events = 10
1087 accounts = get_accounts_info()
1089 # Gather events. (With accounts, when there are more.)
1090 for account in accounts:
1091 account_name = account['name']
1092 account['event_lines'] = []
1093 # Gather events per-account
1094 pending_events = gajim.events.get_events(account = account_name)
1095 messages, non_messages, total_messages, total_non_messages = {}, {}, 0, 0
1096 for jid in pending_events:
1097 for event in pending_events[jid]:
1098 if event.type_.count('file') > 0:
1099 # This is a non-messagee event.
1100 messages[jid] = non_messages.get(jid, 0) + 1
1101 total_non_messages = total_non_messages + 1
1102 else:
1103 # This is a message.
1104 messages[jid] = messages.get(jid, 0) + 1
1105 total_messages = total_messages + 1
1106 # Display unread messages numbers, if any
1107 if total_messages > 0:
1108 if total_messages > max_ungrouped_events:
1109 text = ngettext(
1110 '%d message pending',
1111 '%d messages pending',
1112 total_messages, total_messages, total_messages)
1113 account['event_lines'].append(text)
1114 else:
1115 for jid in messages.keys():
1116 text = ngettext(
1117 '%d message pending',
1118 '%d messages pending',
1119 messages[jid], messages[jid], messages[jid])
1120 contact = gajim.contacts.get_first_contact_from_jid(
1121 account['name'], jid)
1122 if jid in gajim.gc_connected[account['name']]:
1123 text += _(' from room %s') % (jid)
1124 elif contact:
1125 name = contact.get_shown_name()
1126 text += _(' from user %s') % (name)
1127 else:
1128 text += _(' from %s') % (jid)
1129 account['event_lines'].append(text)
1131 # Display unseen events numbers, if any
1132 if total_non_messages > 0:
1133 if total_non_messages > max_ungrouped_events:
1134 text = ngettext(
1135 '%d event pending',
1136 '%d events pending',
1137 total_non_messages, total_non_messages,total_non_messages)
1138 account['event_lines'].append(text)
1139 else:
1140 for jid in non_messages.keys():
1141 text = ngettext('%d event pending', '%d events pending',
1142 non_messages[jid], non_messages[jid], non_messages[jid])
1143 text += _(' from user %s') % (jid)
1144 account[account]['event_lines'].append(text)
1146 return accounts
1148 def get_notification_icon_tooltip_text():
1149 text = None
1150 # How many events must there be before they're shown summarized, not per-user
1151 # max_ungrouped_events = 10
1152 # Character which should be used to indent in the tooltip.
1153 indent_with = ' '
1155 accounts = get_notification_icon_tooltip_dict()
1157 if len(accounts) == 0:
1158 # No configured account
1159 return _('Gajim')
1161 # at least one account present
1163 # Is there more that one account?
1164 if len(accounts) == 1:
1165 show_more_accounts = False
1166 else:
1167 show_more_accounts = True
1169 # If there is only one account, its status is shown on the first line.
1170 if show_more_accounts:
1171 text = _('Gajim')
1172 else:
1173 text = _('Gajim - %s') % (get_account_status(accounts[0]))
1175 # Gather and display events. (With accounts, when there are more.)
1176 for account in accounts:
1177 account_name = account['name']
1178 # Set account status, if not set above
1179 if (show_more_accounts):
1180 message = '\n' + indent_with + ' %s - %s'
1181 text += message % (account_name, get_account_status(account))
1182 # Account list shown, messages need to be indented more
1183 indent_how = 2
1184 else:
1185 # If no account list is shown, messages could have default indenting.
1186 indent_how = 1
1187 for line in account['event_lines']:
1188 text += '\n' + indent_with * indent_how + ' '
1189 text += line
1190 return text
1192 def get_accounts_info():
1194 Helper for notification icon tooltip
1196 accounts = []
1197 accounts_list = sorted(gajim.contacts.get_accounts())
1198 for account in accounts_list:
1199 status_idx = gajim.connections[account].connected
1200 # uncomment the following to hide offline accounts
1201 # if status_idx == 0: continue
1202 status = gajim.SHOW_LIST[status_idx]
1203 message = gajim.connections[account].status
1204 single_line = get_uf_show(status)
1205 if message is None:
1206 message = ''
1207 else:
1208 message = message.strip()
1209 if message != '':
1210 single_line += ': ' + message
1211 accounts.append({'name': account, 'status_line': single_line,
1212 'show': status, 'message': message})
1213 return accounts
1216 def get_iconset_path(iconset):
1217 if os.path.isdir(os.path.join(gajim.DATA_DIR, 'iconsets', iconset)):
1218 return os.path.join(gajim.DATA_DIR, 'iconsets', iconset)
1219 elif os.path.isdir(os.path.join(gajim.MY_ICONSETS_PATH, iconset)):
1220 return os.path.join(gajim.MY_ICONSETS_PATH, iconset)
1222 def get_mood_iconset_path(iconset):
1223 if os.path.isdir(os.path.join(gajim.DATA_DIR, 'moods', iconset)):
1224 return os.path.join(gajim.DATA_DIR, 'moods', iconset)
1225 elif os.path.isdir(os.path.join(gajim.MY_MOOD_ICONSETS_PATH, iconset)):
1226 return os.path.join(gajim.MY_MOOD_ICONSETS_PATH, iconset)
1228 def get_activity_iconset_path(iconset):
1229 if os.path.isdir(os.path.join(gajim.DATA_DIR, 'activities', iconset)):
1230 return os.path.join(gajim.DATA_DIR, 'activities', iconset)
1231 elif os.path.isdir(os.path.join(gajim.MY_ACTIVITY_ICONSETS_PATH,
1232 iconset)):
1233 return os.path.join(gajim.MY_ACTIVITY_ICONSETS_PATH, iconset)
1235 def get_transport_path(transport):
1236 if os.path.isdir(os.path.join(gajim.DATA_DIR, 'iconsets', 'transports',
1237 transport)):
1238 return os.path.join(gajim.DATA_DIR, 'iconsets', 'transports', transport)
1239 elif os.path.isdir(os.path.join(gajim.MY_ICONSETS_PATH, 'transports',
1240 transport)):
1241 return os.path.join(gajim.MY_ICONSETS_PATH, 'transports', transport)
1242 # No transport folder found, use default jabber one
1243 return get_iconset_path(gajim.config.get('iconset'))
1245 def prepare_and_validate_gpg_keyID(account, jid, keyID):
1247 Return an eight char long keyID that can be used with for GPG encryption
1248 with this contact
1250 If the given keyID is None, return UNKNOWN; if the key does not match the
1251 assigned key XXXXXXXXMISMATCH is returned. If the key is trusted and not yet
1252 assigned, assign it.
1254 if gajim.connections[account].USE_GPG:
1255 if keyID and len(keyID) == 16:
1256 keyID = keyID[8:]
1258 attached_keys = gajim.config.get_per('accounts', account,
1259 'attached_gpg_keys').split()
1261 if jid in attached_keys and keyID:
1262 attachedkeyID = attached_keys[attached_keys.index(jid) + 1]
1263 if attachedkeyID != keyID:
1264 # Get signing subkeys for the attached key
1265 subkeys = []
1266 for key in gajim.connections[account].gpg.list_keys():
1267 if key['keyid'][8:] == attachedkeyID:
1268 subkeys = [subkey[0][8:] for subkey in key['subkeys'] \
1269 if subkey[1] == 's']
1270 break
1272 if keyID not in subkeys:
1273 # Mismatch! Another gpg key was expected
1274 keyID += 'MISMATCH'
1275 elif jid in attached_keys:
1276 # An unsigned presence, just use the assigned key
1277 keyID = attached_keys[attached_keys.index(jid) + 1]
1278 elif keyID:
1279 public_keys = gajim.connections[account].ask_gpg_keys()
1280 # Assign the corresponding key, if we have it in our keyring
1281 if keyID in public_keys:
1282 for u in gajim.contacts.get_contacts(account, jid):
1283 u.keyID = keyID
1284 keys_str = gajim.config.get_per('accounts', account,
1285 'attached_gpg_keys')
1286 keys_str += jid + ' ' + keyID + ' '
1287 gajim.config.set_per('accounts', account, 'attached_gpg_keys',
1288 keys_str)
1289 elif keyID is None:
1290 keyID = 'UNKNOWN'
1291 return keyID
1293 def update_optional_features(account = None):
1294 import xmpp
1295 if account:
1296 accounts = [account]
1297 else:
1298 accounts = [a for a in gajim.connections]
1299 for a in accounts:
1300 gajim.gajim_optional_features[a] = []
1301 if gajim.config.get_per('accounts', a, 'subscribe_mood'):
1302 gajim.gajim_optional_features[a].append(xmpp.NS_MOOD + '+notify')
1303 if gajim.config.get_per('accounts', a, 'subscribe_activity'):
1304 gajim.gajim_optional_features[a].append(xmpp.NS_ACTIVITY + '+notify')
1305 if gajim.config.get_per('accounts', a, 'publish_tune'):
1306 gajim.gajim_optional_features[a].append(xmpp.NS_TUNE)
1307 if gajim.config.get_per('accounts', a, 'publish_location'):
1308 gajim.gajim_optional_features[a].append(xmpp.NS_LOCATION)
1309 if gajim.config.get_per('accounts', a, 'subscribe_tune'):
1310 gajim.gajim_optional_features[a].append(xmpp.NS_TUNE + '+notify')
1311 if gajim.config.get_per('accounts', a, 'subscribe_nick'):
1312 gajim.gajim_optional_features[a].append(xmpp.NS_NICK + '+notify')
1313 if gajim.config.get_per('accounts', a, 'subscribe_location'):
1314 gajim.gajim_optional_features[a].append(xmpp.NS_LOCATION + '+notify')
1315 if gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
1316 gajim.gajim_optional_features[a].append(xmpp.NS_CHATSTATES)
1317 if not gajim.config.get('ignore_incoming_xhtml'):
1318 gajim.gajim_optional_features[a].append(xmpp.NS_XHTML_IM)
1319 if gajim.HAVE_PYCRYPTO \
1320 and gajim.config.get_per('accounts', a, 'enable_esessions'):
1321 gajim.gajim_optional_features[a].append(xmpp.NS_ESESSION)
1322 if gajim.config.get_per('accounts', a, 'answer_receipts'):
1323 gajim.gajim_optional_features[a].append(xmpp.NS_RECEIPTS)
1324 if gajim.HAVE_FARSIGHT:
1325 gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE)
1326 gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP)
1327 gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO)
1328 gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO)
1329 gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP)
1330 gajim.caps_hash[a] = caps_cache.compute_caps_hash([gajim.gajim_identity],
1331 gajim.gajim_common_features + gajim.gajim_optional_features[a])
1332 # re-send presence with new hash
1333 connected = gajim.connections[a].connected
1334 if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
1335 gajim.connections[a].change_status(gajim.SHOW_LIST[connected],
1336 gajim.connections[a].status)
1338 def jid_is_blocked(account, jid):
1339 return ((jid in gajim.connections[account].blocked_contacts) or \
1340 gajim.connections[account].blocked_all)
1342 def group_is_blocked(account, group):
1343 return ((group in gajim.connections[account].blocked_groups) or \
1344 gajim.connections[account].blocked_all)
1346 def get_subscription_request_msg(account=None):
1347 s = gajim.config.get_per('accounts', account, 'subscription_request_msg')
1348 if s:
1349 return s
1350 s = _('I would like to add you to my contact list.')
1351 if account:
1352 s = _('Hello, I am $name.') + ' ' + s
1353 our_jid = gajim.get_jid_from_account(account)
1354 vcard = gajim.connections[account].get_cached_vcard(our_jid)
1355 name = ''
1356 if vcard:
1357 if 'N' in vcard:
1358 if 'GIVEN' in vcard['N'] and 'FAMILY' in vcard['N']:
1359 name = vcard['N']['GIVEN'] + ' ' + vcard['N']['FAMILY']
1360 if not name and 'FN' in vcard:
1361 name = vcard['FN']
1362 nick = gajim.nicks[account]
1363 if name and nick:
1364 name += ' (%s)' % nick
1365 elif nick:
1366 name = nick
1367 s = Template(s).safe_substitute({'name': name})
1368 return s
1370 def replace_dataform_media(form, stanza):
1371 import xmpp
1372 found = False
1373 for field in form.getTags('field'):
1374 for media in field.getTags('media'):
1375 for uri in media.getTags('uri'):
1376 uri_data = uri.getData()
1377 if uri_data.startswith('cid:'):
1378 uri_data = uri_data[4:]
1379 for data in stanza.getTags('data', namespace=xmpp.NS_BOB):
1380 if data.getAttr('cid') == uri_data:
1381 uri.setData(data.getData())
1382 found = True
1383 return found