common.py 16.3 KB
Newer Older
louiz’'s avatar
louiz’ committed
1 2 3 4 5
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7

8
"""
9
Various useful functions.
10
"""
11

12
from sys import version_info
13
from datetime import datetime, timedelta
14
from sleekxmpp import JID, InvalidJID
15

16 17 18 19
import base64
import os
import mimetypes
import hashlib
20
import subprocess
21
import time
22
import string
23
import poezio_shlex as shlex
24

25 26 27 28 29

# Needed to avoid datetime.datetime.timestamp()
# on python < 3.3. Older versions do not get good dst detection.
OLD_PYTHON = (version_info.major + version_info.minor/10) < 3.3

30 31 32 33 34 35
ROOM_STATE_NONE = 11
ROOM_STATE_CURRENT = 10
ROOM_STATE_PRIVATE = 15
ROOM_STATE_MESSAGE = 12
ROOM_STATE_HL = 13

36
def get_base64_from_file(path):
37 38
    """
    Convert the content of a file to base64
39 40 41 42 43 44 45

    :param str path: The path of the file to convert.
    :return: A tuple of (encoded data, mime type, sha1 hash) if
        the file exists and does not exceeds the upper size limit of 16384.
    :return: (None, None, error message) if it fails
    :rtype: :py:class:`tuple`

46
    """
47 48 49 50 51
    if not os.path.isfile(path):
        return (None, None, "File does not exist")
    size = os.path.getsize(path)
    if size > 16384:
        return (None, None,"File is too big")
52 53
    fdes = open(path, 'rb')
    data = fdes.read()
54 55 56 57
    encoded = base64.encodestring(data)
    sha1 = hashlib.sha1(data).hexdigest()
    mime_type = mimetypes.guess_type(path)[0]
    return (encoded, mime_type, sha1)
58 59

def get_output_of_command(command):
60
    """
61 62 63 64 65
    Runs a command and returns its output.

    :param str command: The command to run.
    :return: The output or None
    :rtype: :py:class:`str`
66
    """
67
    try:
68 69
        return subprocess.check_output(command.split()).decode('utf-8').split('\n')
    except subprocess.CalledProcessError:
70 71 72 73
        return None

def is_in_path(command, return_abs_path=False):
    """
74 75 76 77 78 79 80 81
    Check if *command* is in the $PATH or not.

    :param str command: The command to be checked.
    :param bool return_abs_path: Return the absolute path of the command instead
        of True if the command is found.
    :return: True if the command is found, the command path if the command is found
        and *return_abs_path* is True, otherwise False.

82 83 84 85 86 87 88 89 90 91 92 93 94
    """
    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

95
DISTRO_INFO = {
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        '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',
        # many distros use the /etc/redhat-release for compatibility
        # so Redhat is the last
        'Redhat Linux': '/etc/redhat-release'
}
116

117
def get_os_info():
118 119 120
    """
    Returns a detailed and well formated string containing
    informations about the operating system
121 122

    :rtype: str
123 124
    """
    if os.name == 'posix':
125 126 127 128 129
        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
130 131 132 133 134
            process = subprocess.Popen([command], shell=True,
                                       stdin=subprocess.PIPE,
                                       stdout=subprocess.PIPE,
                                       close_fds=True)
            process.wait()
135
            output = process.stdout.readline().decode('utf-8').strip()
136 137 138 139 140
            # some distros put n/a in places, so remove those
            output = output.replace('n/a', '').replace('N/A', '')
            return output

        # lsb_release executable not available, so parse files
141 142
        for distro_name in DISTRO_INFO:
            path_to_file = DISTRO_INFO[distro_name]
143 144 145 146 147 148
            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:
149
                    fdes = open(path_to_file, encoding='utf-8')
150 151
                    text = fdes.readline().strip() # get only first line
                    fdes.close()
152 153 154 155 156 157 158 159 160 161 162
                    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
163 164
                    elif path_to_file.endswith('lfs-release'):
                        # file just has version
