helpers.py 53.2 KB
Newer Older
roidelapluie's avatar
roidelapluie committed
1
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
2 3
## src/common/helpers.py
##
Dicson's avatar
Dicson committed
4
## Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
roidelapluie's avatar
roidelapluie committed
5 6 7 8 9 10 11 12 13 14 15
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
## Copyright (C) 2006 Alex Mauer <hawke AT hawkesnest.net>
## Copyright (C) 2006-2007 Travis Shirk <travis AT pobox.com>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
##                    James Newton <redshodan AT gmail.com>
##                    Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
##                    Jonathan Schleifer <js-gajim AT webkeks.org>
16
##
17 18 19
## This file is part of Gajim.
##
## 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
roidelapluie's avatar
roidelapluie committed
25
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 27
## GNU General Public License for more details.
##
28
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
29
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
30
##
31

32
import sys
33
import re
34
import os
35
import subprocess
36
import urllib
37
import webbrowser
38
import errno
39
import select
40
import base64
41
import hashlib
42
import shlex
Yann Leboulanger's avatar
Yann Leboulanger committed
43
from common import caps_cache
44 45
import socket
import time
46
import datetime
47

48
from encodings.punycode import punycode_encode
49
from string import Template
50

51 52
from common.i18n import Q_
from common.i18n import ngettext
53

54 55 56 57 58 59 60 61 62
if os.name == 'nt':
    try:
        HAS_WINSOUND = True
        import winsound  # windows-only built-in module for playing wav
    except ImportError:
        HAS_WINSOUND = False
        print('Gajim is not able to playback sound because'
              'pywin32 is missing', file=sys.stderr)

63
try:
64 65
    import wave     # posix-only fallback wav playback
    import ossaudiodev as oss
66
except Exception:
67
    pass
68

69 70 71
import logging
log = logging.getLogger('gajim.c.helpers')

72
special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
73

74
class InvalidFormat(Exception):
75
    pass
76

77
def decompose_jid(jidstring):
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
    user = None
    server = None
    resource = None

    # Search for delimiters
    user_sep = jidstring.find('@')
    res_sep = jidstring.find('/')

    if user_sep == -1:
        if res_sep == -1:
            # host
            server = jidstring
        else:
            # host/resource
            server = jidstring[0:res_sep]
93
            resource = jidstring[res_sep + 1:]
94 95 96
    else:
        if res_sep == -1:
            # user@host
97
            user = jidstring[0:user_sep]
98 99 100 101
            server = jidstring[user_sep + 1:]
        else:
            if user_sep < res_sep:
                # user@host/resource
102
                user = jidstring[0:user_sep]
103
                server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
104
                resource = jidstring[res_sep + 1:]
105 106 107
            else:
                # server/resource (with an @ in resource)
                server = jidstring[0:res_sep]
108
                resource = jidstring[res_sep + 1:]
109
    return user, server, resource
110 111

def parse_jid(jidstring):
112 113 114 115 116
    """
    Perform stringprep on all JID fragments from a string and return the full
    jid
    """
    # This function comes from http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
117

118
    return prep(*decompose_jid(jidstring))
119

120
def idn_to_ascii(host):
121 122 123 124 125 126 127 128
    """
    Convert IDN (Internationalized Domain Names) to ACE (ASCII-compatible
    encoding)
    """
    from encodings import idna
    labels = idna.dots.split(host)
    converted_labels = []
    for label in labels:
129 130 131 132
        if label:
            converted_labels.append(idna.ToASCII(label).decode('utf-8'))
        else:
            converted_labels.append('')
133
    return ".".join(converted_labels)
134

135
def ascii_to_idn(host):
136 137 138 139 140 141 142 143 144 145
    """
    Convert ACE (ASCII-compatible encoding) to IDN (Internationalized Domain
    Names)
    """
    from encodings import idna
    labels = idna.dots.split(host)
    converted_labels = []
    for label in labels:
        converted_labels.append(idna.ToUnicode(label))
    return ".".join(converted_labels)
146

147 148 149 150
def puny_encode_url(url):
    _url = url
    if '//' not in _url:
        _url = '//' + _url
151 152 153 154 155 156
    try:
        o = urllib.parse.urlparse(_url)
        p_loc = idn_to_ascii(o.netloc)
    except Exception:
        log.debug('urlparse failed: %s', url)
        return False
157 158
    return url.replace(o.netloc, p_loc)

159
def parse_resource(resource):
160 161 162 163 164
    """
    Perform stringprep on resource and return it
    """
    if resource:
        try:
165
            from nbxmpp.stringprepare import resourceprep
166
            return resourceprep.prepare(resource)
167
        except UnicodeError:
168
            raise InvalidFormat('Invalid character in resource.')
169 170

def prep(user, server, resource):
171 172 173 174 175
    """
    Perform stringprep on all JID fragments and return the full jid
    """
    # This function comes from
    #http://svn.twistedmatrix.com/cvs/trunk/twisted/words/protocols/jabber/jid.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204

    ip_address = False

    try:
        socket.inet_aton(server)
        ip_address = True
    except socket.error:
        pass

    if not ip_address and hasattr(socket, 'inet_pton'):
        try:
            socket.inet_pton(socket.AF_INET6, server.strip('[]'))
            server = '[%s]' % server.strip('[]')
            ip_address = True
        except (socket.error, ValueError):
            pass

    if not ip_address:
        if server is not None:
            if len(server) < 1 or len(server) > 1023:
                raise InvalidFormat(_('Server must be between 1 and 1023 chars'))
            try:
                from nbxmpp.stringprepare import nameprep
                server = nameprep.prepare(server)
            except UnicodeError:
                raise InvalidFormat(_('Invalid character in hostname.'))
        else:
            raise InvalidFormat(_('Server address required.'))

205 206
    if user is not None:
        if len(user) < 1 or len(user) > 1023:
207
            raise InvalidFormat(_('Username must be between 1 and 1023 chars'))
208
        try:
209
            from nbxmpp.stringprepare import nodeprep
Yann Leboulanger's avatar
Yann Leboulanger committed
210
            user = nodeprep.prepare(user)
