view frontends/src/jp/ @ 2457:49aa1e3a90b8

jp (base): added exitCb generic callback to display a message and quit
author Goffi <>
date Tue, 12 Dec 2017 00:11:27 +0100
parents 8b37a62336c3
children 0046283a285d
line wrap: on
line source

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

# jp: a SAT command line tool
# Copyright (C) 2009-2017 Jérôme Poisson (

# 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
# 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 <>.

from sat.core.i18n import _

### logging ###
import logging as log

import sys
import locale
import os.path
import argparse
from glob import iglob
from importlib import import_module
from import JID
from import config
from import dynamic_import
from sat.core import exceptions
from import Const as C
import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
import shlex
from collections import OrderedDict

## bridge handling
# we get bridge name from conf and initialise the right class accordingly
main_config = config.parseMainConf()
bridge_name = config.getConfig(main_config, '', 'bridge', 'dbus')

# TODO: move loops handling in a separated module
if 'dbus' in bridge_name:
    from gi.repository import GLib

    class JPLoop(object):

        def __init__(self):
            self.loop = GLib.MainLoop()

        def run(self):

        def quit(self):

        def call_later(self, delay, callback, *args):
            """call a callback repeatedly

            @param delay(int): delay between calls in ms
            @param callback(callable): method to call
                if the callback return True, the call will continue
                else the calls will stop
            @param *args: args of the callbac
            GLib.timeout_add(delay, callback, *args)

    print u"can't start jp: only D-Bus bridge is currently handled"
    # FIXME: twisted loop can be used when jp can handle fully async bridges
    # from twisted.internet import reactor

    # class JPLoop(object):

    #     def run(self):

    #     def quit(self):
    #         reactor.stop()

    #     def _timeout_cb(self, args, callback, delay):
    #         ret = callback(*args)
    #         if ret:
    #             reactor.callLater(delay, self._timeout_cb, args, callback, delay)

    #     def call_later(self, delay, callback, *args):
    #         delay = float(delay) / 1000
    #         reactor.callLater(delay, self._timeout_cb, args, callback, delay)

if bridge_name == "embedded":
    from sat.core import sat_main
    sat = sat_main.SAT()

if sys.version_info < (2, 7, 3):
    # XXX: shlex.split only handle unicode since python 2.7.3
    # this is a workaround for older versions
    old_split = shlex.split
    new_split = (lambda s, *a, **kw: [t.decode('utf-8') for t in old_split(s.encode('utf-8'), *a, **kw)]
        if isinstance(s, unicode) else old_split(s, *a, **kw))
    shlex.split = new_split

    import progressbar
except ImportError:
    msg = (_(u'ProgressBar not available, please download it at\n') +
           _(u'Progress bar deactivated\n--\n'))
    print >>sys.stderr,msg.encode('utf-8')

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-2017 Jérôme Poisson, Adrien Cossa
This program comes with ABSOLUTELY NO WARRANTY;
This is free software, and you are welcome to redistribute it under certain conditions.

PROGRESS_DELAY = 10 # the progression will be checked every PROGRESS_DELAY ms

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

        @attribute quit_on_progress_end (bool): set to False if you manage yourself exiting,
            or if you want the user to stop by himself
        @attribute progress_success(callable): method to call when progress just started
            by default display a message
        @attribute progress_success(callable): method to call when progress is successfully finished
            by default display a message
        @attribute progress_failure(callable): method to call when progress failed
            by default display a message
        # FIXME: need_loop should be removed, everything must be async in bridge so
        #        loop will always be needed
        bridge_module = dynamic_import.bridge(bridge_name, 'sat_frontends.bridge')
        if bridge_module is None:
            log.error(u"Can't import {} bridge".format(bridge_name))

        self.bridge = bridge_module.Bridge()
        self.bridge.bridgeConnect(callback=self._bridgeCb, errback=self._bridgeEb)

    def _bridgeCb(self):
        self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
        self.subparsers = self.parser.add_subparsers(title=_(u'Available commands'), dest='subparser_name')
        self._auto_loop = False # when loop is used for internal reasons
        self._need_loop = False

        # progress attributes
        self._progress_id = None # TODO: manage several progress ids
        self.quit_on_progress_end = True

        # outputs
        self._outputs = {}
        for type_ in C.OUTPUT_TYPES:
            self._outputs[type_] = OrderedDict()
        self.default_output = {}

    def _bridgeEb(self, failure):
        if isinstance(failure, exceptions.BridgeExceptionNoService):
            print(_(u"Can't connect to SàT backend, are you sure it's launched ?"))
        elif isinstance(failure, exceptions.BridgeInitError):
            print(_(u"Can't init bridge"))
            print(_(u"Error while initialising bridge: {}".format(failure)))

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

    def progress_id(self):
        return self._progress_id

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

    def watch_progress(self):
        except AttributeError:
            return False
            return True

    def watch_progress(self, watch_progress):
        if watch_progress:
            self.pbar = None

    def verbosity(self):
            return self.args.verbose
        except AttributeError:
            return 0

    def replayCache(self, cache_attribute):
        """Replay cached signals

        @param cache_attribute(str): name of the attribute containing the cache
            if the attribute doesn't exist, there is no cache and the call is ignored
            else the cache must be a list of tuples containing the replay callback as first item,
            then the arguments to use
            cache = getattr(self, cache_attribute)
        except AttributeError:
            for cache_data in cache:

    def disp(self, msg, verbosity=0, error=False, no_lf=False):
        """Print a message to user

        @param msg(unicode): message to print
        @param verbosity(int): minimal verbosity to display the message
        @param error(bool): if True, print to stderr instead of stdout
        @param no_lf(bool): if True, do not emit line feed at the end of line
        if self.verbosity >= verbosity:
            if error:
                if no_lf:
                    print >>sys.stderr,msg.encode('utf-8'),
                    print >>sys.stderr,msg.encode('utf-8')
                if no_lf:
                    print msg.encode('utf-8'),
                    print msg.encode('utf-8')

    def output(self, type_, name, extra_outputs, data):
        if name in extra_outputs:

    def addOnQuitCallback(self, callback, *args, **kwargs):
        """Add a callback which will be called on quit command

        @param callback(callback): method to call
            callbacks_list = self._onQuitCallbacks
        except AttributeError:
            callbacks_list = self._onQuitCallbacks = []
            callbacks_list.append((callback, args, kwargs))

    def getOutputChoices(self, output_type):
        """Return valid output filters for output_type

        @param output_type: True for default,
            else can be any registered type
        return self._outputs[output_type].keys()

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

        # we have a special case here as the start-session option is present only if connection is not needed,
        # so we create two similar parents, one with the option, the other one without it
        for parent_name in ('profile', 'profile_session'):
            parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
            parent.add_argument("-p", "--profile", action="store", type=str, default='@DEFAULT@', help=_("Use PROFILE profile key (default: %(default)s)"))
            parent.add_argument("--pwd", action="store", type=unicode_decoder, default='', metavar='PASSWORD', help=_("Password used to connect profile, if necessary"))

        profile_parent, profile_session_parent = self.parents['profile'], self.parents['profile_session']

        connect_short, connect_long, connect_action, connect_help = "-c", "--connect", "store_true", _(u"Connect the profile before doing anything else")
        profile_parent.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)

        profile_session_connect_group = profile_session_parent.add_mutually_exclusive_group()
        profile_session_connect_group.add_argument(connect_short, connect_long, action=connect_action, help=connect_help)
        profile_session_connect_group.add_argument("--start-session", action="store_true", help=_("Start a profile session without connecting"))

        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"))

        verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
        verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)"))

        for parent_name in ('pubsub', 'pubsub_node_req'):
            parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
            parent.add_argument("-s", "--service", type=unicode_decoder, default=u'',
                                help=_(u"JID of the PubSub service (default: PEP service)"))
            if parent_name == 'pubsub_node_req':
                parent.add_argument("node", type=unicode_decoder, help=_(u"node to request"))
                parent.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=_(u"node to request"))

    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 register_output(self, type_, name, callback, description="", default=False):
        if type_ not in C.OUTPUT_TYPES:
            log.error(u"Invalid output type {}".format(type_))
        self._outputs[type_][name] = {'callback': callback,
                                      'description': description
        if default:
            if type_ in self.default_output:
                self.disp(_(u'there is already a default output for {}, ignoring new one').format(type_))
                self.default_output[type_] = name

    def parse_output_options(self):
        options = self.command.args.output_opts
        options_dict = {}
        for option in options:
                key, value = option.split(u'=', 1)
            except ValueError:
                key, value = option, None
            options_dict[key.strip()] = value.strip() if value is not None else None
        return options_dict

    def check_output_options(self, accepted_set, options):
        if not accepted_set.issuperset(options):
            self.disp(u"The following output options are invalid: {invalid_options}".format(
                invalid_options = u', '.join(set(options).difference(accepted_set))),

    def import_plugins(self):
        """Automaticaly import commands and outputs in jp

        looks from modules names cmd_*.py in jp path and import them
        path = os.path.dirname(
        # XXX: outputs must be imported before commands as they are used for arguments
        for type_, pattern in ((C.PLUGIN_OUTPUT, 'output_*.py'), (C.PLUGIN_CMD, 'cmd_*.py')):
            modules = (os.path.splitext(module)[0] for module in map(os.path.basename, iglob(os.path.join(path, pattern))))
            for module_name in modules:
                module_path = "" + module_name
                    module = import_module(module_path)
                    self.import_plugin_module(module, type_)
                except ImportError as e:
                    self.disp(_(u"Can't import {module_path} plugin, ignoring it: {msg}".format(
                    module_path = module_path,
                    msg = e)), error=True)
                except exceptions.CancelError:
                except exceptions.MissingModule as e:
                    self.disp(_(u"Missing module for plugin {name}: {missing}".format(
                        name = module_path,
                        missing = e)), error=True)

    def import_plugin_module(self, module, type_):
        """add commands or outpus from a module to jp

        @param module: module containing commands or outputs
        @param type_(str): one of C_PLUGIN_*
            class_names =  getattr(module, '__{}__'.format(type_))
        except AttributeError:
            log.disp(_(u"Invalid plugin module [{type}] {module}").format(type=type_, module=module), error=True)
            raise ImportError
            for class_name in class_names:
                cls = getattr(module, class_name)

    def run(self, args=None, namespace=None):
        self.args = self.parser.parse_args(args, namespace=None)
            if self._need_loop or self._auto_loop:
        except KeyboardInterrupt:
  "User interruption: good bye"))

    def _start_loop(self):
        self.loop = JPLoop()

    def stop_loop(self):
        except AttributeError:

    def quitFromSignal(self, errcode=0):
        """Same as self.quit, but from a signal handler

        /!\: return must be used after calling this method !
        assert self._need_loop
        # XXX: python-dbus will show a traceback if we exit in a signal handler
        # so we use this little timeout trick to avoid it
        self.loop.call_later(0, self.quit, errcode)

    def quit(self, errcode=0):
        # first the onQuitCallbacks
            callbacks_list = self._onQuitCallbacks
        except AttributeError:
            for callback, args, kwargs in callbacks_list:
                callback(*args, **kwargs)


    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)
                names2jid[attr["name"].lower()] = jid_s
            except KeyError:

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

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

            for i in range(len(jids)):
        except AttributeError:

        return dest_jids

    def connect_profile(self, callback):
        """ Check if the profile is connected and do it if requested

        @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):
            log.error(_(u"Can't connect profile: {reason}").format(reason=failure))

        def cant_start_session(failure):
            log.error(_(u"Can't start {profile}'s session: {reason}").format(profile=self.profile, reason=failure))

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

        if not self.profile:
            log.error(_("The profile [{profile}] doesn't exist").format(profile=self.args.profile))

            start_session = self.args.start_session
        except AttributeError:
            if start_session:
                self.bridge.profileStartSession(self.args.pwd, self.profile, lambda dummy: callback(), cant_start_session)
                self._auto_loop = True
            elif not self.bridge.profileIsSessionStarted(self.profile):
                if not self.args.connect:
                    log.error(_(u"Session for [{profile}] is not started, please start it before using jp, or use either --start-session or --connect option").format(profile=self.profile))

        if not hasattr(self.args, 'connect'):
            # a profile can be present without connect option (e.g. on profile creation/deletion)
        elif self.args.connect is True:  # if connection is asked, we connect the profile
            self.bridge.connect(self.profile, self.args.pwd, {}, lambda dummy: callback(), cant_connect)
            self._auto_loop = True
            if not self.bridge.isConnected(self.profile):
                log.error(_(u"Profile [{profile}] is not connected, please connect it before using jp, or use --connect option").format(profile=self.profile))


    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 main resource
            main_resource = self.bridge.getMainResource(param_jid, self.profile)
            if main_resource:
                return "%s/%s" % (_jid.bare, main_resource)
        return param_jid

class CommandBase(object):

    def __init__(self, host, name, use_profile=True, use_output=False, extra_outputs=None,
                       need_connect=None, help=None, **kwargs):
        """Initialise CommandBase

        @param host: Jp instance
        @param name(unicode): name of the new command
        @param use_profile(bool): if True, add profile selection/connection commands
        @param use_output(bool, unicode): if not False, add --output option
        @param extra_outputs(dict): list of command specific outputs:
            key is output name ("default" to use as main output)
            value is a callable which will format the output (data will be used as only argument)
            if a key already exists with normal outputs, the extra one will be used
        @param need_connect(bool, None): True if profile connection is needed
            False else (profile session must still be started)
            None to set auto value (i.e. True if use_profile is set)
            Can't be set if use_profile is False
        @param help(unicode): help message to display
        @param **kwargs: args passed to ArgumentParser
            use_* are handled directly, they can be:
            - use_progress(bool): if True, add progress bar activation option
                progress* signals will be handled
            - use_verbose(bool): if True, add verbosity option
        @attribute need_loop(bool): to set by commands when loop is needed
        self.need_loop = False # to be set by commands when loop is needed
        try: # If we have subcommands, host is a CommandBase and we need to use
        except AttributeError:
   = host

        # --profile option
        parents = kwargs.setdefault('parents', set())
        if use_profile:
  ['profile'] is an ArgumentParser with profile connection arguments
            if need_connect is None:
                need_connect = True
            parents.add(['profile' if need_connect else 'profile_session'])
            assert need_connect is None
        self.need_connect = need_connect
        # from this point, self.need_connect is None if connection is not needed at all
        # False if session starting is needed, and True if full connection is needed

        # --output option
        if use_output:
            if extra_outputs is None:
                extra_outputs = {}
            self.extra_outputs = extra_outputs
            if use_output == True:
                use_output = C.OUTPUT_TEXT
            assert use_output in C.OUTPUT_TYPES
            self._output_type = use_output
            output_parent = argparse.ArgumentParser(add_help=False)
            choices = set(
            if not choices:
                raise exceptions.InternalError("No choice found for {} output type".format(use_output))
                default =[use_output]
            except KeyError:
                if u'default' in choices:
                    default = u'default'
                elif u'simple' in choices:
                    default = u'simple'
                    default = list(choices)[0]
            output_parent.add_argument('--output', '-O', choices=sorted(choices), default=default, help=_(u"select output format (default: {})".format(default)))
            output_parent.add_argument('--output-option', '--oo', type=unicode_decoder, action="append", dest='output_opts', default=[], help=_(u"output specific option"))
            assert extra_outputs is None

        if 'use_pubsub' in kwargs and 'use_pubsub_node_req' in kwargs:
            raise exceptions.InternalError(u"use_pubsub and use_pubsub_node_req can't be used at the same time."
                                           u"Use the later one when node is required (else an empty string is used as default)")

        # other common options
        use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
        for param, do_use in use_opts.iteritems():
            opt=param[4:] # if param is use_verbose, opt is verbose
            if opt not in
                raise exceptions.InternalError(u"Unknown parent option {}".format(opt))
            del kwargs[param]
            if do_use:

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

    def args(self):

    def profile(self):

    def progress_id(self):

    def progress_id(self, value): = value

    def progressStartedHandler(self, uid, metadata, profile):
        if profile != self.profile:
        if self.progress_id is None:
            # the progress started message can be received before the id
            # so we keep progressStarted signals in cache to replay they
            # when the progress_id is received
            cache_data = (self.progressStartedHandler, uid, metadata, profile)
            except AttributeError:
       = [cache_data]
            if and uid == self.progress_id:
      , self.progressUpdate)

    def progressFinishedHandler(self, uid, metadata, profile):
        if profile != self.profile:
        if uid == self.progress_id:
            except AttributeError:

    def progressErrorHandler(self, uid, message, profile):
        if profile != self.profile:
        if uid == self.progress_id:
            if self.args.progress:
                self.disp('') # progress is not finished, so we skip a line

    def progressUpdate(self):
        """This method is continualy called to update the progress bar"""
        data =, self.profile)
        if data:
                size = data['size']
            except KeyError:
                self.disp(_(u"file size is not known, we can't show a progress bar"), 1, error=True)
                return False
            if is None:
                #first answer, we must construct the bar
       = progressbar.ProgressBar(max_value=int(size),
                                                         widgets=[_(u"Progress: "),progressbar.Percentage(),
                                                         " ",
                                                         " ",
                                                         " ",


        elif is not None:
            return False


        return True

    def onProgressStarted(self, metadata):
        """Called when progress has just started

        can be overidden by a command
        @param metadata(dict): metadata as sent by bridge.progressStarted
        self.disp(_(u"Operation started"), 2)

    def onProgressUpdate(self, metadata):
        """Method called on each progress updata

        can be overidden by a command to handle progress metadata
        @para metadata(dict): metadata as returned by bridge.progressGet

    def onProgressFinished(self, metadata):
        """Called when progress has just finished

        can be overidden by a command
        @param metadata(dict): metadata as sent by bridge.progressFinished
        self.disp(_(u"Operation successfully finished"), 2)

    def onProgressError(self, error_msg):
        """Called when a progress failed

        @param error_msg(unicode): error message as sent by bridge.progressError
        self.disp(_(u"Error while doing operation: {}").format(error_msg), error=True)

    def disp(self, msg, verbosity=0, error=False, no_lf=False):
        return, verbosity, error, no_lf)

    def output(self, data):
            output_type = self._output_type
        except AttributeError:
            raise exceptions.InternalError(_(u'trying to use output when use_output has not been set'))
        return, self.args.output, self.extra_outputs, data)

    def exitCb(self, msg=None):
        """generic callback for success

        optionally print a message, and quit
        msg(None, unicode): if not None, print this message
        if msg is not None:

    def errback(self, failure_, msg=None, exit_code=C.EXIT_ERROR):
        """generic callback for errbacks

        display failure_ then quit with generic error
        @param failure_: arguments returned by errback
        @param msg(unicode, None): message template
            use {} if you want to display failure message
        @param exit_code(int): shell exit code
        if msg is None:
            msg = _(u"error: {}")
        self.disp(msg.format(failure_), error=True)

    def add_parser_options(self):
            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:

    def run(self):
        """this method is called when a command is actually run

        It set stuff like progression callbacks and profile connection
        You should not overide this method: you should call self.start instead
        # we keep a reference to run command, it may be useful e.g. for outputs = self
        # host._need_loop is set here from our current value and not before
        # as the need_loop decision must be taken only by then running command = self.need_loop

            show_progress = self.args.progress
        except AttributeError:
            # the command doesn't use progress bar
            if show_progress:
       = True
            # we need to register the following signal even if we don't display the progress bar
  "progressStarted", self.progressStartedHandler)
  "progressFinished", self.progressFinishedHandler)
  "progressError", self.progressErrorHandler)

        if self.need_connect is not None:

    def connected(self):
        """this method is called when profile is connected (or session is started)

        this method is only called when use_profile is True
        most of time you should override self.start instead of this method, but if loop
        if not always needed depending on your arguments, you may override this method,
        but don't forget to call the parent one (i.e. this one) after self.need_loop is set
        if not self.need_loop:

    def start(self):
        """This is the starting point of the command, this method should be overriden

        at this point, profile are connected if needed

class CommandAnswering(CommandBase):
    """Specialised commands which answer to specific actions

    to manage action_types answer,
    action_callbacks = {} # XXX: set managed action types in a dict here:
                          # key is the action_type, value is the callable
                          # which will manage the answer. profile filtering is
                          # already managed when callback is called

    def onActionNew(self, action_data, action_id, security_limit, profile):
        if profile != self.profile:
            action_type = action_data['meta_type']
        except KeyError:
                xml_ui = action_data["xmlui"]
            except KeyError:
                callback = self.action_callbacks[action_type]
            except KeyError:
                callback(action_data, action_id, security_limit, profile)

    def onXMLUI(self, xml_ui):
        """Display a dialog received from the backend.

        @param xml_ui (unicode): dialog XML representation
        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
        #        should be available in the future
        # TODO: XMLUI module
        ui = ET.fromstring(xml_ui.encode('utf-8'))
        dialog = ui.find("dialog")
        if dialog is not None:
            self.disp(dialog.findtext("message"), error=dialog.get("level") == "error")

    def connected(self):
        """Auto reply to confirmations requests"""
        self.need_loop = True
        super(CommandAnswering, self).connected()"actionNew", self.onActionNew)
        actions =
        for action_data, action_id, security_limit in actions:
            self.onActionNew(action_data, action_id, security_limit, self.profile)