helpers.py 50.2 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# 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>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
27

Alexander Cherniuk's avatar
Alexander Cherniuk committed
28
import sys
29
import re
30
import os
31
import subprocess
nkour's avatar
nkour committed
32
import urllib
33
import webbrowser
34
import errno
35
import select
36
import base64
37
import hashlib
38
import shlex
39 40
import socket
import time
41
import logging
42
import json
43
import shutil
Philipp Hörist's avatar
Philipp Hörist committed
44
from datetime import datetime, timedelta
Philipp Hörist's avatar
Philipp Hörist committed
45
from distutils.version import LooseVersion as V
46
from encodings.punycode import punycode_encode
47
from string import Template
48

49
import nbxmpp
50 51
from nbxmpp.stringprepare import nameprep
import precis_i18n.codec  # pylint: disable=unused-import
52

53 54
from gajim.common import caps_cache
from gajim.common import configpaths
André's avatar
André committed
55
from gajim.common.i18n import Q_
56
from gajim.common.i18n import _
André's avatar
André committed
57
from gajim.common.i18n import ngettext
nkour's avatar
nkour committed
58

59 60
log = logging.getLogger('gajim.c.helpers')

61
special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
62

63

64
class InvalidFormat(Exception):
65
    pass
66

67

68
def decompose_jid(jidstring):
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
    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]
84
            resource = jidstring[res_sep + 1:]
85 86 87
    else:
        if res_sep == -1:
            # user@host
88
            user = jidstring[0:user_sep]
89 90 91 92
            server = jidstring[user_sep + 1:]
        else:
            if user_sep < res_sep:
                # user@host/resource
93
                user = jidstring[0:user_sep]
94
                server = jidstring[user_sep + 1:user_sep + (res_sep - user_sep)]
95
                resource = jidstring[res_sep + 1:]
96 97 98
            else:
                # server/resource (with an @ in resource)
                server = jidstring[0:res_sep]
99
                resource = jidstring[res_sep + 1:]
100
    return user, server, resource
101 102

def parse_jid(jidstring):
103 104 105 106 107
    """
    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
108

109
    return prep(*decompose_jid(jidstring))
110

111
def idn_to_ascii(host):
112 113 114 115 116 117 118 119
    """
    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:
120 121 122 123
        if label:
            converted_labels.append(idna.ToASCII(label).decode('utf-8'))
        else:
            converted_labels.append('')
124
    return ".".join(converted_labels)
125

126
def ascii_to_idn(host):
127 128 129 130 131 132 133 134 135 136
    """
    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)
137

138 139 140 141
def puny_encode_url(url):
    _url = url
    if '//' not in _url:
        _url = '//' + _url
142 143 144 145 146 147
    try:
        o = urllib.parse.urlparse(_url)
        p_loc = idn_to_ascii(o.netloc)
    except Exception:
        log.debug('urlparse failed: %s', url)
        return False
148 149
    return url.replace(o.netloc, p_loc)

150
def parse_resource(resource):
151 152 153 154 155
    """
    Perform stringprep on resource and return it
    """
    if resource:
        try:
156
            return resource.encode('OpaqueString').decode('utf-8')
157
        except UnicodeError:
158
            raise InvalidFormat('Invalid character in resource.')
159 160

def prep(user, server, resource):
161 162 163 164 165
    """
    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
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

    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:
185
            if server.endswith('.'):  # RFC7622, 3.2
186
                server = server[:-1]
187
            if not server or len(server.encode('utf-8')) > 1023:
188
                raise InvalidFormat(_('Server must be between 1 and 1023 bytes'))
189
            try:
190
                server = nameprep.prepare(server)
191 192 193 194 195
            except UnicodeError:
                raise InvalidFormat(_('Invalid character in hostname.'))
        else:
            raise InvalidFormat(_('Server address required.'))

196
    if user is not None:
197
        if not user or len(user.encode('utf-8')) > 1023:
198
            raise InvalidFormat(_('Username must be between 1 and 1023 bytes'))
199
        try:
200
            user = user.encode('UsernameCaseMapped').decode('utf-8')
201
        except UnicodeError:
202
            raise InvalidFormat(_('Invalid character in username.'))
203 204 205
    else:
        user = None

206
    if resource is not None:
207
        if not resource or len(resource.encode('utf-8')) > 1023:
208
            raise InvalidFormat(_('Resource must be between 1 and 1023 bytes'))
209
        try:
210
            resource = resource.encode('OpaqueString').decode('utf-8')
211
        except UnicodeError:
212
            raise InvalidFormat(_('Invalid character in resource.'))
213 214 215 216 217 218
    else:
        resource = None

    if user:
        if resource:
            return '%s@%s/%s' % (user, server, resource)
219 220 221 222 223
        return '%s@%s' % (user, server)

    if resource:
        return '%s/%s' % (server, resource)
    return server
224

225
def windowsify(s):
226 227 228
    if os.name == 'nt':
        return s.capitalize()
    return s
229

230
def temp_failure_retry(func, *args, **kwargs):
231 232 233
    while True:
        try:
            return func(*args, **kwargs)
Yann Leboulanger's avatar
Yann Leboulanger committed
234
        except (os.error, IOError, select.error) as ex:
235 236 237 238
            if ex.errno == errno.EINTR:
                continue
            else:
                raise
239

240
def get_uf_show(show, use_mnemonic=False):
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
    """
    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:
265
            uf_show = Q_('?user status:_Available')
266
        else:
267
            uf_show = Q_('?user status:Available')
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    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')
291
    return uf_show
nkour's avatar
nkour committed
292

Philipp Hörist's avatar
Philipp Hörist committed
293 294 295
def get_css_show_color(show):
    if show in ('online', 'chat', 'invisible'):
        return 'status-online'
296
    if show in ('offline', 'not in roster', 'requested'):
Philipp Hörist's avatar
Philipp Hörist committed
297
        return None
298
    if show in ('xa', 'dnd'):
Philipp Hörist's avatar
Philipp Hörist committed
299
        return 'status-dnd'
300
    if show == 'away':
Philipp Hörist's avatar
Philipp Hörist committed
301 302
        return 'status-away'

303
def get_uf_sub(sub):
304 305 306 307 308 309 310 311 312
    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:
313
        uf_sub = _('Unknown')
314

315
    return uf_sub
nkour's avatar
nkour committed
316

317
def get_uf_ask(ask):
318 319 320 321 322 323
    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
324

325
    return uf_ask
326

327
def get_uf_role(role, plural=False):
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    ''' 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
347

348
def get_uf_affiliation(affiliation):
349 350 351 352 353 354 355 356 357 358 359 360
    '''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
361

362
def get_sorted_keys(adict):
363 364
    keys = sorted(adict.keys())
    return keys
365 366

def to_one_line(msg):
367 368 369 370 371 372 373 374
    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
375 376

def from_one_line(msg):
377 378 379 380 381 382 383 384 385 386 387 388 389
    # (?<!\\) 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
390 391

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

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
def find_soundplayer():
    if sys.platform in ('win32', 'darwin'):
        return

    if app.config.get('soundplayer') != '':
        return

    commands = ('aucat', 'paplay', 'aplay', 'play', 'ossplay')
    for command in commands:
        if shutil.which(command) is not None:
            if command == 'paplay':
                command += ' -n gajim --property=media.role=event'
            elif command in ('aplay', 'play'):
                command += ' -q'
            elif command == 'ossplay':
                command += ' -qq'
            elif command == 'aucat':
                command += ' -i'
            app.config.set('soundplayer', command)
            break
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
        p = subprocess.Popen(args)
440
        app.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):
Yann Leboulanger's avatar
Yann Leboulanger committed
450
    path = urllib.parse.unquote(uri) # 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 get_xmpp_show(show):
462 463 464
    if show in ('online', 'offline'):
        return None
    return show
465 466

def sanitize_filename(filename):
467 468 469 470 471 472
    """
    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:
473 474
        hash_ = hashlib.md5(filename.encode('utf-8'))
        filename = base64.b64encode(hash_.digest()).decode('utf-8')
475

476 477
    # make it latin chars only
    filename = punycode_encode(filename).decode('utf-8')
478 479 480 481 482 483 484
    filename = filename.replace('/', '_')
    if os.name == 'nt':
        filename = filename.replace('?', '_').replace(':', '_')\
                .replace('\\', '_').replace('"', "'").replace('|', '_')\
                .replace('*', '_').replace('<', '_').replace('>', '_')

    return filename