211
        except UnicodeError:
212
            raise InvalidFormat(_('Invalid character in username.'))
213 214 215
    else:
        user = None

216 217
    if resource is not None:
        if len(resource) < 1 or len(resource) > 1023:
218
            raise InvalidFormat(_('Resource must be between 1 and 1023 chars'))
219
        try:
220
            from nbxmpp.stringprepare import resourceprep
Yann Leboulanger's avatar
Yann Leboulanger committed
221
            resource = resourceprep.prepare(resource)
222
        except UnicodeError:
223
            raise InvalidFormat(_('Invalid character in resource.'))
224 225 226 227 228 229 230 231 232 233 234 235 236
    else:
        resource = None

    if user:
        if resource:
            return '%s@%s/%s' % (user, server, resource)
        else:
            return '%s@%s' % (user, server)
    else:
        if resource:
            return '%s/%s' % (server, resource)
        else:
            return server
237

238
def windowsify(s):
239 240 241
    if os.name == 'nt':
        return s.capitalize()
    return s
242

243
def temp_failure_retry(func, *args, **kwargs):
244 245 246
    while True:
        try:
            return func(*args, **kwargs)
Yann Leboulanger's avatar
Yann Leboulanger committed
247
        except (os.error, IOError, select.error) as ex:
248 249 250 251
            if ex.errno == errno.EINTR:
                continue
            else:
                raise
252

253
def get_uf_show(show, use_mnemonic = False):
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
    """
    Return a userfriendly string for dnd/xa/chat and make all strings
    translatable

    If use_mnemonic is True, it adds _ so GUI should call with True for
    accessibility issues
    """
    if show == 'dnd':
        if use_mnemonic:
            uf_show = _('_Busy')
        else:
            uf_show = _('Busy')
    elif show == 'xa':
        if use_mnemonic:
            uf_show = _('_Not Available')
        else:
            uf_show = _('Not Available')
    elif show == 'chat':
        if use_mnemonic:
            uf_show = _('_Free for Chat')
        else:
            uf_show = _('Free for Chat')
    elif show == 'online':
        if use_mnemonic:
278
            uf_show = Q_('?user status:_Available')
279
        else:
280
            uf_show = Q_('?user status:Available')
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    elif show == 'connecting':
        uf_show = _('Connecting')
    elif show == 'away':
        if use_mnemonic:
            uf_show = _('A_way')
        else:
            uf_show = _('Away')
    elif show == 'offline':
        if use_mnemonic:
            uf_show = _('_Offline')
        else:
            uf_show = _('Offline')
    elif show == 'invisible':
        if use_mnemonic:
            uf_show = _('_Invisible')
        else:
            uf_show = _('Invisible')
    elif show == 'not in roster':
        uf_show = _('Not in Roster')
    elif show == 'requested':
        uf_show = Q_('?contact has status:Unknown')
    else:
        uf_show = Q_('?contact has status:Has errors')
304
    return uf_show
nkour's avatar
nkour committed
305

306
def get_uf_sub(sub):
307 308 309 310 311 312 313 314 315 316 317
    if sub == 'none':
        uf_sub = Q_('?Subscription we already have:None')
    elif sub == 'to':
        uf_sub = _('To')
    elif sub == 'from':
        uf_sub = _('From')
    elif sub == 'both':
        uf_sub = _('Both')
    else:
        uf_sub = sub

318
    return uf_sub
nkour's avatar
nkour committed
319

320
def get_uf_ask(ask):
321 322 323 324 325 326
    if ask is None:
        uf_ask = Q_('?Ask (for Subscription):None')
    elif ask == 'subscribe':
        uf_ask = _('Subscribe')
    else:
        uf_ask = ask
nkour's avatar
nkour committed
327

328
    return uf_ask
329

nkour's avatar
typo  
nkour committed
330
def get_uf_role(role, plural = False):
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    ''' plural determines if you get Moderators or Moderator'''
    if role == 'none':
        role_name = Q_('?Group Chat Contact Role:None')
    elif role == 'moderator':
        if plural:
            role_name = _('Moderators')
        else:
            role_name = _('Moderator')
    elif role == 'participant':
        if plural:
            role_name = _('Participants')
        else:
            role_name = _('Participant')
    elif role == 'visitor':
        if plural:
            role_name = _('Visitors')
        else:
            role_name = _('Visitor')
    return role_name
350

351
def get_uf_affiliation(affiliation):
352 353 354 355 356 357 358 359 360 361 362 363
    '''Get a nice and translated affilition for muc'''
    if affiliation == 'none':
        affiliation_name = Q_('?Group Chat Contact Affiliation:None')
    elif affiliation == 'owner':
        affiliation_name = _('Owner')
    elif affiliation == 'admin':
        affiliation_name = _('Administrator')
    elif affiliation == 'member':
        affiliation_name = _('Member')
    else: # Argl ! An unknown affiliation !
        affiliation_name = affiliation.capitalize()
    return affiliation_name
364

365
def get_sorted_keys(adict):
366 367
    keys = sorted(adict.keys())
    return keys
368 369

def to_one_line(msg):
370 371 372 373 374 375 376 377
    msg = msg.replace('\\', '\\\\')
    msg = msg.replace('\n', '\\n')
    # s1 = 'test\ntest\\ntest'
    # s11 = s1.replace('\\', '\\\\')
    # s12 = s11.replace('\n', '\\n')
    # s12
    # 'test\\ntest\\\\ntest'
    return msg
378 379

def from_one_line(msg):
380 381 382 383 384 385 386 387 388 389 390 391 392
    # (?<!\\) is a lookbehind assertion which asks anything but '\'
    # to match the regexp that follows it

    # So here match '\\n' but not if you have a '\' before that
    expr = re.compile(r'(?<!\\)\\n')
    msg = expr.sub('\n', msg)
    msg = msg.replace('\\\\', '\\')
    # s12 = 'test\\ntest\\\\ntest'
    # s13 = re.sub('\n', s12)
    # s14 s13.replace('\\\\', '\\')
    # s14
    # 'test\ntest\\ntest'
    return msg
393 394

