2 ## src/common/helpers.py
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/>.
45 from encodings
.punycode
import punycode_encode
46 from string
import Template
49 from i18n
import ngettext
52 import winsound
# windows-only built-in module for playing wav
55 import wave
# posix-only fallback wav playback
56 import ossaudiodev
as oss
60 special_groups
= (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
62 class InvalidFormat(Exception):
65 def decompose_jid(jidstring
):
70 # Search for delimiters
71 user_sep
= jidstring
.find('@')
72 res_sep
= jidstring
.find('/')
80 server
= jidstring
[0:res_sep
]
81 resource
= jidstring
[res_sep
+ 1:]
85 user
= jidstring
[0:user_sep
]
86 server
= jidstring
[user_sep
+ 1:]
88 if user_sep
< res_sep
:
90 user
= jidstring
[0:user_sep
]
91 server
= jidstring
[user_sep
+ 1:user_sep
+ (res_sep
- user_sep
)]
92 resource
= jidstring
[res_sep
+ 1:]
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
113 from encodings
import idna
114 labels
= idna
.dots
.split(host
)
115 converted_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
125 from encodings
import idna
126 labels
= idna
.dots
.split(host
)
127 converted_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
138 from xmpp
.stringprepare
import resourceprep
139 return resourceprep
.prepare(unicode(resource
))
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
150 if len(user
) < 1 or len(user
) > 1023:
151 raise InvalidFormat
, _('Username must be between 1 and 1023 chars')
153 from xmpp
.stringprepare
import nodeprep
154 user
= nodeprep
.prepare(unicode(user
))
156 raise InvalidFormat
, _('Invalid character in username.')
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')
164 from xmpp
.stringprepare
import nameprep
165 server
= nameprep
.prepare(unicode(server
))
167 raise InvalidFormat
, _('Invalid character in hostname.')
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')
175 from xmpp
.stringprepare
import resourceprep
176 resource
= resourceprep
.prepare(unicode(resource
))
178 raise InvalidFormat
, _('Invalid character in resource.')
184 return '%s@%s/%s' % (user
, server
, resource
)
186 return '%s@%s' % (user
, server
)
189 return '%s/%s' % (server
, resource
)
195 return s
.capitalize()
198 def temp_failure_retry(func
, *args
, **kwargs
):
201 return func(*args
, **kwargs
)
202 except (os
.error
, IOError, select
.error
), ex
:
203 if ex
.errno
== errno
.EINTR
:
208 def get_uf_show(show
, use_mnemonic
= False):
210 Return a userfriendly string for dnd/xa/chat and make all strings
213 If use_mnemonic is True, it adds _ so GUI should call with True for
223 uf_show
= _('_Not Available')
225 uf_show
= _('Not Available')
228 uf_show
= _('_Free for Chat')
230 uf_show
= _('Free for Chat')
231 elif show
== 'online':
233 uf_show
= Q_('?user status:_Available')
235 uf_show
= Q_('?user status:Available')
236 elif show
== 'connecting':
237 uf_show
= _('Connecting')
243 elif show
== 'offline':
245 uf_show
= _('_Offline')
247 uf_show
= _('Offline')
248 elif show
== 'invisible':
250 uf_show
= _('_Invisible')
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')
258 uf_show
= Q_('?contact has status:Has errors')
259 return unicode(uf_show
)
263 uf_sub
= Q_('?Subscription we already have:None')
273 return unicode(uf_sub
)
277 uf_ask
= Q_('?Ask (for Subscription):None')
278 elif ask
== 'subscribe':
279 uf_ask
= _('Subscribe')
283 return unicode(uf_ask
)
285 def get_uf_role(role
, plural
= False):
286 ''' plural determines if you get Moderators or Moderator'''
288 role_name
= Q_('?Group Chat Contact Role:None')
289 elif role
== 'moderator':
291 role_name
= _('Moderators')
293 role_name
= _('Moderator')
294 elif role
== 'participant':
296 role_name
= _('Participants')
298 role_name
= _('Participant')
299 elif role
== 'visitor':
301 role_name
= _('Visitors')
303 role_name
= _('Visitor')
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())
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')
331 # 'test\\ntest\\\\ntest'
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('\\\\', '\\')
346 # 'test\ntest\\ntest'
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')
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
):
374 if command
in os
.listdir(directory
):
376 return os
.path
.join(directory
, command
)
380 # If the user has non directories in his path
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
)
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:')
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'):
411 else: # '0', 'false' or anything else
416 def get_xmpp_show(show
):
417 if show
in ('online', 'offline'):
421 def get_output_of_command(command
):
423 child_stdin
, child_stdout
= os
.popen2(command
)
427 output
= child_stdout
.readlines()
433 def decode_string(string
):
435 Try to decode (to make it Unicode instance) given string
437 if isinstance(string
, unicode):
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
:
443 string
= string
.decode(encoding
)
450 def ensure_utf8_string(string
):
452 Make sure string is in UTF-8
455 string
= decode_string(string
).encode('utf-8')
460 def get_windows_reg_env(varname
, default
=''):
462 Ask for paths commonly used but not exposed as ENVs in english Windows 2003
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
488 rkey
= win32api
.RegOpenKey(win32con
.HKEY_CURRENT_USER
,
489 r
'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders')
491 val
= str(win32api
.RegQueryValueEx(rkey
, varname
)[0])
492 val
= win32api
.ExpandEnvironmentStrings(val
) # expand using environ
496 win32api
.RegCloseKey(rkey
)
499 def get_my_pictures_path():
503 return get_windows_reg_env('My Pictures')
505 def get_desktop_path():
507 path
= get_windows_reg_env('Desktop')
509 path
= os
.path
.join(os
.path
.expanduser('~'), 'Desktop')
512 def get_documents_path():
514 path
= get_windows_reg_env('Personal')
516 path
= os
.path
.expanduser('~')
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)
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('/', '_')
532 filename
= filename
.replace('?', '_').replace(':', '_')\
533 .replace('\\', '_').replace('"', "'").replace('|', '_')\
534 .replace('*', '_').replace('<', '_').replace('>', '_')
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
543 If any of the params is not present (None or 0) the action on it is not
546 def _cut_if_long(string
):
547 if len(string
) > max_chars
:
548 string
= string
[:max_chars
- 3] + '...'
551 if isinstance(text
, str):
552 text
= text
.decode('utf-8')
555 lines
= text
.split('\n')
557 lines
= text
.split('\n', max_lines
)[:max_lines
]
560 lines
= [_cut_if_long(e
) for e
in lines
]
562 reduced_text
= '\n'.join(lines
)
563 if reduced_text
!= text
:
564 reduced_text
+= '...'
569 def get_account_status(account
):
570 status
= reduce_chars_newlines(account
['status_line'], 100, 1)
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_
):
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_
):
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
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
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
613 def convert_bytes(string
):
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')
619 bytes
= float(string
)
621 bytes
= round(bytes
/align
, 1)
623 bytes
= round(bytes
/align
, 1)
625 bytes
= round(bytes
/align
, 1)
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
658 for jid
in gajim
.contacts
.get_jid_list(account
):
659 contact
= gajim
.contacts
.get_contact_with_highest_priority(account
,
661 contacts_dict
[jid
] = contact
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
669 if contact
.name
== gajim
.get_nick_from_jid(jid
):
670 del contacts_dict
[jid
]
671 contacts_dict
[name
] = contact
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'):
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
690 command
= build_command(command
, uri
)
692 exec_command(command
)
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
706 command
= build_command(command
, path_to_open
)
708 exec_command(command
)
714 def play_sound(event
):
715 if not gajim
.config
.get('sounds_on'):
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
,
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.
732 elif os
.path
.exists(file):
736 d
= os
.path
.join(d
, 'sounds', file)
737 if os
.path
.exists(d
):
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
755 name
= os
.path
.basename(file)
757 d
= os
.path
.join(d
, 'sounds', name
)
759 d
= os
.path
.abspath(d
)
764 def play_sound_file(path_to_soundfile
):
765 if path_to_soundfile
== 'beep':
768 path_to_soundfile
= check_soundfile_path(path_to_soundfile
)
769 if path_to_soundfile
is None:
771 elif os
.name
== 'nt':
773 winsound
.PlaySound(path_to_soundfile
,
774 winsound
.SND_FILENAME|winsound
.SND_ASYNC
)
777 elif os
.name
== 'posix':
778 if gajim
.config
.get('soundplayer') == '':
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
))
787 gajim
.thread_interface(_oss_play
)
789 player
= gajim
.config
.get('soundplayer')
790 command
= build_command(player
, path_to_soundfile
)
791 exec_command(command
)
793 def get_global_show():
795 for account
in gajim
.connections
:
796 if not gajim
.config
.get_per('accounts', account
,
797 'sync_with_global_status'):
799 connected
= gajim
.connections
[account
].connected
802 return gajim
.SHOW_LIST
[maxi
]
804 def get_global_status():
806 for account
in gajim
.connections
:
807 if not gajim
.config
.get_per('accounts', account
,
808 'sync_with_global_status'):
810 connected
= gajim
.connections
[account
].connected
813 status
= gajim
.connections
[account
].status
817 def statuses_unified():
819 Test if all statuses are the same
822 for account
in gajim
.connections
:
823 if not gajim
.config
.get_per('accounts', account
,
824 'sync_with_global_status'):
826 if reference
is None:
827 reference
= gajim
.connections
[account
].connected
828 elif reference
!= gajim
.connections
[account
].connected
:
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
):
838 if account
and gajim
.events
.get_nb_roster_events(account
,
839 contact
.get_full_jid()):
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:
845 if account
and contact
.jid
in gajim
.gc_connected
[account
]:
846 if gajim
.gc_connected
[account
][contact
.jid
]:
849 return 'muc_inactive'
850 if contact
.jid
.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent
852 if contact
.sub
in ('both', 'to'):
854 if contact
.ask
== 'subscribe':
856 transport
= gajim
.get_transport_name_from_jid(contact
.jid
)
859 if contact
.show
in gajim
.SHOW_LIST
:
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
):
884 string
= re
.sub(gajim
.interface
.invalid_XML_chars_re
, '', string
)
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
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))
926 # platform.release() seems to return the name of the windows
927 ver
= sys
.getwindowsversion()
928 ver_format
= ver
[3], ver
[0], ver
[1]
940 if ver_format
in win_version
:
941 os_info
= 'Windows' + ' ' + win_version
[ver_format
]
944 gajim
.os_info
= 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)
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
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]
970 fd
= open(path_to_file
)
971 text
= fd
.readline().strip() # get only first line
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
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
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
998 gajim
.os_info
= 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
),
1018 if type_
and (not gajim
.config
.get(type_
) or not is_first_message
):
1020 if gajim
.config
.get('autopopupaway'): # always show notification
1022 if gajim
.connections
[account
].connected
in (2, 3): # we're online or chat
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
),
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
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
),
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'):
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
,
1072 elif highest_contact
and highest_contact
.resource
and \
1073 contact
.resource
!= highest_contact
.resource
:
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
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
:
1110 '%d message pending',
1111 '%d messages pending',
1112 total_messages
, total_messages
, total_messages
)
1113 account
['event_lines'].append(text
)
1115 for jid
in messages
.keys():
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
)
1125 name
= contact
.get_shown_name()
1126 text
+= _(' from user %s') % (name
)
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
:
1136 '%d events pending',
1137 total_non_messages
, total_non_messages
,total_non_messages
)
1138 account
['event_lines'].append(text
)
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
)
1148 def get_notification_icon_tooltip_text():
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.
1155 accounts
= get_notification_icon_tooltip_dict()
1157 if len(accounts
) == 0:
1158 # No configured account
1161 # at least one account present
1163 # Is there more that one account?
1164 if len(accounts
) == 1:
1165 show_more_accounts
= False
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
:
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
1185 # If no account list is shown, messages could have default indenting.
1187 for line
in account
['event_lines']:
1188 text
+= '\n' + indent_with
* indent_how
+ ' '
1192 def get_accounts_info():
1194 Helper for notification icon tooltip
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
)
1208 message
= message
.strip()
1210 single_line
+= ': ' + message
1211 accounts
.append({'name': account
, 'status_line': single_line
,
1212 'show': status
, 'message': message
})
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
,
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',
1238 return os
.path
.join(gajim
.DATA_DIR
, 'iconsets', 'transports', transport
)
1239 elif os
.path
.isdir(os
.path
.join(gajim
.MY_ICONSETS_PATH
, 'transports',
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
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:
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
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']
1272 if keyID
not in subkeys
:
1273 # Mismatch! Another gpg key was expected
1275 elif jid
in attached_keys
:
1276 # An unsigned presence, just use the assigned key
1277 keyID
= attached_keys
[attached_keys
.index(jid
) + 1]
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
):
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',
1293 def update_optional_features(account
= None):
1296 accounts
= [account
]
1298 accounts
= [a
for a
in gajim
.connections
]
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')
1350 s
= _('I would like to add you to my contact list.')
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
)
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
:
1362 nick
= gajim
.nicks
[account
]
1364 name
+= ' (%s)' % nick
1367 s
= Template(s
).safe_substitute({'name': name
})
1370 def replace_dataform_media(form
, stanza
):
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())