Package common :: Module helpers
[hide private]
[frames] | no frames]

Source Code for Module common.helpers

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