def get_uf_chatstate(chatstate):
395 396 397 398 399 400 401 402
    """
    Remove chatstate jargon and returns user friendly messages
    """
    if chatstate == 'active':
        return _('is paying attention to the conversation')
    elif chatstate == 'inactive':
        return _('is doing something else')
    elif chatstate == 'composing':
403
        return _('is composing a message…')
404 405 406 407 408 409
    elif chatstate == 'paused':
        #paused means he or she was composing but has stopped for a while
        return _('paused composing a message')
    elif chatstate == 'gone':
        return _('has closed the chat window or tab')
    return ''
410

411
def is_in_path(command, return_abs_path=False):
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    """
    Return True if 'command' is found in one of the directories in the user's
    path. If 'return_abs_path' is True, return the absolute path of the first
    found command instead. Return False otherwise and on errors
    """
    for directory in os.getenv('PATH').split(os.pathsep):
        try:
            if command in os.listdir(directory):
                if return_abs_path:
                    return os.path.join(directory, command)
                else:
                    return True
        except OSError:
            # If the user has non directories in his path
            pass
    return False
428

429
def exec_command(command, use_shell=False, posix=True):
430 431 432 433 434 435 436 437
    """
    execute a command. if use_shell is True, we run the command as is it was
    typed in a console. So it may be dangerous if you are not sure about what
    is executed.
    """
    if use_shell:
        subprocess.Popen('%s &' % command, shell=True).wait()
    else:
438
        args = shlex.split(command, posix=posix)
439 440
        p = subprocess.Popen(args)
        gajim.thread_interface(p.wait)
441 442

def build_command(executable, parameter):
443 444 445 446 447
    # we add to the parameter (can hold path with spaces)
    # "" so we have good parsing from shell
    parameter = parameter.replace('"', '\\"') # but first escape "
    command = '%s "%s"' % (executable, parameter)
    return command
448

449
def get_file_path_from_dnd_dropped_uri(uri):
450
    path = urllib.parse.unquote(uri.decode('utf-8')) # escape special chars
451 452 453 454 455 456 457 458 459
    path = path.strip('\r\n\x00') # remove \r\n and NULL
    # get the path to file
    if re.match('^file:///[a-zA-Z]:/', path): # windows
        path = path[8:] # 8 is len('file:///')
    elif path.startswith('file://'): # nautilus, rox
        path = path[7:] # 7 is len('file://')
    elif path.startswith('file:'): # xffm
        path = path[5:] # 5 is len('file:')
    return path
460 461

def from_xs_boolean_to_python_boolean(value):
462 463 464 465 466 467
    # this is xs:boolean so 'true', 'false', '1', '0'
    # convert those to True/False (python booleans)
    if value in ('1', 'true'):
        val = True
    else: # '0', 'false' or anything else
        val = False
468

469
    return val
470 471

def get_xmpp_show(show):
472 473 474
    if show in ('online', 'offline'):
        return None
    return show
475 476

def get_output_of_command(command):
477 478 479 480
    try:
        child_stdin, child_stdout = os.popen2(command)
    except ValueError:
        return None
481

482 483 484
    output = child_stdout.readlines()
    child_stdout.close()
    child_stdin.close()
485

486
    return output
487 488

def sanitize_filename(filename):
489 490 491 492 493 494
    """
    Make sure the filename we will write does contain only acceptable and latin
    characters, and is not too long (in that case hash it)
    """
    # 48 is the limit
    if len(filename) > 48:
Yann Leboulanger's avatar
Yann Leboulanger committed
495 496
        hash = hashlib.md5(filename.encode('utf-8'))
        filename = base64.b64encode(hash.digest()).decode('utf-8')
497

498 499
    # make it latin chars only
    filename = punycode_encode(filename).decode('utf-8')
500 501 502 503 504 505 506
    filename = filename.replace('/', '_')
    if os.name == 'nt':
        filename = filename.replace('?', '_').replace(':', '_')\
                .replace('\\', '_').replace('"', "'").replace('|', '_')\
                .replace('*', '_').replace('<', '_').replace('>', '_')

    return filename
507 508

def reduce_chars_newlines(text, max_chars = 0, max_lines = 0):
509 510 511 512 513 514 515 516 517
    """
    Cut the chars after 'max_chars' on each line and show only the first
    'max_lines'

    If any of the params is not present (None or 0) the action on it is not
    performed
    """
    def _cut_if_long(string):
        if len(string) > max_chars:
518
            string = string[:max_chars - 3] + '…'
519 520 521 522 523 524 525 526 527 528 529 530
        return string

    if max_lines == 0:
        lines = text.split('\n')
    else:
        lines = text.split('\n', max_lines)[:max_lines]
    if max_chars > 0:
        if lines:
            lines = [_cut_if_long(e) for e in lines]
    if lines:
        reduced_text = '\n'.join(lines)
        if reduced_text != text:
531
            reduced_text += '…'
532 533 534
    else:
        reduced_text = ''
    return reduced_text
535 536

def get_account_status(account):
537 538
    status = reduce_chars_newlines(account['status_line'], 100, 1)
    return status
539 540

def get_avatar_path(prefix):
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
    """
    Return the filename of the avatar, distinguishes between user- and contact-
    provided one. Return None if no avatar was found at all.  prefix is the path
    to the requested avatar just before the ".png" or ".jpeg"
    """
    # First, scan for a local, user-set avatar
    for type_ in ('jpeg', 'png'):
        file_ = prefix + '_local.' + type_
        if os.path.exists(file_):
            return file_
    # If none available, scan for a contact-provided avatar
    for type_ in ('jpeg', 'png'):
        file_ = prefix + '.' + type_
        if os.path.exists(file_):
            return file_
    return None
557 558

def datetime_tuple(timestamp):
559 560 561 562 563 564 565 566 567 568
    """
    Convert timestamp using strptime and the format: %Y%m%dT%H:%M:%S

    Because of various datetime formats are used the following exceptions
    are handled:
            - Optional milliseconds appened to the string are removed
            - Optional Z (that means UTC) appened to the string are removed
            - XEP-082 datetime strings have all '-' cahrs removed to meet
              the above format.
    """