485

486
def reduce_chars_newlines(text, max_chars=0, max_lines=0):
487 488 489 490 491 492 493 494 495
    """
    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:
496
            string = string[:max_chars - 3] + '…'
497 498 499 500 501 502 503 504 505 506 507 508
        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:
509
            reduced_text += '…'
510 511 512
    else:
        reduced_text = ''
    return reduced_text
513 514

def get_account_status(account):
515 516
    status = reduce_chars_newlines(account['status_line'], 100, 1)
    return status
517 518

def datetime_tuple(timestamp):
519 520 521 522 523 524 525 526 527 528
    """
    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.
    """
529 530 531 532 533 534 535 536 537 538 539 540 541 542
    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:
543
        zone = zone.replace(':', '')
544
        tim = datetime.fromtimestamp(time.mktime(tim))
545 546 547 548
        if len(zone) > 2:
            zone = time.strptime(zone, '%H%M')
        else:
            zone = time.strptime(zone, '%H')
549
        zone = timedelta(hours=zone.tm_hour, minutes=zone.tm_min)
550 551 552
        tim += zone * sign
        tim = tim.timetuple()
    return tim
553

554

555
from gajim.common import app
556
if app.is_installed('PYCURL'):
557
    import pycurl
558
    from io import StringIO
559 560

def convert_bytes(string):
561 562 563
    suffix = ''
    # IEC standard says KiB = 1024 bytes KB = 1000 bytes
    # but do we use the standard?
564
    use_kib_mib = app.config.get('use_kib_mib')
565
    align = 1024.
Yann Leboulanger's avatar
Yann Leboulanger committed
566 567 568 569 570 571 572
    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)
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
                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
596
    return suffix % str(bytes_)
597 598

def get_contact_dict_for_account(account):
599 600 601 602 603 604
    """
    Create a dict of jid, nick -> contact with all contacts of account.

    Can be used for completion lists
    """
    contacts_dict = {}
605 606
    for jid in app.contacts.get_jid_list(account):
        contact = app.contacts.get_contact_with_highest_priority(account,
607 608 609 610 611 612 613 614
                        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
615
        elif contact.name:
616
            if contact.name == app.get_nick_from_jid(jid):
617 618 619
                del contacts_dict[jid]
            contacts_dict[name] = contact
    return contacts_dict
620

nkour's avatar
nkour committed
621
def launch_browser_mailer(kind, uri):
622
    # kind = 'url' or 'mail'
623 624 625
    if kind == 'url' and uri.startswith('file://'):
        launch_file_manager(uri)
        return
626 627
    if kind in ('mail', 'sth_at_sth') and not uri.startswith('mailto:'):
        uri = 'mailto:' + uri
628

629 630 631
    if kind == 'url' and uri.startswith('www.'):
        uri = 'http://' + uri

632
    if not app.config.get('autodetect_browser_mailer'):
633
        if kind == 'url':
634
            command = app.config.get('custombrowser')
635
        elif kind in ('mail', 'sth_at_sth'):
636
            command = app.config.get('custommailapp')
637 638
        if command == '': # if no app is configured
            return
639 640 641 642 643 644

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

646
    else:
647 648 649
        webbrowser.open(uri)


nkour's avatar
nkour committed
650
def launch_file_manager(path_to_open):
651 652 653 654 655 656
    if os.name == 'nt':
        try:
            os.startfile(path_to_open) # if pywin32 is installed we open
        except Exception:
            pass
    else:
657 658
        if not app.config.get('autodetect_browser_mailer'):
            command = app.config.get('custom_file_manager')
659 660 661 662
            if command == '': # if no app is configured
                return
        else:
            command = 'xdg-open'
663 664 665 666 667
        command = build_command(command, path_to_open)
        try:
            exec_command(command)
        except Exception:
            pass
nkour's avatar
nkour committed
668 669

def play_sound(event):
670
    if not app.config.get('sounds_on'):
671
        return
672
    path_to_soundfile = app.config.get_per('soundevents', event, 'path')
673
    play_sound_file(path_to_soundfile)
674

Philipp Hörist's avatar
Philipp Hörist committed
675
def check_soundfile_path(file_, dirs=None):
676 677 678
    """
    Check if the sound file exists

