view frontends/src/jp/base.py @ 853:c2f6ada7858f

core (sqlite): automatic database update: - new Updater class check database consistency (by calculating a hash on the .schema), and updates base if necessary - database now has a version (1 for current, 0 will be for 0.3's database), for each change this version will be increased - creation statements and update statements are in the form of dict of dict with tuples. There is a help text at the top of the module to explain how it works - if we are on a development version, the updater try to update the database automaticaly (without deleting table or columns). The Updater.generateUpdateData method can be used to ease the creation of update data (i.e. the dictionary at the top, see the one for the key 1 for an example). - if there is an inconsistency, an exception is raised, and a message indicate the SQL statements that should fix the situation. - well... this is rather complicated, a KISS method would maybe have been better. The future will say if we need to simplify it :-/ - new DatabaseError exception
author Goffi <goffi@goffi.org>
date Sun, 23 Feb 2014 23:30:32 +0100
parents 300b4de701a6
children 241f6baa6687
line wrap: on
line source

#! /usr/bin/python
# -*- coding: utf-8 -*-

# jp: a SAT command line tool
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from sat.core.i18n import _

global pbar_available
pbar_available = True #checked before using ProgressBar

### logging ###
import logging
from logging import debug, info, error, warning
logging.basicConfig(level=logging.DEBUG,
                    format='%(message)s')
###

import sys
import locale
import os.path
import argparse
import gobject
from glob import iglob
from importlib import import_module
from sat.tools.jid import JID
from sat_frontends.bridge.DBus import DBusBridgeFrontend
from sat.core import exceptions
import sat_frontends.jp
try:
    import progressbar
except ImportError:
    info (_('ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbar'))
    info (_('Progress bar deactivated\n--\n'))
    progressbar=None

#consts
prog_name = u"jp"
description = """This software is a command line tool for XMPP.
Get the latest version at http://sat.goffi.org"""

copyleft = u"""Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (aka Goffi)
This program comes with ABSOLUTELY NO WARRANTY;
This is free software, and you are welcome to redistribute it under certain conditions.
"""


def unicode_decoder(arg):
    # Needed to have unicode strings from arguments
    return arg.decode(locale.getpreferredencoding())


class Jp(object):
    """
    This class can be use to establish a connection with the
    bridge. Moreover, it should manage a main loop.

    To use it, you mainly have to redefine the method run to perform
    specify what kind of operation you want to perform.

    """
    def __init__(self):
        try:
            self.bridge = DBusBridgeFrontend()
        except exceptions.BridgeExceptionNoService:
            print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
            sys.exit(1)
        except excpetions.BridgeInitError:
            print(_(u"Can't init bridge"))
            sys.exit(1)

        self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
                                              description=description)

        self._make_parents()
        self.add_parser_options()
        self.subparsers = self.parser.add_subparsers(title=_('Available commands'), dest='subparser_name')
        self._auto_loop = False # when loop is used for internal reasons
        self.need_loop = False # to set by commands when loop is needed
        self._progress_id = None # TODO: manage several progress ids
        self.quit_on_progress_end = True # set to False if you manage yourself exiting, or if you want the user to stop by himself

    @property
    def version(self):
        return self.bridge.getVersion()

    @property
    def progress_id(self):
        return self._progress_id

    @progress_id.setter
    def progress_id(self, value):
        self._progress_id = value

    def _make_parents(self):
        self.parents = {}

        profile_parent = self.parents['profile'] = argparse.ArgumentParser(add_help=False)
        profile_parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
        profile_parent.add_argument("-c", "--connect", action="store_true", help=_("Connect the profile before doing anything else"))

        progress_parent = self.parents['progress'] = argparse.ArgumentParser(add_help=False)
        if progressbar:
            progress_parent.add_argument("-P", "--progress", action="store_true", help=_("Show progress bar"))

    def add_parser_options(self):
        self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': prog_name, 'version': self.version, 'copyleft': copyleft}))

    def import_commands(self):
        """ Automaticaly import commands to jp
        looks from modules names cmd_*.py in jp path and import them

        """
        path = os.path.dirname(sat_frontends.jp.__file__)
        modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, "cmd_*.py"))))
        for module_name in modules:
            module = import_module("sat_frontends.jp."+module_name)
            try:
                self.import_command_module(module)
            except ImportError:
                continue

    def import_command_module(self, module):
        """ Add commands from a module to jp
        @param module: module containing commands

        """
        try:
            for classname in module.__commands__:
                cls = getattr(module, classname)
        except AttributeError:
            warning(_("Invalid module %s") % module)
            raise ImportError
        cls(self)


    def run(self, args=None):
        self.args = self.parser.parse_args(args)
        self.args.func()
        if self.need_loop or self._auto_loop:
            self._start_loop()

    def _start_loop(self):
        self.loop = gobject.MainLoop()
        try:
            self.loop.run()
        except KeyboardInterrupt:
            info(_("User interruption: good bye"))

    def stop_loop(self):
        try:
            self.loop.quit()
        except AttributeError:
            pass

    def quit(self, errcode=0):
        self.stop_loop()
        if errcode:
            sys.exit(errcode)

    def check_jids(self, jids):
        """Check jids validity, transform roster name to corresponding jids

        @param profile: profile name
        @param jids: list of jids
        @return: List of jids

        """
        names2jid = {}
        nodes2jid = {}

        for contact in self.bridge.getContacts(self.profile):
            _jid, attr, groups = contact
            if attr.has_key("name"):
                names2jid[attr["name"].lower()] = _jid
            nodes2jid[JID(_jid).node.lower()] = _jid

        def expand_jid(jid):
            _jid = jid.lower()
            if _jid in names2jid:
                expanded = names2jid[_jid]
            elif _jid in nodes2jid:
                expanded = nodes2jid[_jid]
            else:
                expanded = jid
            return unicode(expanded)

        def check(jid):
            if not jid.is_valid:
                error (_("%s is not a valid JID !"), jid)
                self.quit(1)

        dest_jids=[]
        try:
            for i in range(len(jids)):
                dest_jids.append(expand_jid(jids[i]))
                check(dest_jids[i])
        except AttributeError:
            pass

        return dest_jids

    def connect_profile(self, callback):
        """ Check if the profile is connected
        @param callback: method to call when profile is connected
        @exit: - 1 when profile is not connected and --connect is not set
               - 1 when the profile doesn't exists
               - 1 when there is a connection error
        """
        # FIXME: need better exit codes

        def cant_connect():
            error(_(u"Can't connect profile"))
            self.quit(1)

        self.profile = self.bridge.getProfileName(self.args.profile)

        if not self.profile:
            error(_("The profile [%s] doesn't exist") % self.args.profile)
            self.quit(1)

        if self.args.connect: #if connection is asked, we connect the profile
            self.bridge.asyncConnect(self.profile, callback, cant_connect)
            self._auto_loop = True
            return

        elif not self.bridge.isConnected(self.profile):
            error(_(u"Profile [%(profile)s] is not connected, please connect it before using jp, or use --connect option") % { "profile": self.profile })
            self.quit(1)

        callback()

    def get_full_jid(self, param_jid):
        """Return the full jid if possible (add last resource when find a bare jid)"""
        _jid = JID(param_jid)
        if not _jid.resource:
            #if the resource is not given, we try to add the last known resource
            last_resource = self.bridge.getLastResource(param_jid, self.profile)
            if last_resource:
                return "%s/%s" % (_jid.bare, last_resource)
        return param_jid

    def watch_progress(self):
        self.pbar = None
        gobject.timeout_add(10, self._progress_cb)

    def _progress_cb(self):
        if self.progress_id:
            data = self.bridge.getProgress(self.progress_id, self.profile)
            if data:
                if not data['position']:
                    data['position'] = '0'
                if not self.pbar:
                    #first answer, we must construct the bar
                    self.pbar = progressbar.ProgressBar(int(data['size']),
                                                        [_("Progress: "),progressbar.Percentage(),
                                                         " ",
                                                         progressbar.Bar(),
                                                         " ",
                                                         progressbar.FileTransferSpeed(),
                                                         " ",
                                                         progressbar.ETA()])
                    self.pbar.start()

                self.pbar.update(int(data['position']))

            elif self.pbar:
                self.pbar.finish()
                if self.quit_on_progress_end:
                    self.quit()
                return False

        return True