569 570 571 572 573 574 575 576 577 578 579 580 581 582
    date, tim = timestamp.split('T', 1)
    date = date.replace('-', '')
    tim = tim.replace('z', '')
    tim = tim.replace('Z', '')
    zone = None
    if '+' in tim:
        sign = -1
        tim, zone = tim.split('+', 1)
    if '-' in tim:
        sign = 1
        tim, zone = tim.split('-', 1)
    tim = tim.split('.')[0]
    tim = time.strptime(date + 'T' + tim, '%Y%m%dT%H:%M:%S')
    if zone:
583 584 585 586 587 588
        zone = zone.replace(':', '')
        tim = datetime.datetime.fromtimestamp(time.mktime(tim))
        if len(zone) > 2:
            zone = time.strptime(zone, '%H%M')
        else:
            zone = time.strptime(zone, '%H')
589 590 591 592
        zone = datetime.timedelta(hours=zone.tm_hour, minutes=zone.tm_min)
        tim += zone * sign
        tim = tim.timetuple()
    return tim
593

594
from common import gajim
595 596
if gajim.HAVE_PYCURL:
    import pycurl
597
    from io import StringIO
598 599

def convert_bytes(string):
600 601 602 603 604
    suffix = ''
    # IEC standard says KiB = 1024 bytes KB = 1000 bytes
    # but do we use the standard?
    use_kib_mib = gajim.config.get('use_kib_mib')
    align = 1024.
Yann Leboulanger's avatar
Yann Leboulanger committed
605 606 607 608 609 610 611
    bytes_ = float(string)
    if bytes_ >= align:
        bytes_ = round(bytes_/align, 1)
        if bytes_ >= align:
            bytes_ = round(bytes_/align, 1)
            if bytes_ >= align:
                bytes_ = round(bytes_/align, 1)
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
                if use_kib_mib:
                    #GiB means gibibyte
                    suffix = _('%s GiB')
                else:
                    #GB means gigabyte
                    suffix = _('%s GB')
            else:
                if use_kib_mib:
                    #MiB means mibibyte
                    suffix = _('%s MiB')
                else:
                    #MB means megabyte
                    suffix = _('%s MB')
        else:
            if use_kib_mib:
                #KiB means kibibyte
                suffix = _('%s KiB')
            else:
                #KB means kilo bytes
                suffix = _('%s KB')
    else:
        #B means bytes
        suffix = _('%s B')
Yann Leboulanger's avatar
Yann Leboulanger committed
635
    return suffix % str(bytes_)
636 637

def get_contact_dict_for_account(account):
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
    """
    Create a dict of jid, nick -> contact with all contacts of account.

    Can be used for completion lists
    """
    contacts_dict = {}
    for jid in gajim.contacts.get_jid_list(account):
        contact = gajim.contacts.get_contact_with_highest_priority(account,
                        jid)
        contacts_dict[jid] = contact
        name = contact.name
        if name in contacts_dict:
            contact1 = contacts_dict[name]
            del contacts_dict[name]
            contacts_dict['%s (%s)' % (name, contact1.jid)] = contact1
            contacts_dict['%s (%s)' % (name, jid)] = contact
654
        elif contact.name:
655 656 657 658
            if contact.name == gajim.get_nick_from_jid(jid):
                del contacts_dict[jid]
            contacts_dict[name] = contact
    return contacts_dict
659

nkour's avatar
nkour committed
660
def launch_browser_mailer(kind, uri):
661
    # kind = 'url' or 'mail'
662 663 664
    if kind == 'url' and uri.startswith('file://'):
        launch_file_manager(uri)
        return
665 666
    if kind in ('mail', 'sth_at_sth') and not uri.startswith('mailto:'):
        uri = 'mailto:' + uri
667

668 669 670 671 672 673 674 675 676 677
    if kind == 'url' and uri.startswith('www.'):
        uri = 'http://' + uri

    if not gajim.config.get('autodetect_browser_mailer'):
        if kind == 'url':
            command = gajim.config.get('custombrowser')
        elif kind in ('mail', 'sth_at_sth'):
            command = gajim.config.get('custommailapp')
        if command == '': # if no app is configured
            return
678 679 680 681 682 683

        command = build_command(command, uri)
        try:
            exec_command(command)
        except Exception:
            pass
684

685
    else:
686 687 688
        webbrowser.open(uri)


nkour's avatar
nkour committed
689
def launch_file_manager(path_to_open):
690 691
    if not path_to_open.startswith('file://'):
        uri = 'file://' + path_to_open
692 693 694 695 696 697
    if os.name == 'nt':
        try:
            os.startfile(path_to_open) # if pywin32 is installed we open
        except Exception:
            pass
    else:
698
        if not gajim.config.get('autodetect_browser_mailer'):
699
            command = gajim.config.get('custom_file_manager')
700 701 702 703
            if command == '': # if no app is configured
                return
        else:
            command = 'xdg-open'
704 705 706 707 708
        command = build_command(command, path_to_open)
        try:
            exec_command(command)
        except Exception:
            pass
nkour's avatar
nkour committed
709 710

def play_sound(event):
711 712 713 714
    if not gajim.config.get('sounds_on'):
        return
    path_to_soundfile = gajim.config.get_per('soundevents', event, 'path')
    play_sound_file(path_to_soundfile)
715

Yann Leboulanger's avatar
Yann Leboulanger committed
716
def check_soundfile_path(file_, dirs=(gajim.gajimpaths.data_root,
717
gajim.DATA_DIR)):
718 719 720
    """
    Check if the sound file exists

Yann Leboulanger's avatar
Yann Leboulanger committed
721
    :param file_: the file to check, absolute or relative to 'dirs' path
722 723 724 725
    :param dirs: list of knows paths to fallback if the file doesn't exists
                                     (eg: ~/.gajim/sounds/, DATADIR/sounds...).
    :return      the path to file or None if it doesn't exists.
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
726
    if not file_:
727
        return None
Yann Leboulanger's avatar
Yann Leboulanger committed
728 729
    elif os.path.exists(file_):
        return file_
730 731

    for d in dirs:
Yann Leboulanger's avatar
Yann Leboulanger committed
732
        d = os.path.join(d, 'sounds', file_)
733 734 735
        if os.path.exists(d):
            return d
    return None
736

Yann Leboulanger's avatar
Yann Leboulanger committed
737
def strip_soundfile_path(file_, dirs=(gajim.gajimpaths.data_root,
738
gajim.DATA_DIR), abs=True):
739 740 741 742 743
    """
    Remove knowns paths from a sound file

    Filechooser returns absolute path. If path is a known fallback path, we remove it.
    So config have no hardcoded path        to DATA_DIR and text in textfield is shorther.