Yann Leboulanger's avatar
Yann Leboulanger committed
679
    :param file_: the file to check, absolute or relative to 'dirs' path
680 681 682 683
    :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.
    """
Philipp Hörist's avatar
Philipp Hörist committed
684
    if dirs is None:
685 686
        dirs = [configpaths.get('MY_DATA'),
                configpaths.get('DATA')]
Philipp Hörist's avatar
Philipp Hörist committed
687

Yann Leboulanger's avatar
Yann Leboulanger committed
688
    if not file_:
689
        return None
690
    if os.path.exists(file_):
Yann Leboulanger's avatar
Yann Leboulanger committed
691
        return file_
692 693

    for d in dirs:
Yann Leboulanger's avatar
Yann Leboulanger committed
694
        d = os.path.join(d, 'sounds', file_)
695 696 697
        if os.path.exists(d):
            return d
    return None
698

699
def strip_soundfile_path(file_, dirs=None, abs_=True):
700 701 702 703 704
    """
    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
705
    param: file_: the filename to strip.
706
    param: dirs: list of knowns paths from which the filename should be stripped.
707
    param:  abs_: force absolute path on dirs
708
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
709
    if not file_:
710 711
        return None

Philipp Hörist's avatar
Philipp Hörist committed
712
    if dirs is None:
713 714
        dirs = [configpaths.get('MY_DATA'),
                configpaths.get('DATA')]
Philipp Hörist's avatar
Philipp Hörist committed
715

Yann Leboulanger's avatar
Yann Leboulanger committed
716
    name = os.path.basename(file_)
717 718
    for d in dirs:
        d = os.path.join(d, 'sounds', name)
719
        if abs_:
720
            d = os.path.abspath(d)
Yann Leboulanger's avatar
Yann Leboulanger committed
721
        if file_ == d:
722
            return name
Yann Leboulanger's avatar
Yann Leboulanger committed
723
    return file_
724

725
def play_sound_file(path_to_soundfile):
726 727 728
    path_to_soundfile = check_soundfile_path(path_to_soundfile)
    if path_to_soundfile is None:
        return
729 730 731

    if sys.platform == 'win32':
        import winsound
732 733
        try:
            winsound.PlaySound(path_to_soundfile,
734
                               winsound.SND_FILENAME|winsound.SND_ASYNC)
735
        except Exception:
736
            log.exception('Sound Playback Error')
737

738
    elif sys.platform == 'darwin':
Philipp Hörist's avatar
Philipp Hörist committed
739 740 741 742
        try:
            from AppKit import NSSound
        except ImportError:
            log.exception('Sound Playback Error')
743
            return
Philipp Hörist's avatar
Philipp Hörist committed
744

Philipp Hörist's avatar
Philipp Hörist committed
745 746 747
        sound = NSSound.alloc()
        sound.initWithContentsOfFile_byReference_(path_to_soundfile, True)
        sound.play()
748

749
    elif app.config.get('soundplayer') == '':
Philipp Hörist's avatar
Philipp Hörist committed
750 751 752 753 754 755 756
        try:
            import wave
            import ossaudiodev
        except Exception:
            log.exception('Sound Playback Error')
            return

757 758 759
        def _oss_play():
            sndfile = wave.open(path_to_soundfile, 'rb')
            nc, sw, fr, nf, _comptype, _compname = sndfile.getparams()
Philipp Hörist's avatar
Philipp Hörist committed
760
            dev = ossaudiodev.open('/dev/dsp', 'w')
761 762 763 764 765 766 767 768 769 770 771
            dev.setparameters(sw * 8, nc, fr)
            dev.write(sndfile.readframes(nf))
            sndfile.close()
            dev.close()
        app.thread_interface(_oss_play)

    else:
        player = app.config.get('soundplayer')
        command = build_command(player, path_to_soundfile)
        exec_command(command)

772
def get_global_show():
773
    maxi = 0
774 775
    for account in app.connections:
        if not app.config.get_per('accounts', account,
776 777
        'sync_with_global_status'):
            continue
778
        connected = app.connections[account].connected
779 780
        if connected > maxi:
            maxi = connected
781
    return app.SHOW_LIST[maxi]
nkour's avatar
nkour committed
782