165 166 167 168 169 170 171 172 173 174 175
                        text = distro_name + ' ' + text
                os_info = text.replace('\n', '')
                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
            return os_info
    os_info = 'N/A'
    return os_info
176 177 178

def datetime_tuple(timestamp):
    """
179
    Convert a timestamp using strptime and the format: %Y%m%dT%H:%M:%S.
180

181
    Because various datetime formats are used, the following exceptions
182
    are handled:
183 184 185 186 187 188 189 190 191

    * 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 '-' chars removed to meet the above format.

    :param str timestamp: The string containing the formatted date.
    :return: The date.
    :rtype: :py:class:`datetime.datetime`

mathieui's avatar
mathieui committed
192
    >>> time.timezone = 0; time.altzone = 0
193
    >>> datetime_tuple('20130226T06:23:12')
mathieui's avatar
mathieui committed
194 195 196 197 198
    datetime.datetime(2013, 2, 26, 6, 23, 12)
    >>> datetime_tuple('2013-02-26T06:23:12+02:00')
    datetime.datetime(2013, 2, 26, 4, 23, 12)
    >>> time.timezone = -3600; time.altzone = -3600
    >>> datetime_tuple('20130226T07:23:12')
199
    datetime.datetime(2013, 2, 26, 8, 23, 12)
mathieui's avatar
mathieui committed
200 201
    >>> datetime_tuple('2013-02-26T07:23:12+02:00')
    datetime.datetime(2013, 2, 26, 6, 23, 12)
202
    """
mathieui's avatar
mathieui committed
203 204 205
    timestamp = timestamp.replace('-', '', 2).replace(':', '')
    date = timestamp[:15]
    tz_msg = timestamp[15:]
206
    try:
mathieui's avatar
mathieui committed
207
        ret = datetime.strptime(date, '%Y%m%dT%H%M%S')
208
    except Exception as e:
mathieui's avatar
mathieui committed
209 210 211 212 213 214 215 216 217 218 219
        ret = datetime.now()
    # add the message timezone if any
    try:
        if tz_msg and tz_msg != 'Z':
            tz_mod = -1 if tz_msg[0] == '-' else 1
            tz_msg = time.strptime(tz_msg[1:], '%H%M')
            tz_msg = tz_msg.tm_hour * 3600 + tz_msg.tm_min * 60
            tz_msg = timedelta(seconds=tz_mod * tz_msg)
            ret -= tz_msg
    except Exception as e:
        pass # ignore if we got a badly-formatted offset
220
    # convert UTC to local time, with DST etc.
mathieui's avatar
mathieui committed
221 222 223 224 225
    if time.daylight and time.localtime().tm_isdst:
        tz = timedelta(seconds=-time.altzone)
    else:
        tz = timedelta(seconds=-time.timezone)
    ret += tz
226
    return ret
227

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
def get_utc_time(local_time=None):
    """
    Get the current time in UTC

    :param datetime local_time: The current local time
    :return: The current UTC time
    >>> delta = timedelta(seconds=-3600)
    >>> d = datetime.now()
    >>> time.timezone = -3600; time.altzone = -3600
    >>> get_utc_time(local_time=d) == d + delta
    True
    """
    if local_time is None:
        local_time = datetime.now()
        isdst = time.localtime().tm_isdst
    else:
244
        if OLD_PYTHON:
245
            isdst = time.localtime(int(local_time.strftime("%s"))).tm_isdst
246 247
        else:
            isdst = time.localtime(int(local_time.timestamp())).tm_isdst
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267

    if time.daylight and isdst:
        tz = timedelta(seconds=time.altzone)
    else:
        tz = timedelta(seconds=time.timezone)

    utc_time = local_time + tz

    return utc_time

def get_local_time(utc_time):
    """
    Get the local time from an UTC time

    >>> delta = timedelta(seconds=-3600)
    >>> d = datetime.now()
    >>> time.timezone = -3600; time.altzone = -3600
    >>> get_local_time(d) == d - delta
    True
    """
268
    if OLD_PYTHON:
269
        isdst = time.localtime(int(utc_time.strftime("%s"))).tm_isdst
270 271
    else:
        isdst = time.localtime(int(utc_time.timestamp())).tm_isdst
272

273 274 275 276 277 278 279 280 281
    if time.daylight and isdst:
        tz = timedelta(seconds=time.altzone)
    else:
        tz = timedelta(seconds=time.timezone)

    local_time = utc_time - tz

    return local_time

mathieui's avatar
mathieui committed
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
def find_delayed_tag(message):
    """
    Check if a message is delayed or not.

    :param sleekxmpp.Message message: The message to check.
    :return: A tuple containing (True, the datetime) or (False, None)
    :rtype: :py:class:`tuple`
    """

    delay_tag = message.find('{urn:xmpp:delay}delay')
    if delay_tag is not None:
        delayed = True
        date = datetime_tuple(delay_tag.attrib['stamp'])
    else:
        # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html
        # But it sucks, please, Jabber servers, don't do this :(
        delay_tag = message.find('{jabber:x:delay}x')
        if delay_tag is not None:
            delayed = True
301
            date = datetime_tuple(delay_tag.attrib['stamp'])
mathieui's avatar
mathieui committed
302 303 304 305 306
        else:
            delayed = False
            date = None
    return (delayed, date)

louiz’'s avatar
louiz’ committed
307
def shell_split(st):
308 309 310 311 312 313 314 315 316 317
    """
    Split a string correctly according to the quotes
    around the elements.

    :param str st: The string to split.
    :return: A list of the different of the string.
    :rtype: :py:class:`list`

    >>> shell_split('"sdf 1" "toto 2"')
    ['sdf 1', 'toto 2']
318 319 320 321 322 323
    >>> shell_split('toto "titi"')
    ['toto', 'titi']
    >>> shell_split('toto ""')
    ['toto', '']
    >>> shell_split('"toto titi" toto ""')
    ['toto titi', 'toto', '']
mathieui's avatar
mathieui committed
324 325
    >>> shell_split('toto "titi')
    ['toto', 'titi']
326
    """
327 328 329 330 331
    sh = shlex.shlex(st)
    ret = []
    w = sh.get_token()
    while w and w[2] is not None:
        ret.append(w[2])
mathieui's avatar
mathieui committed
332 333
        if w[1] == len(st):
            return ret
334
        w = sh.get_token()
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
    return ret

def find_argument(pos, text, quoted=True):
    """
    Split an input into a list of arguments, return the number of the
    argument selected by pos.

    If the position searched is outside the string, or in a space between words,
    then it will return the position of an hypothetical new argument.

    See the doctests of the two methods for example behaviors.

    :param int pos: The position to search.
    :param str text: The text to analyze.
    :param quoted: Whether to take quotes into account or not.
    :rtype: int
    """
    if quoted:
        return find_argument_quoted(pos, text)
    else:
        return find_argument_unquoted(pos, text)

def find_argument_quoted(pos, text):
    """
    >>> find_argument_quoted(4, 'toto titi tata')
    3
    >>> find_argument_quoted(4, '"toto titi" tata')
    0
    >>> find_argument_quoted(8, '"toto" "titi tata"')
    1
    >>> find_argument_quoted(8, '"toto" "titi tata')
    1
mathieui's avatar
mathieui committed
367 368
    >>> find_argument_quoted(3, '"toto" "titi tata')
    0
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    >>> find_argument_quoted(18, '"toto" "titi tata" ')
    2
    """
    sh = shlex.shlex(text)
    count = -1
    w = sh.get_token()
    while w and w[2] is not None:
        count += 1
        if w[0] <= pos < w[1]:
            return count
        w = sh.get_token()

    return count + 1