Yann Leboulanger's avatar
Yann Leboulanger committed
744
    param: file_: the filename to strip.
745 746 747
    param: dirs: list of knowns paths from which the filename should be stripped.
    param:  abs: force absolute path on dirs
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
748
    if not file_:
749 750
        return None

Yann Leboulanger's avatar
Yann Leboulanger committed
751
    name = os.path.basename(file_)
752 753 754 755
    for d in dirs:
        d = os.path.join(d, 'sounds', name)
        if abs:
            d = os.path.abspath(d)
Yann Leboulanger's avatar
Yann Leboulanger committed
756
        if file_ == d:
757
            return name
Yann Leboulanger's avatar
Yann Leboulanger committed
758
    return file_
759

760
def play_sound_file(path_to_soundfile):
761 762 763 764 765 766
    if path_to_soundfile == 'beep':
        exec_command('beep')
        return
    path_to_soundfile = check_soundfile_path(path_to_soundfile)
    if path_to_soundfile is None:
        return
767
    elif sys.platform == 'win32' and HAS_WINSOUND:
768 769 770 771
        try:
            winsound.PlaySound(path_to_soundfile,
                    winsound.SND_FILENAME|winsound.SND_ASYNC)
        except Exception:
772
            log.exception('Sound Playback Error')
773
    elif sys.platform == 'linux':
774
        if gajim.config.get('soundplayer') == '':
775 776 777 778 779 780 781 782 783
            def _oss_play():
                sndfile = wave.open(path_to_soundfile, 'rb')
                (nc, sw, fr, nf, comptype, compname) = sndfile.getparams()
                dev = oss.open('/dev/dsp', 'w')
                dev.setparameters(sw * 8, nc, fr)
                dev.write(sndfile.readframes(nf))
                sndfile.close()
                dev.close()
            gajim.thread_interface(_oss_play)
784 785 786 787
            return
        player = gajim.config.get('soundplayer')
        command = build_command(player, path_to_soundfile)
        exec_command(command)
788 789

def get_global_show():
790 791 792 793 794 795 796 797 798
    maxi = 0
    for account in gajim.connections:
        if not gajim.config.get_per('accounts', account,
        'sync_with_global_status'):
            continue
        connected = gajim.connections[account].connected
        if connected > maxi:
            maxi = connected
    return gajim.SHOW_LIST[maxi]
nkour's avatar
nkour committed
799

nicfit's avatar
nicfit committed
800
def get_global_status():
801 802 803 804 805 806 807 808 809 810
    maxi = 0
    for account in gajim.connections:
        if not gajim.config.get_per('accounts', account,
        'sync_with_global_status'):
            continue
        connected = gajim.connections[account].connected
        if connected > maxi:
            maxi = connected
            status = gajim.connections[account].status
    return status
811

812

813
def statuses_unified():
814 815 816 817 818 819 820 821 822 823 824 825 826
    """
    Test if all statuses are the same
    """
    reference = None
    for account in gajim.connections:
        if not gajim.config.get_per('accounts', account,
        'sync_with_global_status'):
            continue
        if reference is None:
            reference = gajim.connections[account].connected
        elif reference != gajim.connections[account].connected:
            return False
    return True
827

Yann Leboulanger's avatar
Yann Leboulanger committed
828
def get_icon_name_to_show(contact, account = None):
829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
    """
    Get the icon name to show in online, away, requested, etc
    """
    if account and gajim.events.get_nb_roster_events(account, contact.jid):
        return 'event'
    if account and gajim.events.get_nb_roster_events(account,
    contact.get_full_jid()):
        return 'event'
    if account and account in gajim.interface.minimized_controls and \
    contact.jid in gajim.interface.minimized_controls[account] and gajim.interface.\
            minimized_controls[account][contact.jid].get_nb_unread_pm() > 0:
        return 'event'
    if account and contact.jid in gajim.gc_connected[account]:
        if gajim.gc_connected[account][contact.jid]:
            return 'muc_active'
        else:
            return 'muc_inactive'
    if contact.jid.find('@') <= 0: # if not '@' or '@' starts the jid ==> agent
        return contact.show
    if contact.sub in ('both', 'to'):
        return contact.show
    if contact.ask == 'subscribe':
        return 'requested'
    transport = gajim.get_transport_name_from_jid(contact.jid)
    if transport:
        return contact.show
    if contact.show in gajim.SHOW_LIST:
        return contact.show
    return 'not in roster'
nkour's avatar
nkour committed
858

859
def get_full_jid_from_iq(iq_obj):
860
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
861
    Return the full jid (with resource) from an iq
862
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
863
    return parse_jid(str(iq_obj.getFrom()))
864 865

def get_jid_from_iq(iq_obj):
866
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
867
    Return the jid (without resource) from an iq
868 869 870
    """
    jid = get_full_jid_from_iq(iq_obj)
    return gajim.get_jid_without_resource(jid)
871 872

def get_auth_sha(sid, initiator, target):
873 874 875
    """
    Return sha of sid + initiator + target used for proxy auth
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
876 877
    return hashlib.sha1(("%s%s%s" % (sid, initiator, target)).encode('utf-8')).\
        hexdigest()
878

879
def remove_invalid_xml_chars(string):
880 881 882
    if string:
        string = re.sub(gajim.interface.invalid_XML_chars_re, '', string)
    return string
883 884