class CommandBase(object):

    def __init__(self, host, name, use_profile=True, use_progress=False, help=None, **kwargs):
        """ Initialise CommandBase
        @param host: Jp instance
        @param name: name of the new command
        @param use_profile: if True, add profile selection/connection commands
        @param use_progress: if True, add progress bar activation commands
        @param help: help message to display
        @param **kwargs: args passed to ArgumentParser

        """
        try: # If we have subcommands, host is a CommandBase and we need to use host.host
            self.host = host.host
        except AttributeError:
            self.host = host

        parents = kwargs.setdefault('parents', set())
        if use_profile:
            #self.host.parents['profile'] is an ArgumentParser with profile connection arguments
            parents.add(self.host.parents['profile'])
        if use_progress:
            parents.add(self.host.parents['progress'])

        self.parser = host.subparsers.add_parser(name, help=help, **kwargs)
        if hasattr(self, "subcommands"):
            self.subparsers = self.parser.add_subparsers()
        else:
            self.parser.set_defaults(func=self.run)
        self.add_parser_options()

    @property
    def args(self):
        return self.host.args

    @property
    def need_loop(self):
        return self.host.need_loop

    @need_loop.setter
    def need_loop(self, value):
        self.host.need_loop = value

    @property
    def profile(self):
        return self.host.profile

    @property
    def progress_id(self):
        return self.host.progress_id

    @progress_id.setter
    def progress_id(self, value):
        self.host.progress_id = value

    def add_parser_options(self):
        try:
            subcommands = self.subcommands
        except AttributeError:
            # We don't have subcommands, the class need to implements add_parser_options
            raise NotImplementedError

        # now we add subcommands to ourself
        for cls in subcommands:
            cls(self)

    def run(self):
        try:
            if self.args.profile:
                self.host.connect_profile(self.connected)
        except AttributeError:
            # the command doesn't need to connect profile
            pass
        try:
            if self.args.progress:
                self.host.watch_progress()
        except AttributeError:
            # the command doesn't use progress bar
            pass

    def connected(self):
        if not self.need_loop:
            self.host.stop_loop()


class CommandAnswering(CommandBase):
    #FIXME: temp, will be refactored when progress_bar/confirmations will be refactored

    def _ask_confirmation(self, confirm_id, confirm_type, data, profile):
        """ Callback used for file transfer, accept files depending on parameters"""
        if profile != self.profile:
            debug("Ask confirmation ignored: not our profile")
            return
        if confirm_type == self.confirm_type:
            if self.dest_jids and not JID(data['from']).bare in [JID(_jid).bare for _jid in self.dest_jids()]:
                return #file is not sent by a filtered jid
            else:
                self.ask(data, confirm_id)

    def ask(self):
        """
        The return value is used to answer to the bridge.
        @return: bool or dict
        """
        raise NotImplementedError

    def connected(self):
        """Auto reply to confirmations requests"""
        self.need_loop = True
        super(CommandAnswering, self).connected()
        # we watch confirmation signals
        self.host.bridge.register("ask_confirmation", self._ask_confirmation)

        #and we ask those we have missed
        for confirm_id, confirm_type, data in self.host.bridge.getWaitingConf(self.profile):
            self._ask_confirmation(confirm_id, confirm_type, data, self.profile)