common.py 13.6 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 datetime import datetime, timedelta
13
from sleekxmpp import JID, InvalidJID
14 15 16 17
import base64
import os
import mimetypes
import hashlib
18
import subprocess
19
import time
20
import string
21
import shlex
22

23 24 25 26 27 28
ROOM_STATE_NONE = 11
ROOM_STATE_CURRENT = 10
ROOM_STATE_PRIVATE = 15
ROOM_STATE_MESSAGE = 12
ROOM_STATE_HL = 13

29
def get_base64_from_file(path):
30 31
    """
    Convert the content of a file to base64
32 33 34 35 36 37 38

    :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`

39
    """
40 41 42 43 44
    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")
45 46
    fdes = open(path, 'rb')
    data = fdes.read()
47 48 49 50
    encoded = base64.encodestring(data)
    sha1 = hashlib.sha1(data).hexdigest()
    mime_type = mimetypes.guess_type(path)[0]
    return (encoded, mime_type, sha1)
51 52

def get_output_of_command(command):
53
    """
54 55 56 57 58
    Runs a command and returns its output.

    :param str command: The command to run.
    :return: The output or None
    :rtype: :py:class:`str`
59
    """
60
    try:
61 62
        return subprocess.check_output(command.split()).decode('utf-8').split('\n')
    except subprocess.CalledProcessError:
63 64 65 66
        return None

def is_in_path(command, return_abs_path=False):
    """
67 68 69 70 71 72 73 74
    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.

75 76 77 78 79 80 81 82 83 84 85 86 87
    """
    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

88
DISTRO_INFO = {
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
        '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'
}
109

110
def get_os_info():
111 112 113
    """
    Returns a detailed and well formated string containing
    informations about the operating system
114 115

    :rtype: str
116 117
    """
    if os.name == 'posix':
118 119 120 121 122
        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
123 124 125 126 127
            process = subprocess.Popen([command], shell=True,
                                       stdin=subprocess.PIPE,
                                       stdout=subprocess.PIPE,
                                       close_fds=True)
            process.wait()
128
            output = process.stdout.readline().decode('utf-8').strip()
129 130 131 132 133
            # 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
134 135
        for distro_name in DISTRO_INFO:
            path_to_file = DISTRO_INFO[distro_name]
136 137 138 139 140 141
            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:
142
                    fdes = open(path_to_file, encoding='utf-8')
143 144
                    text = fdes.readline().strip() # get only first line
                    fdes.close()
145 146 147 148 149 150 151 152 153 154 155
                    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
156 157
                    elif path_to_file.endswith('lfs-release'):
                        # file just has version
158 159 160 161 162 163 164 165 166 167 168
                        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
169 170 171

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

174
    Because various datetime formats are used, the following exceptions
175
    are handled:
176 177 178 179 180 181 182 183 184

    * 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`

185
    >>> datetime_tuple('20130226T06:23:12')
186
    datetime.datetime(2013, 2, 26, 8, 23, 12)
187 188 189
    """
    timestamp = timestamp.split('.')[0]
    timestamp = timestamp.replace('-', '')
190 191 192 193
    try:
        ret = datetime.strptime(timestamp, '%Y%m%dT%H:%M:%SZ')
    except ValueError:      # Support the deprecated format, XEP 0091 :(
        ret = datetime.strptime(timestamp, '%Y%m%dT%H:%M:%S')
194 195 196 197
    # convert UTC to local time, with DST etc.
    dst = timedelta(seconds=time.altzone)
    ret -= dst
    return ret
198

mathieui's avatar
mathieui committed
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
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
218
            date = datetime_tuple(delay_tag.attrib['stamp'])
mathieui's avatar
mathieui committed
219 220 221 222 223
        else:
            delayed = False
            date = None
    return (delayed, date)

louiz’'s avatar
louiz’ committed
224
def shell_split(st):
225 226 227 228 229 230 231 232 233 234
    """
    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']
235 236 237 238 239 240
    >>> shell_split('toto "titi"')
    ['toto', 'titi']
    >>> shell_split('toto ""')
    ['toto', '']
    >>> shell_split('"toto titi" toto ""')
    ['toto titi', 'toto', '']
241
    """
242 243 244 245 246
    sh = shlex.shlex(st)
    ret = []
    w = sh.get_token()
    while w and w[2] is not None:
        ret.append(w[2])
247
        w = sh.get_token()
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
    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
    >>> 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
318

319
def parse_str_to_secs(duration=''):
320
    """
321 322 323 324 325 326
    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`

327 328 329
    >>> parse_str_to_secs("1d3m1h")
    90180
    """
330
    values = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
331 332 333 334 335 336 337 338 339 340
    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
341
            return 0
342 343
    if tmp != '0':
        result += int(tmp)
344 345 346
    return result

def parse_secs_to_str(duration=0):
347
    """
348 349
    Do the reverse operation of :py:func:`parse_str_to_secs`.

350 351
    Parse a number of seconds to a human-readable string.
    The string has the form XdXhXmXs. 0 units are removed.
352 353 354 355 356

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

357
    >>> parse_secs_to_str(3601)
358
    '1h1s'
359
    """
360 361 362 363 364 365 366 367 368 369 370
    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 ''
371 372
    if not result:
        result = '0s'
373 374
    return result

mathieui's avatar
mathieui committed
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 407 408 409 410 411
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
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
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

431
def safeJID(*args, **kwargs):
432
    """
433
    Construct a :py:class:`sleekxmpp.JID` object from a string.
434

435 436 437
    Used to avoid tracebacks during is stringprep fails
    (fall back to a JID with an empty string).
    """
438 439 440 441
    try:
        return JID(*args, **kwargs)
    except InvalidJID:
        return JID('')
442 443 444 445 446


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