distro_info = {
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
        'Arch Linux': '/etc/arch-release',
        'Aurox Linux': '/etc/aurox-release',
        'Conectiva Linux': '/etc/conectiva-release',
        'CRUX': '/usr/bin/crux',
        'Debian GNU/Linux': '/etc/debian_version',
        'Fedora Linux': '/etc/fedora-release',
        'Gentoo Linux': '/etc/gentoo-release',
        'Linux from Scratch': '/etc/lfs-release',
        'Mandrake Linux': '/etc/mandrake-release',
        'Slackware Linux': '/etc/slackware-version',
        'Solaris/Sparc': '/etc/release',
        'Source Mage': '/etc/sourcemage_version',
        'SUSE Linux': '/etc/SuSE-release',
        'Sun JDS': '/etc/sun-release',
        'PLD Linux': '/etc/pld-release',
        'Yellow Dog Linux': '/etc/yellowdog-release',
901
        'AgiliaLinux': '/etc/agilialinux-version',
902 903 904
        # many distros use the /etc/redhat-release for compatibility
        # so Redhat is the last
        'Redhat Linux': '/etc/redhat-release'
905 906
}

907
def get_random_string_16():
908 909 910
    """
    Create random string of length 16
    """
Dicson's avatar
Dicson committed
911
    rng = list(range(65, 90))
912 913 914 915
    rng.extend(range(48, 57))
    char_sequence = [chr(e) for e in rng]
    from random import sample
    return ''.join(sample(char_sequence, 16))
916

917
def get_os_info():
918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
    if gajim.os_info:
        return gajim.os_info
    if os.name == 'nt':
        # platform.release() seems to return the name of the windows
        ver = sys.getwindowsversion()
        ver_format = ver[3], ver[0], ver[1]
        win_version = {
                (1, 4, 0): '95',
                (1, 4, 10): '98',
                (1, 4, 90): 'ME',
                (2, 4, 0): 'NT',
                (2, 5, 0): '2000',
                (2, 5, 1): 'XP',
                (2, 5, 2): '2003',
                (2, 6, 0): 'Vista',
                (2, 6, 1): '7',
        }
        if ver_format in win_version:
            os_info = 'Windows' + ' ' + win_version[ver_format]
        else:
            os_info = 'Windows'
        gajim.os_info = os_info
        return os_info
    elif os.name == 'posix':
        executable = 'lsb_release'
        params = ' --description --codename --release --short'
        full_path_to_executable = is_in_path(executable, return_abs_path = True)
        if full_path_to_executable:
            command = executable + params
            p = subprocess.Popen([command], shell=True, stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE, close_fds=True)
            p.wait()
            output = temp_failure_retry(p.stdout.readline).strip()
            # some distros put n/a in places, so remove those
Dicson's avatar
Dicson committed
952
            output = output.decode('utf-8').replace('n/a', '').replace('N/A', '')
953
            gajim.os_info = output
Dicson's avatar
Dicson committed
954 955
            p.stdout.close()
            p.stdin.close()
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
            return output

        # lsb_release executable not available, so parse files
        for distro_name in distro_info:
            path_to_file = distro_info[distro_name]
            if os.path.exists(path_to_file):
                if os.access(path_to_file, os.X_OK):
                    # the file is executable (f.e. CRUX)
                    # yes, then run it and get the first line of output.
                    text = get_output_of_command(path_to_file)[0]
                else:
                    fd = open(path_to_file)
                    text = fd.readline().strip() # get only first line
                    fd.close()
                    if path_to_file.endswith('version'):
                        # sourcemage_version and slackware-version files
                        # have all the info we need (name and version of distro)
                        if not os.path.basename(path_to_file).startswith(
                        'sourcemage') or not\
                        os.path.basename(path_to_file).startswith('slackware'):
                            text = distro_name + ' ' + text
                    elif path_to_file.endswith('aurox-release') or \
                    path_to_file.endswith('arch-release'):
                        # file doesn't have version
                        text = distro_name
Dicson's avatar
Dicson committed
981 982
                    elif path_to_file.endswith('lfs-release'):
                        # file just has version
983 984 985 986 987 988 989 990 991 992 993 994 995 996
                        text = distro_name + ' ' + text
                os_info = text.replace('\n', '')
                gajim.os_info = os_info
                return os_info

        # our last chance, ask uname and strip it
        uname_output = get_output_of_command('uname -sr')
        if uname_output is not None:
            os_info = uname_output[0] # only first line
            gajim.os_info = os_info
            return os_info
    os_info = 'N/A'
    gajim.os_info = os_info
    return os_info
997

998

999 1000
def allow_showing_notification(account, type_='notify_on_new_message',
is_first_message=True):
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014
    """
    Is it allowed to show nofication?

    Check OUR status and if we allow notifications for that status type is the
    option that need to be True e.g.: notify_on_signing is_first_message: set it
    to false when it's not the first message
    """
    if type_ and (not gajim.config.get(type_) or not is_first_message):
        return False
    if gajim.config.get('autopopupaway'): # always show notification
        return True
    if gajim.connections[account].connected in (2, 3): # we're online or chat
        return True
    return False
1015

1016
def allow_popup_window(account):
1017 1018 1019 1020 1021 1022 1023 1024 1025
    """
    Is it allowed to popup windows?
    """
    autopopup = gajim.config.get('autopopup')
    autopopupaway = gajim.config.get('autopopupaway')
    if autopopup and (autopopupaway or \
    gajim.connections[account].connected in (2, 3)): # we're online or chat
        return True
    return False
1026

1027
def allow_sound_notification(account, sound_event):
1028 1029 1030 1031 1032
    if gajim.config.get('sounddnd') or gajim.connections[account].connected != \
    gajim.SHOW_LIST.index('dnd') and gajim.config.get_per('soundevents',
    sound_event, 'enabled'):
        return True
    return False
1033 1034

def get_chat_control(account, contact):
1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
    full_jid_with_resource = contact.jid
    if contact.resource:
        full_jid_with_resource += '/' + contact.resource
    highest_contact = gajim.contacts.get_contact_with_highest_priority(
            account, contact.jid)

    # Look for a chat control that has the given resource, or default to
    # one without resource
    ctrl = gajim.interface.msg_win_mgr.get_control(full_jid_with_resource,
            account)

    if ctrl:
        return ctrl
    elif highest_contact and highest_contact.resource and \
    contact.resource != highest_contact.resource:
        return None
    else:
        # unknown contact or offline message
        return gajim.interface.msg_win_mgr.get_control(contact.jid, account)
