view frontends/src/jp/base.py @ 1554:e281ed2c21db

plugin NAT port: added UPnP IGD mapping + automatic unmapping on backend shut down
author Goffi <goffi@goffi.org>
date Mon, 02 Nov 2015 22:02:41 +0100
parents 3d5193b4c582
children 823a385235ef
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, 2015 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
from gi.repository import GLib, GObject
from glob import iglob
from importlib import import_module
from sat_frontends.tools.jid import JID
from sat_frontends.bridge.DBus import DBusBridgeFrontend
from sat.core import exceptions
import sat_frontends.jp
from sat_frontends.jp.constants import Const as C
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 """ + C.APP_URL

copyleft = u"""Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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 exceptions.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("--pwd", action="store", type=unicode, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary"))
        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 = GLib.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_s, attr, groups = contact
            _jid = JID(jid_s)
            try:
                names2jid[attr["name"].lower()] = jid_s
            except KeyError:
                pass

            if _jid.node:
                nodes2jid[_jid.node.lower()] = jid_s

        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 expanded.decode('utf-8')

        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(failure):
            error(_(u"Can't connect profile [%s]") % failure)
            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 not hasattr(self.args, 'connect'):
            # a profile can be presente without connect option (e.g. on profile creation/deletion)
            return
        elif self.args.connect is not None:  # if connection is asked, we connect the profile
            self.bridge.asyncConnect(self.profile, self.args.pwd, lambda dummy: 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 main 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.getMainResource(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:
                connect_profile = self.host.connect_profile
        except AttributeError:
            # the command doesn't need to connect profile
            pass
        else:
            connect_profile(self.connected)

        try:
            if self.args.progress:
                watch_progress = self.host.watch_progress
        except AttributeError:
            # the command doesn't use progress bar
            pass
        else:
            watch_progress()

    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)