Mercurial > libervia-backend
view frontends/src/jp/base.py @ 1265:e3a9ea76de35 frontends_multi_profiles
quick_frontend, primitivus: multi-profiles refactoring part 1 (big commit, sorry :p):
This refactoring allow primitivus to manage correctly several profiles at once, with various other improvments:
- profile_manager can now plug several profiles at once, requesting password when needed. No more profile plug specific method is used anymore in backend, instead a "validated" key is used in actions
- Primitivus widget are now based on a common "PrimitivusWidget" classe which mainly manage the decoration so far
- all widgets are treated in the same way (contactList, Chat, Progress, etc), no more chat_wins specific behaviour
- widgets are created in a dedicated manager, with facilities to react on new widget creation or other events
- quick_frontend introduce a new QuickWidget class, which aims to be as generic and flexible as possible. It can manage several targets (jids or something else), and several profiles
- each widget class return a Hash according to its target. For example if given a target jid and a profile, a widget class return a hash like (target.bare, profile), the same widget will be used for all resources of the same jid
- better management of CHAT_GROUP mode for Chat widgets
- some code moved from Primitivus to QuickFrontend, the final goal is to have most non backend code in QuickFrontend, and just graphic code in subclasses
- no more (un)escapePrivate/PRIVATE_PREFIX
- contactList improved a lot: entities not in roster and special entities (private MUC conversations) are better managed
- resources can be displayed in Primitivus, and their status messages
- profiles are managed in QuickFrontend with dedicated managers
This is work in progress, other frontends are broken. Urwid SàText need to be updated. Most of features of Primitivus should work as before (or in a better way ;))
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 10 Dec 2014 19:00:09 +0100 |
parents | 75025461141f |
children | faa1129559b8 |
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 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 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("-c", "--connect", action="store", type=str, nargs='?', const='', default=None, metavar='PASSWORD', 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, 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 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 self.args.connect is not None: # if connection is asked, we connect the profile self.bridge.asyncConnect(self.profile, self.args.connect, 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 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)