1054

1055
def get_notification_icon_tooltip_dict():
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
    """
    Return a dict of the form {acct: {'show': show, 'message': message,
    'event_lines': [list of text lines to show in tooltip]}
    """
    # How many events must there be before they're shown summarized, not per-user
    max_ungrouped_events = 10

    accounts = get_accounts_info()

    # Gather events. (With accounts, when there are more.)
    for account in accounts:
        account_name = account['name']
        account['event_lines'] = []
        # Gather events per-account
        pending_events = gajim.events.get_events(account = account_name)
        messages, non_messages, total_messages, total_non_messages = {}, {}, 0, 0
        for jid in pending_events:
            for event in pending_events[jid]:
                if event.type_.count('file') > 0:
                    # This is a non-messagee event.
                    messages[jid] = non_messages.get(jid, 0) + 1
                    total_non_messages = total_non_messages + 1
                else:
                    # This is a message.
                    messages[jid] = messages.get(jid, 0) + 1
                    total_messages = total_messages + 1
        # Display unread messages numbers, if any
        if total_messages > 0:
            if total_messages > max_ungrouped_events:
                text = ngettext(
                        '%d message pending',
                        '%d messages pending',
                        total_messages, total_messages, total_messages)
                account['event_lines'].append(text)
            else:
                for jid in messages.keys():
                    text = ngettext(
                            '%d message pending',
                            '%d messages pending',
                            messages[jid], messages[jid], messages[jid])
                    contact = gajim.contacts.get_first_contact_from_jid(
                            account['name'], jid)
1098
                    text += ' '
1099
                    if jid in gajim.gc_connected[account['name']]:
1100
                        text += _('from room %s') % (jid)
1101 1102
                    elif contact:
                        name = contact.get_shown_name()
1103
                        text += _('from user %s') % (name)
1104
                    else:
1105
                        text += _('from %s') % (jid)
1106 1107 1108 1109 1110 1111
                    account['event_lines'].append(text)

        # Display unseen events numbers, if any
        if total_non_messages > 0:
            if total_non_messages > max_ungrouped_events:
                text = ngettext(
Dicson's avatar
Dicson committed
1112 1113 1114
                    '%d event pending',
                    '%d events pending',
                    total_non_messages, total_non_messages,total_non_messages)
1115 1116 1117
                account['event_lines'].append(text)
            else:
                for jid in non_messages.keys():
Dicson's avatar
Dicson committed
1118 1119
                    text = ngettext('%d event pending', '%d events pending',
                        non_messages[jid], non_messages[jid], non_messages[jid])
1120
                    text += ' ' + _('from user %s') % (jid)
1121 1122 1123
                    account[account]['event_lines'].append(text)

    return accounts
1124

1125
def get_notification_icon_tooltip_text():
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
    text = None
    # How many events must there be before they're shown summarized, not per-user
    # max_ungrouped_events = 10
    # Character which should be used to indent in the tooltip.
    indent_with = ' '

    accounts = get_notification_icon_tooltip_dict()

    if len(accounts) == 0:
        # No configured account
        return _('Gajim')

    # at least one account present

    # Is there more that one account?
    if len(accounts) == 1:
        show_more_accounts = False
    else:
        show_more_accounts = True

    # If there is only one account, its status is shown on the first line.
    if show_more_accounts:
        text = _('Gajim')
    else:
        text = _('Gajim - %s') % (get_account_status(accounts[0]))

    # Gather and display events. (With accounts, when there are more.)
    for account in accounts:
        account_name = account['name']
        # Set account status, if not set above
        if (show_more_accounts):
            message = '\n' + indent_with + ' %s - %s'
            text += message % (account_name, get_account_status(account))
            # Account list shown, messages need to be indented more
            indent_how = 2
        else:
            # If no account list is shown, messages could have default indenting.
            indent_how = 1
        for line in account['event_lines']:
            text += '\n' + indent_with * indent_how + ' '
            text += line
    return text
1168 1169

def get_accounts_info():
1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
    """
    Helper for notification icon tooltip
    """
    accounts = []
    accounts_list = sorted(gajim.contacts.get_accounts())
    for account in accounts_list:
        status_idx = gajim.connections[account].connected
        # uncomment the following to hide offline accounts
        # if status_idx == 0: continue
        status = gajim.SHOW_LIST[status_idx]
        message = gajim.connections[account].status
        single_line = get_uf_show(status)
        if message is None:
            message = ''
        else:
            message = message.strip()
        if message != '':
            single_line += ': ' + message
        accounts.append({'name': account, 'status_line': single_line,
                        'show': status, 'message': message})
    return accounts
1191

1192 1193

def get_iconset_path(iconset):
1194 1195 1196 1197
    if os.path.isdir(os.path.join(gajim.DATA_DIR, 'iconsets', iconset)):
        return os.path.join(gajim.DATA_DIR, 'iconsets', iconset)
    elif os.path.isdir(os.path.join(gajim.MY_ICONSETS_PATH, iconset)):
        return os.path.join(gajim.MY_ICONSETS_PATH, iconset)
1198

js's avatar
js committed
1199
def get_mood_iconset_path(iconset):
1200 1201 1202 1203
    if os.path.isdir(os.path.join(gajim.DATA_DIR, 'moods', iconset)):
        return os.path.join(gajim.DATA_DIR, 'moods', iconset)
    elif os.path.isdir(os.path.join(gajim.MY_MOOD_ICONSETS_PATH, iconset)):
        return os.path.join(gajim.MY_MOOD_ICONSETS_PATH, iconset)
js's avatar
js committed
1204