def find_argument_unquoted(pos, text):
    """
    >>> find_argument_unquoted(2, 'toto titi tata')
    0
    >>> find_argument_unquoted(3, 'toto titi tata')
    0
    >>> find_argument_unquoted(6, 'toto titi tata')
    1
    >>> find_argument_unquoted(4, 'toto titi tata')
    3
    >>> find_argument_unquoted(25, 'toto titi tata')
    3
    """
    ret = text.split()
    search = 0
    argnum = 0
    for i, elem in enumerate(ret):
        elem_start = text.find(elem, search)
        elem_end = elem_start + len(elem)
        search = elem_end
        if elem_start <= pos < elem_end:
            return i
        argnum = i
    return argnum + 1
407

408
def parse_str_to_secs(duration=''):
409
    """
410 411 412 413 414 415
    Parse a string of with a number of d, h, m, s.

    :param str duration: The formatted string.
    :return: The number of seconds represented by the string
    :rtype: :py:class:`int`

416 417 418
    >>> parse_str_to_secs("1d3m1h")
    90180
    """
419
    values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
420 421 422 423 424 425 426 427 428 429
    result = 0
    tmp = '0'
    for char in duration:
        if char in string.digits:
            tmp += char
        elif char in values:
            tmp_i = int(tmp)
            result += tmp_i * values[char]
            tmp = '0'
        else:
mathieui's avatar
mathieui committed
430
            return 0
431 432
    if tmp != '0':
        result += int(tmp)
433 434 435
    return result

def parse_secs_to_str(duration=0):
436
    """
437 438
    Do the reverse operation of :py:func:`parse_str_to_secs`.

439 440
    Parse a number of seconds to a human-readable string.
    The string has the form XdXhXmXs. 0 units are removed.
441 442 443 444 445

    :param int duration: The duration, in seconds.
    :return: A formatted string containing the duration.
    :rtype: :py:class:`str`

446
    >>> parse_secs_to_str(3601)
447
    '1h1s'
448
    """
449 450 451 452 453 454 455 456 457 458 459
    secs, mins, hours, days = 0, 0, 0, 0
    result = ''
    secs = duration % 60
    mins = (duration % 3600) // 60
    hours = (duration % 86400) // 3600
    days = duration // 86400

    result += '%sd' % days if days else ''
    result += '%sh' % hours if hours else ''
    result += '%sm' % mins if mins else ''
    result += '%ss' % secs if secs else ''
460 461
    if not result:
        result = '0s'
462 463
    return result

mathieui's avatar
mathieui committed
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
def format_tune_string(infos):
    """
    Contruct a string from a dict created from an "User tune" event.

    :param dict infos: The informations
    :return: The formatted string
    :rtype: :py:class:`str`
    """
    elems = []
    track = infos.get('track')
    if track:
        elems.append(track)
    title = infos.get('title')
    if title:
        elems.append(title)
    else:
        elems.append('Unknown title')
    elems.append('-')
    artist = infos.get('artist')
    if artist:
        elems.append(artist)
    else:
        elems.append('Unknown artist')

    rating = infos.get('rating')
    if rating:
        elems.append('[ ' + rating + '/10' + ' ]')
    length = infos.get('length')
    if length:
        length = int(length)
        secs = length % 60
        mins = length // 60
        secs = str(secs).zfill(2)
        mins = str(mins).zfill(2)
        elems.append('[' + mins + ':' + secs + ']')
    return ' '.join(elems)

mathieui's avatar
mathieui committed
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
def format_gaming_string(infos):
    """
    Construct a string from a dict containing the "user gaming"
    informations.
    (for now, only use address and name)

    :param dict infos: The informations
    :returns: The formatted string
    :rtype: :py:class:`str`
    """
    name = infos.get('name')
    if not name:
        return ''

    server_address = infos.get('server_address')
    if server_address:
        return '%s on %s' % (name, server_address)
    return name

520
def safeJID(*args, **kwargs):
521
    """
522
    Construct a :py:class:`sleekxmpp.JID` object from a string.
523

524 525 526
    Used to avoid tracebacks during is stringprep fails
    (fall back to a JID with an empty string).
    """
527 528 529 530
    try:
        return JID(*args, **kwargs)
    except InvalidJID:
        return JID('')
531 532 533 534 535


if __name__ == "__main__":
    import doctest
    doctest.testmod()