nicfit's avatar
nicfit committed
783
def get_global_status():
784
    maxi = 0
785 786
    for account in app.connections:
        if not app.config.get_per('accounts', account,
787 788
        'sync_with_global_status'):
            continue
789
        connected = app.connections[account].connected
790 791
        if connected > maxi:
            maxi = connected
792
            status = app.connections[account].status
793
    return status
794

795

796
def statuses_unified():
797 798 799 800
    """
    Test if all statuses are the same
    """
    reference = None
801 802
    for account in app.connections:
        if not app.config.get_per('accounts', account,
803 804 805
        'sync_with_global_status'):
            continue
        if reference is None:
806 807
            reference = app.connections[account].connected
        elif reference != app.connections[account].connected:
808 809
            return False
    return True
810

811
def get_icon_name_to_show(contact, account=None):
812 813 814
    """
    Get the icon name to show in online, away, requested, etc
    """
815
    if account and app.events.get_nb_roster_events(account, contact.jid):
816
        return 'event'
817 818
    if account and app.events.get_nb_roster_events(
        account, contact.get_full_jid()):
819
        return 'event'
820 821
    if account and account in app.interface.minimized_controls and \
    contact.jid in app.interface.minimized_controls[account] and app.interface.\
822 823
            minimized_controls[account][contact.jid].get_nb_unread_pm() > 0:
        return 'event'
824 825
    if account and contact.jid in app.gc_connected[account]:
        if app.gc_connected[account][contact.jid]:
826
            return 'muc_active'
827
        return 'muc_inactive'
828 829 830 831 832 833
    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'
834
    transport = app.get_transport_name_from_jid(contact.jid)
835 836
    if transport:
        return contact.show
837
    if contact.show in app.SHOW_LIST:
838 839
        return contact.show
    return 'not in roster'
nkour's avatar
nkour committed
840

841
def get_full_jid_from_iq(iq_obj):
842
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
843
    Return the full jid (with resource) from an iq
844
    """
Philipp Hörist's avatar
Philipp Hörist committed
845 846 847
    jid = iq_obj.getFrom()
    if jid is None:
        return None
Yann Leboulanger's avatar
Yann Leboulanger committed
848
    return parse_jid(str(iq_obj.getFrom()))
849 850

def get_jid_from_iq(iq_obj):
851
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
852
    Return the jid (without resource) from an iq
853 854
    """
    jid = get_full_jid_from_iq(iq_obj)
855
    return app.get_jid_without_resource(jid)
856 857

def get_auth_sha(sid, initiator, target):
858 859 860
    """
    Return sha of sid + initiator + target used for proxy auth
    """
Yann Leboulanger's avatar
Yann Leboulanger committed
861 862
    return hashlib.sha1(("%s%s%s" % (sid, initiator, target)).encode('utf-8')).\
        hexdigest()
863

864
def remove_invalid_xml_chars(string):
865
    if string:
866
        string = re.sub(app.interface.invalid_XML_chars_re, '', string)
867
    return string
868

869
def get_random_string_16():
870 871 872
    """
    Create random string of length 16
    """
Dicson's avatar
Dicson committed
873
    rng = list(range(65, 90))
874 875 876 877
    rng.extend(range(48, 57))
    char_sequence = [chr(e) for e in rng]
    from random import sample
    return ''.join(sample(char_sequence, 16))
878

879
def get_os_info():
880 881
    if app.os_info:
        return app.os_info
882 883 884 885
    app.os_info = 'N/A'
    if os.name == 'nt' or sys.platform == 'darwin':
        import platform
        app.os_info = platform.system() + " " + platform.release()
886
    elif os.name == 'posix':
887 888 889 890 891 892 893
        try:
            import distro
            app.os_info = distro.name(pretty=True)
        except ImportError:
            import platform
            app.os_info = platform.system()
    return app.os_info
894

895 896
def allow_showing_notification(account, type_='notify_on_new_message',
is_first_message=True):
897 898 899 900 901 902 903
    """
    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
    """
904
    if type_ and (not app.config.get(type_) or not is_first_message):
905
        return False
906
    if app.config.get('autopopupaway'): # always show notification
907
        return True
908
    if app.connections[account].connected in (2, 3): # we're online or chat
909 910
        return True