js's avatar
js committed
1205
def get_activity_iconset_path(iconset):
1206 1207 1208 1209 1210
    if os.path.isdir(os.path.join(gajim.DATA_DIR, 'activities', iconset)):
        return os.path.join(gajim.DATA_DIR, 'activities', iconset)
    elif os.path.isdir(os.path.join(gajim.MY_ACTIVITY_ICONSETS_PATH,
    iconset)):
        return os.path.join(gajim.MY_ACTIVITY_ICONSETS_PATH, iconset)
js's avatar
js committed
1211

1212
def get_transport_path(transport):
1213 1214 1215 1216 1217 1218 1219 1220
    if os.path.isdir(os.path.join(gajim.DATA_DIR, 'iconsets', 'transports',
    transport)):
        return os.path.join(gajim.DATA_DIR, 'iconsets', 'transports', transport)
    elif os.path.isdir(os.path.join(gajim.MY_ICONSETS_PATH, 'transports',
    transport)):
        return os.path.join(gajim.MY_ICONSETS_PATH, 'transports', transport)
    # No transport folder found, use default jabber one
    return get_iconset_path(gajim.config.get('iconset'))
steve-e's avatar
steve-e committed
1221 1222

def prepare_and_validate_gpg_keyID(account, jid, keyID):
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240
    """
    Return an eight char long keyID that can be used with for GPG encryption
    with this contact

    If the given keyID is None, return UNKNOWN; if the key does not match the
    assigned key XXXXXXXXMISMATCH is returned. If the key is trusted and not yet
    assigned, assign it.
    """
    if gajim.connections[account].USE_GPG:
        if keyID and len(keyID) == 16:
            keyID = keyID[8:]

        attached_keys = gajim.config.get_per('accounts', account,
                'attached_gpg_keys').split()

        if jid in attached_keys and keyID:
            attachedkeyID = attached_keys[attached_keys.index(jid) + 1]
            if attachedkeyID != keyID:
1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
                # Get signing subkeys for the attached key
                subkeys = []
                for key in gajim.connections[account].gpg.list_keys():
                    if key['keyid'][8:] == attachedkeyID:
                        subkeys = [subkey[0][8:] for subkey in key['subkeys'] \
                            if subkey[1] == 's']
                        break

                if keyID not in subkeys:
                    # Mismatch! Another gpg key was expected
                    keyID += 'MISMATCH'
1252 1253 1254 1255
        elif jid in attached_keys:
            # An unsigned presence, just use the assigned key
            keyID = attached_keys[attached_keys.index(jid) + 1]
        elif keyID:
1256
            full_key = gajim.connections[account].ask_gpg_keys(keyID=keyID)
1257
            # Assign the corresponding key, if we have it in our keyring
1258
            if full_key:
1259 1260
                for u in gajim.contacts.get_contacts(account, jid):
                    u.keyID = keyID
Dicson's avatar
Dicson committed
1261 1262
                keys_str = gajim.config.get_per('accounts', account,
                    'attached_gpg_keys')
1263
                keys_str += jid + ' ' + keyID + ' '
Dicson's avatar
Dicson committed
1264 1265
                gajim.config.set_per('accounts', account, 'attached_gpg_keys',
                    keys_str)
1266 1267 1268
        elif keyID is None:
            keyID = 'UNKNOWN'
    return keyID
steve-e's avatar
steve-e committed
1269

1270
def update_optional_features(account = None):
1271
    import nbxmpp
1272 1273 1274 1275 1276 1277 1278
    if account:
        accounts = [account]
    else:
        accounts = [a for a in gajim.connections]
    for a in accounts:
        gajim.gajim_optional_features[a] = []
        if gajim.config.get_per('accounts', a, 'subscribe_mood'):
1279
            gajim.gajim_optional_features[a].append(nbxmpp.NS_MOOD + '+notify')
1280
        if gajim.config.get_per('accounts', a, 'subscribe_activity'):
1281 1282
            gajim.gajim_optional_features[a].append(nbxmpp.NS_ACTIVITY + \
                '+notify')
1283
        if gajim.config.get_per('accounts', a, 'publish_tune'):
1284
            gajim.gajim_optional_features[a].append(nbxmpp.NS_TUNE)
1285
        if gajim.config.get_per('accounts', a, 'publish_location'):
1286
            gajim.gajim_optional_features[a].append(nbxmpp.NS_LOCATION)
1287
        if gajim.config.get_per('accounts', a, 'subscribe_tune'):
1288
            gajim.gajim_optional_features[a].append(nbxmpp.NS_TUNE + '+notify')
1289
        if gajim.config.get_per('accounts', a, 'subscribe_nick'):
1290
            gajim.gajim_optional_features[a].append(nbxmpp.NS_NICK + '+notify')
1291
        if gajim.config.get_per('accounts', a, 'subscribe_location'):
1292 1293
            gajim.gajim_optional_features[a].append(nbxmpp.NS_LOCATION + \
                '+notify')
1294
        if gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
1295
            gajim.gajim_optional_features[a].append(nbxmpp.NS_CHATSTATES)
1296
        if not gajim.config.get('ignore_incoming_xhtml'):
1297
            gajim.gajim_optional_features[a].append(nbxmpp.NS_XHTML_IM)
1298 1299
        if gajim.HAVE_PYCRYPTO \
        and gajim.config.get_per('accounts', a, 'enable_esessions'):
1300
            gajim.gajim_optional_features[a].append(nbxmpp.NS_ESESSION)
1301
        if gajim.config.get_per('accounts', a, 'answer_receipts'):
1302
            gajim.gajim_optional_features[a].append(nbxmpp.NS_RECEIPTS)
1303
        gajim.gajim_optional_features[a].append(nbxmpp.NS_JINGLE)
1304
        if gajim.HAVE_FARSTREAM:
1305 1306 1307 1308
            gajim.gajim_optional_features[a].append(nbxmpp.NS_JINGLE_RTP)
            gajim.gajim_optional_features[a].append(nbxmpp.NS_JINGLE_RTP_AUDIO)
            gajim.gajim_optional_features[a].append(nbxmpp.NS_JINGLE_RTP_VIDEO)
            gajim.gajim_optional_features[a].append(nbxmpp.NS_JINGLE_ICE_UDP)
1309
        gajim.gajim_optional_features[a].append(