view frontends/src/jp/cmd_pubsub.py @ 2532:772447ec070f

jp: pubsub options refactoring: There is now only "use_pubsub", and specification are set using "pubsub_flags" argument when instantiating CommandBase. Options are more Python Zen compliant by using explicit arguments for item, draft, url instead of trying to guess with magic keyword and type detection. Pubsub node and item are now always using respecively "-n" and "-i" even when required, this way shell history can be used to change command more easily, and it's globally less confusing for user. if --pubsub-url is used, elements can be overwritten with individual option (e.g. change item id with --item). New "use_draft" argument in CommandBase, to re-use current draft or open a file path as draft. Item can now be specified when using a draft. If it already exists, its content will be added to current draft (with a separator), to avoid loosing data. common.BaseEdit.getItemPath could be simplified thanks to those changes. Pubsub URI handling has been moved to base.py.
author Goffi <goffi@goffi.org>
date Wed, 21 Mar 2018 19:13:22 +0100
parents 0046283a285d
children dcc77f23e370
line wrap: on
line source

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

# jp: a SàT command line tool
# Copyright (C) 2009-2018 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/>.


import base
from sat.core.i18n import _
from sat.core import exceptions
from sat_frontends.jp.constants import Const as C
from sat_frontends.jp import common
from sat_frontends.jp import arg_tools
from functools import partial
from sat.tools.common import uri
from sat.tools.common.ansi import ANSI as A
from sat_frontends.tools import jid, strings
import argparse
import os.path
import re
import subprocess
import sys

__commands__ = ["Pubsub"]

PUBSUB_TMP_DIR = u"pubsub"
PUBSUB_SCHEMA_TMP_DIR = PUBSUB_TMP_DIR + "_schema"
ALLOWED_SUBSCRIPTIONS_OWNER = ('subscribed', 'pending', 'none')

# TODO: need to split this class in several modules, plugin should handle subcommands


class NodeInfo(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'info', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node configuration'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys',
                                 help=_(u"data key to filter"))

    def removePrefix(self, key):
        return key[7:] if key.startswith(u"pubsub#") else key

    def filterKey(self, key):
        return any((key == k or key == u'pubsub#' + k) for k in self.args.keys)

    def psNodeConfigurationGetCb(self, config_dict):
        key_filter = (lambda k: True) if not self.args.keys else self.filterKey
        config_dict = {self.removePrefix(k):v for k,v in config_dict.iteritems() if key_filter(k)}
        self.output(config_dict)
        self.host.quit()

    def psNodeConfigurationGetEb(self, failure_):
        self.disp(u"can't get node configuration: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psNodeConfigurationGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psNodeConfigurationGetCb,
            errback=self.psNodeConfigurationGetEb)


class NodeCreate(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'create', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'create a node'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
                                 default=[], metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set"))
        self.parser.add_argument("-F", "--full-prefix", action="store_true", help=_(u"don't prepend \"pubsub#\" prefix to field names"))

    def psNodeCreateCb(self, node_id):
        if self.host.verbosity:
            announce = _(u'node created successfully: ')
        else:
            announce = u''
        self.disp(announce + node_id)
        self.host.quit()

    def psNodeCreateEb(self, failure_):
        self.disp(u"can't create: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        if not self.args.full_prefix:
            options = {u'pubsub#' + k: v for k,v in self.args.fields}
        else:
            options = dict(self.args.fields)
        self.host.bridge.psNodeCreate(
            self.args.service,
            self.args.node,
            options,
            self.profile,
            callback=self.psNodeCreateCb,
            errback=partial(self.errback,
                            msg=_(u"can't create node: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class NodeDelete(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a node'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument('-f', '--force', action='store_true', help=_(u'delete node without confirmation'))

    def psNodeDeleteCb(self):
        self.disp(_(u'node [{node}] deleted successfully').format(node=self.args.node))
        self.host.quit()

    def start(self):
        if not self.args.force:
            if not self.args.service:
                message = _(u"Are you sure to delete pep node [{node_id}] ?").format(
                    node_id=self.args.node)
            else:
                message = _(u"Are you sure to delete node [{node_id}] on service [{service}] ?").format(
                    node_id=self.args.node, service=self.args.service)

            res = raw_input("{} (y/N)? ".format(message))
            if res not in ("y", "Y"):
                self.disp(_(u"node deletion cancelled"))
                self.host.quit(2)

        self.host.bridge.psNodeDelete(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psNodeDeleteCb,
            errback=partial(self.errback,
                            msg=_(u"can't delete node: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class NodeSet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'set', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set node configuration'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
                                 required=True, metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set (required)"))

    def psNodeConfigurationSetCb(self):
        self.disp(_(u'node configuration successful'), 1)
        self.host.quit()

    def psNodeConfigurationSetEb(self, failure_):
        self.disp(u"can't set node configuration: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def getKeyName(self, k):
        if not k.startswith(u'pubsub#'):
            return u'pubsub#' + k
        else:
            return k

    def start(self):
        self.host.bridge.psNodeConfigurationSet(
            self.args.service,
            self.args.node,
            {self.getKeyName(k): v for k,v in self.args.fields},
            self.profile,
            callback=self.psNodeConfigurationSetCb,
            errback=self.psNodeConfigurationSetEb)


class NodeAffiliationsGet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node affiliations (for node owner)'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psNodeAffiliationsGetCb(self, affiliations):
        self.output(affiliations)
        self.host.quit()

    def psNodeAffiliationsGetEb(self, failure_):
        self.disp(u"can't get node affiliations: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psNodeAffiliationsGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psNodeAffiliationsGetCb,
            errback=self.psNodeAffiliationsGetEb)


class NodeAffiliationsSet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set affiliations (for node owner)'))
        self.need_loop=True

    def add_parser_options(self):
        # XXX: we use optional argument syntax for a required one because list of list of 2 elements
        #      (uses to construct dicts) don't work with positional arguments
        self.parser.add_argument("-a",
                                 "--affiliation",
                                 dest="affiliations",
                                 metavar=('JID', 'AFFILIATION'),
                                 required=True,
                                 type=base.unicode_decoder,
                                 action="append",
                                 nargs=2,
                                 help=_(u"entity/affiliation couple(s)"))

    def psNodeAffiliationsSetCb(self):
        self.disp(_(u"affiliations have been set"), 1)
        self.host.quit()

    def psNodeAffiliationsSetEb(self, failure_):
        self.disp(u"can't set node affiliations: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        affiliations = dict(self.args.affiliations)
        self.host.bridge.psNodeAffiliationsSet(
            self.args.service,
            self.args.node,
            affiliations,
            self.profile,
            callback=self.psNodeAffiliationsSetCb,
            errback=self.psNodeAffiliationsSetEb)


class NodeAffiliations(base.CommandBase):
    subcommands = (NodeAffiliationsGet, NodeAffiliationsSet)

    def __init__(self, host):
        super(NodeAffiliations, self).__init__(host, 'affiliations', use_profile=False, help=_(u'set or retrieve node affiliations'))


class NodeSubscriptionsGet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'retrieve node subscriptions (for node owner)'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psNodeSubscriptionsGetCb(self, subscriptions):
        self.output(subscriptions)
        self.host.quit()

    def psNodeSubscriptionsGetEb(self, failure_):
        self.disp(u"can't get node subscriptions: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psNodeSubscriptionsGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psNodeSubscriptionsGetCb,
            errback=self.psNodeSubscriptionsGetEb)


class StoreSubscriptionAction(argparse.Action):
    """Action which handle subscription parameter for owner

    list is given by pairs: jid and subscription state
    if subscription state is not specified, it default to "subscribed"
    """

    def __call__(self, parser, namespace, values, option_string):
        dest_dict = getattr(namespace, self.dest)
        while values:
            jid_s = values.pop(0)
            try:
                subscription = values.pop(0)
            except IndexError:
                subscription = 'subscribed'
            if subscription not in ALLOWED_SUBSCRIPTIONS_OWNER:
                parser.error(_(u"subscription must be one of {}").format(u', '.join(ALLOWED_SUBSCRIPTIONS_OWNER)))
            dest_dict[jid_s] = subscription


class NodeSubscriptionsSet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/modify subscriptions (for node owner)'))
        self.need_loop=True

    def add_parser_options(self):
        # XXX: we use optional argument syntax for a required one because list of list of 2 elements
        #      (uses to construct dicts) don't work with positional arguments
        self.parser.add_argument("-S",
                                 "--subscription",
                                 dest="subscriptions",
                                 default={},
                                 nargs='+',
                                 metavar=('JID [SUSBSCRIPTION]'),
                                 required=True,
                                 type=base.unicode_decoder,
                                 action=StoreSubscriptionAction,
                                 help=_(u"entity/subscription couple(s)"))

    def psNodeSubscriptionsSetCb(self):
        self.disp(_(u"subscriptions have been set"), 1)
        self.host.quit()

    def psNodeSubscriptionsSetEb(self, failure_):
        self.disp(u"can't set node subscriptions: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psNodeSubscriptionsSet(
            self.args.service,
            self.args.node,
            self.args.subscriptions,
            self.profile,
            callback=self.psNodeSubscriptionsSetCb,
            errback=self.psNodeSubscriptionsSetEb)


class NodeSubscriptions(base.CommandBase):
    subcommands = (NodeSubscriptionsGet, NodeSubscriptionsSet)

    def __init__(self, host):
        super(NodeSubscriptions, self).__init__(host, 'subscriptions', use_profile=False, help=_(u'get or modify node subscriptions'))


class NodeSchemaSet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'set/replace a schema'))
        self.need_loop = True

    def add_parser_options(self):
        self.parser.add_argument('schema', help=_(u"schema to set (must be XML)"))

    def psSchemaSetCb(self):
        self.disp(_(u'schema has been set'), 1)
        self.host.quit()

    def start(self):
        self.host.bridge.psSchemaSet(
            self.args.service,
            self.args.node,
            self.args.schema,
            self.profile,
            callback=self.psSchemaSetCb,
            errback=partial(self.errback,
                            msg=_(u"can't set schema: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class NodeSchemaEdit(base.CommandBase, common.BaseEdit):
    use_items=False

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.NODE}, use_draft=True, use_verbose=True, help=_(u'edit a schema'))
        common.BaseEdit.__init__(self, self.host, PUBSUB_SCHEMA_TMP_DIR)
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psSchemaSetCb(self):
        self.disp(_(u'schema has been set'), 1)
        self.host.quit()

    def publish(self, schema):
        self.host.bridge.psSchemaSet(
            self.args.service,
            self.args.node,
            schema,
            self.profile,
            callback=self.psSchemaSetCb,
            errback=partial(self.errback,
                            msg=_(u"can't set schema: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))

    def psSchemaGetCb(self, schema):
        try:
            from lxml import etree
        except ImportError:
            self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
            self.host.quit(1)
        content_file_obj, content_file_path = self.getTmpFile()
        schema = schema.strip()
        if schema:
            parser = etree.XMLParser(remove_blank_text=True)
            schema_elt = etree.fromstring(schema, parser)
            content_file_obj.write(etree.tostring(schema_elt, encoding="utf-8", pretty_print=True))
            content_file_obj.seek(0)
        self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj)

    def start(self):
        self.host.bridge.psSchemaGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psSchemaGetCb,
            errback=partial(self.errback,
                            msg=_(u"can't edit schema: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class NodeSchemaGet(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_XML, use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'get schema'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psSchemaGetCb(self, schema):
        if not schema:
            self.disp(_(u'no schema found'), 1)
            self.host.quit(1)
        self.output(schema)
        self.host.quit()

    def start(self):
        self.host.bridge.psSchemaGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psSchemaGetCb,
            errback=partial(self.errback,
                            msg=_(u"can't get schema: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class NodeSchema(base.CommandBase):
    subcommands = (NodeSchemaSet, NodeSchemaEdit, NodeSchemaGet)

    def __init__(self, host):
        super(NodeSchema, self).__init__(host, 'schema', use_profile=False, help=_(u"data schema manipulation"))


class Node(base.CommandBase):
    subcommands = (NodeInfo, NodeCreate, NodeDelete, NodeSet, NodeAffiliations, NodeSubscriptions, NodeSchema)

    def __init__(self, host):
        super(Node, self).__init__(host, 'node', use_profile=False, help=_('node handling'))


class Set(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'publish a new item or update an existing one'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'', help=_(u"id, URL of the item to update, keyword, or nothing for new item"))

    def psItemsSendCb(self, published_id):
        if published_id:
            self.disp(u"Item published at {pub_id}".format(pub_id=published_id))
        else:
            self.disp(u"Item published")
        self.host.quit(C.EXIT_OK)

    def start(self):
        try:
            from lxml import etree
        except ImportError:
            self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
            self.host.quit(1)
        try:
            element = etree.parse(sys.stdin).getroot()
        except Exception as e:
            self.parser.error(_(u"Can't parse the payload XML in input: {msg}").format(msg=e))
        if element.tag in ('item', '{http://jabber.org/protocol/pubsub}item'):
            if len(element) > 1:
                self.parser.error(_(u"<item> can only have one child element (the payload)"))
            element = element[0]
        payload = etree.tostring(element, encoding='unicode')

        self.host.bridge.psItemSend(self.args.service,
                                    self.args.node,
                                    payload,
                                    self.args.item,
                                    {},
                                    self.profile,
                                    callback=self.psItemsSendCb,
                                    errback=partial(self.errback,
                                                    msg=_(u"can't send item: {}"),
                                                    exit_code=C.EXIT_BRIDGE_ERRBACK))


class Get(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_LIST_XML, use_pubsub=True, pubsub_flags={C.NODE, C.MULTI_ITEMS}, help=_(u'get pubsub item(s)'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-S", "--sub-id", type=base.unicode_decoder, default=u'',
                                 help=_(u"subscription id"))
        self.parser.add_argument("-m", "--max", type=int, default=10, help=_(u"maximum number of items to get ({} to get all items)".format(C.NO_LIMIT)))
        # TODO: a key(s) argument to select keys to display
        # TODO: add MAM filters


    def psItemsGetCb(self, ps_result):
        self.output(ps_result[0])
        self.host.quit(C.EXIT_OK)

    def psItemsGetEb(self, failure_):
        self.disp(u"can't get pubsub items: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psItemsGet(
            self.args.service,
            self.args.node,
            self.args.max,
            self.args.items,
            self.args.sub_id,
            {},
            self.profile,
            callback=self.psItemsGetCb,
            errback=self.psItemsGetEb)

class Delete(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'delete an item'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-f", "--force", action='store_true', help=_(u"delete without confirmation"))
        self.parser.add_argument("-N", "--notify", action='store_true', help=_(u"notify deletion"))

    def psItemsDeleteCb(self):
        self.disp(_(u'item {item_id} has been deleted').format(item_id=self.args.item))
        self.host.quit(C.EXIT_OK)

    def start(self):
        if not self.args.item:
            self.parser.error(_(u"You need to specify an item to delete"))
        if not self.args.force:
            message = _(u"Are you sure to delete item {item_id} ?").format(item_id=self.args.item)
            res = raw_input("{} (y/N)? ".format(message))
            if res not in ("y", "Y"):
                self.disp(_(u"Item deletion cancelled"))
                self.host.quit(2)
        self.host.bridge.psRetractItem(
            self.args.service,
            self.args.node,
            self.args.item,
            self.args.notify,
            self.profile,
            callback=self.psItemsDeleteCb,
            errback=partial(self.errback,
                            msg=_(u"can't delete item: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class Edit(base.CommandBase, common.BaseEdit):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'edit', use_verbose=True, use_pubsub=True,
                                  pubsub_flags={C.NODE, C.SINGLE_ITEM}, use_draft=True, help=_(u'edit an existing or new pubsub item'))
        common.BaseEdit.__init__(self, self.host, PUBSUB_TMP_DIR)

    def add_parser_options(self):
        pass

    def edit(self, content_file_path, content_file_obj):
        # we launch editor
        self.runEditor("pubsub_editor_args", content_file_path, content_file_obj)

    def publish(self, content):
        published_id = self.host.bridge.psItemSend(self.pubsub_service, self.pubsub_node, content, self.pubsub_item or '', {}, self.profile)
        if published_id:
            self.disp(u"Item published at {pub_id}".format(pub_id=published_id))
        else:
            self.disp(u"Item published")

    def getItemData(self, service, node, item):
        try:
            from lxml import etree
        except ImportError:
            self.disp(u"lxml module must be installed to use edit, please install it with \"pip install lxml\"", error=True)
            self.host.quit(1)
        items = [item] if item is not None else []
        item_raw =  self.host.bridge.psItemsGet(service, node, 1, items, "", {}, self.profile)[0][0]
        parser = etree.XMLParser(remove_blank_text=True)
        item_elt = etree.fromstring(item_raw, parser)
        item_id = item_elt.get('id')
        try:
            payload = item_elt[0]
        except IndexError:
            self.disp(_(u'Item has not payload'), 1)
            return  u''
        return etree.tostring(payload, encoding="unicode", pretty_print=True), item_id

    def start(self):
        self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj = self.getItemPath()
        self.edit(content_file_path, content_file_obj)


class Subscribe(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'subscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'subscribe to a node'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psSubscribeCb(self, sub_id):
        self.disp(_(u'subscription done'), 1)
        if sub_id:
            self.disp(_(u'subscription id: {sub_id}').format(sub_id=sub_id))
        self.host.quit()

    def start(self):
        self.host.bridge.psSubscribe(
            self.args.service,
            self.args.node,
            {},
            self.profile,
            callback=self.psSubscribeCb,
            errback=partial(self.errback,
                            msg=_(u"can't subscribe to node: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class Unsubscribe(base.CommandBase):
    # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'unsubscribe', use_pubsub=True, pubsub_flags={C.NODE}, use_verbose=True, help=_(u'unsubscribe from a node'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psUnsubscribeCb(self):
        self.disp(_(u'subscription removed'), 1)
        self.host.quit()

    def start(self):
        self.host.bridge.psUnsubscribe(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psUnsubscribeCb,
            errback=partial(self.errback,
                            msg=_(u"can't unsubscribe from node: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class Subscriptions(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'subscriptions', use_output=C.OUTPUT_LIST_DICT, use_pubsub=True, help=_(u'retrieve all subscriptions on a service'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psSubscriptionsGetCb(self, subscriptions):
        self.output(subscriptions)
        self.host.quit()

    def start(self):
        self.host.bridge.psSubscriptionsGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psSubscriptionsGetCb,
            errback=partial(self.errback,
                            msg=_(u"can't retrieve subscriptions: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class Affiliations(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'affiliations', use_output=C.OUTPUT_DICT, use_pubsub=True, help=_(u'retrieve all affiliations on a service'))
        self.need_loop=True

    def add_parser_options(self):
        pass

    def psAffiliationsGetCb(self, affiliations):
        self.output(affiliations)
        self.host.quit()

    def psAffiliationsGetEb(self, failure_):
        self.disp(u"can't get node affiliations: {reason}".format(
            reason=failure_), error=True)
        self.host.quit(C.EXIT_BRIDGE_ERRBACK)

    def start(self):
        self.host.bridge.psAffiliationsGet(
            self.args.service,
            self.args.node,
            self.profile,
            callback=self.psAffiliationsGetCb,
            errback=self.psAffiliationsGetEb)


class Search(base.CommandBase):
    """this command to a search without using MAM, i.e. by checking every items if dound by itself, so it may be heavy in resources both for server and client"""
    RE_FLAGS = re.MULTILINE | re.UNICODE
    EXEC_ACTIONS = (u'exec', u'external')

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'search', use_output=C.OUTPUT_XML, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS},
                                  use_verbose=True, help=_(u'search items corresponding to filters'))
        self.need_loop=True

    @property
    def etree(self):
        """load lxml.etree only if needed"""
        if self._etree is None:
            from lxml import etree
            self._etree = etree
        return self._etree

    def filter_opt(self, value, type_):
        value = base.unicode_decoder(value)
        return (type_, value)

    def filter_flag(self, value, type_):
        value = C.bool(value)
        return (type_, value)

    def add_parser_options(self):
        self.parser.add_argument("-D", "--max-depth", type=int, default=0, help=_(u"maximum depth of recursion (will search linked nodes if > 0, default: 0)"))
        self.parser.add_argument("-m", "--max", type=int, default=30, help=_(u"maximum number of items to get per node ({} to get all items, default: 30)".format(C.NO_LIMIT)))
        self.parser.add_argument("-N", "--namespace", action='append', nargs=2, default=[],
                                 metavar="NAME NAMESPACE", help=_(u"namespace to use for xpath"))

        # filters
        filter_text = partial(self.filter_opt, type_=u'text')
        filter_re = partial(self.filter_opt, type_=u'regex')
        filter_xpath = partial(self.filter_opt, type_=u'xpath')
        filter_python = partial(self.filter_opt, type_=u'python')
        filters = self.parser.add_argument_group(_(u'filters'), _(u'only items corresponding to following filters will be kept'))
        filters.add_argument("-t", "--text",
                             action='append', dest='filters', type=filter_text,
                             metavar='TEXT',
                             help=_(u"full text filter, item must contain this string (XML included)"))
        filters.add_argument("-r", "--regex",
                             action='append', dest='filters', type=filter_re,
                             metavar='EXPRESSION',
                             help=_(u"like --text but using a regular expression"))
        filters.add_argument("-x", "--xpath",
                             action='append', dest='filters', type=filter_xpath,
                             metavar='XPATH',
                             help=_(u"filter items which has elements matching this xpath"))
        filters.add_argument("-P", "--python",
                             action='append', dest='filters', type=filter_python,
                             metavar='PYTHON_CODE',
                             help=_(u'Python expression which much return a bool (True to keep item, False to reject it). "item" is raw text item, "item_xml" is lxml\'s etree.Element'))

        # filters flags
        flag_case = partial(self.filter_flag, type_=u'ignore-case')
        flag_invert = partial(self.filter_flag, type_=u'invert')
        flag_dotall = partial(self.filter_flag, type_=u'dotall')
        flag_matching = partial(self.filter_flag, type_=u'only-matching')
        flags = self.parser.add_argument_group(_(u'filters flags'), _(u'filters modifiers (change behaviour of following filters)'))
        flags.add_argument("-C", "--ignore-case",
                           action='append', dest='filters', type=flag_case,
                           const=('ignore-case', True), nargs='?',
                           metavar='BOOLEAN',
                           help=_(u"(don't) ignore case in following filters (default: case sensitive)"))
        flags.add_argument("-I", "--invert",
                           action='append', dest='filters', type=flag_invert,
                           const=('invert', True), nargs='?',
                           metavar='BOOLEAN',
                           help=_(u"(don't) invert effect of following filters (default: don't invert)"))
        flags.add_argument("-A", "--dot-all",
                           action='append', dest='filters', type=flag_dotall,
                           const=('dotall', True), nargs='?',
                           metavar='BOOLEAN',
                           help=_(u"(don't) use DOTALL option for regex (default: don't use)"))
        flags.add_argument("-o", "--only-matching",
                           action='append', dest='filters', type=flag_matching,
                           const=('only-matching', True), nargs='?',
                           metavar='BOOLEAN',
                           help=_(u"keep only the matching part of the item"))

        # action
        self.parser.add_argument("action",
                                 default="print",
                                 nargs='?',
                                 choices=('print', 'exec', 'external'),
                                 help=_(u"action to do on found items (default: print)"))
        self.parser.add_argument("command", nargs=argparse.REMAINDER)

    def psItemsGetEb(self, failure_, service, node):
        self.disp(u"can't get pubsub items at {service} (node: {node}): {reason}".format(
            service=service,
            node=node,
            reason=failure_), error=True)
        self.to_get -= 1

    def getItems(self, depth, service, node, items):
        search = partial(self.search, depth=depth)
        errback = partial(self.psItemsGetEb, service=service, node=node)
        self.host.bridge.psItemsGet(
            service,
            node,
            self.args.max,
            items,
            "",
            {},
            self.profile,
            callback=search,
            errback=errback
            )
        self.to_get += 1

    def _checkPubsubURL(self, match, found_nodes):
        """check that the matched URL is an xmpp: one

        @param found_nodes(list[unicode]): found_nodes
            this list will be filled while xmpp: URIs are discovered
        """
        url = match.group(0)
        if url.startswith(u'xmpp'):
            try:
                url_data = uri.parseXMPPUri(url)
            except ValueError:
                return
            if url_data[u'type'] == u'pubsub':
                found_node = {u'service': url_data[u'path'],
                              u'node': url_data[u'node']}
                if u'item' in url_data:
                    found_node[u'item'] = url_data[u'item']
                found_nodes.append(found_node)

    def getSubNodes(self, item, depth):
        """look for pubsub URIs in item, and getItems on the linked nodes"""
        found_nodes = []
        checkURI = partial(self._checkPubsubURL, found_nodes=found_nodes)
        strings.RE_URL.sub(checkURI, item)
        for data in found_nodes:
            self.getItems(depth+1,
                          data[u'service'],
                          data[u'node'],
                          [data[u'item']] if u'item' in data else []
                          )

    def parseXml(self, item):
        try:
            return self.etree.fromstring(item)
        except self.etree.XMLSyntaxError:
            self.disp(_(u"item doesn't looks like XML, you have probably used --only-matching somewhere before and we have no more XML"), error=True)
            self.host.quit(C.EXIT_BAD_ARG)

    def filter(self, item):
        """apply filters given on command line

        if only-matching is used, item may be modified
        @return (tuple[bool, unicode]): a tuple with:
            - keep: True if item passed the filters
            - item: it is returned in case of modifications
        """
        ignore_case = False
        invert = False
        dotall = False
        only_matching = False
        item_xml = None
        for type_, value in self.args.filters:
            keep = True

            ## filters

            if type_ == u'text':
                if ignore_case:
                    if value.lower() not in item.lower():
                        keep = False
                else:
                    if value not in item:
                        keep = False
                if keep and only_matching:
                    # doesn't really make sens to keep a fixed string
                    # so we raise an error
                    self.host.disp(_(u"--only-matching used with fixed --text string, are you sure?"), error=True)
                    self.host.quit(C.EXIT_BAD_ARG)
            elif type_ == u'regex':
                flags = self.RE_FLAGS
                if ignore_case:
                    flags |= re.IGNORECASE
                if dotall:
                    flags |= re.DOTALL
                match = re.search(value, item, flags)
                keep = match != None
                if keep and only_matching:
                    item = match.group()
                    item_xml = None
            elif type_ == u'xpath':
                if item_xml is None:
                    item_xml = self.parseXml(item)
                try:
                    elts = item_xml.xpath(value, namespaces=self.args.namespace)
                except self.etree.XPathEvalError as e:
                    self.disp(_(u"can't use xpath: {reason}").format(reason=e), error=True)
                    self.host.quit(C.EXIT_BAD_ARG)
                keep = bool(elts)
                if keep and only_matching:
                    item_xml = elts[0]
                    try:
                        item = self.etree.tostring(item_xml, encoding='unicode')
                    except TypeError:
                        # we have a string only, not an element
                        item = unicode(item_xml)
                        item_xml = None
            elif type_ == u'python':
                if item_xml is None:
                    item_xml = self.parseXml(item)
                cmd_ns = {u'item': item,
                          u'item_xml': item_xml
                         }
                try:
                    keep = eval(value, cmd_ns)
                except SyntaxError as e:
                    self.disp(unicode(e), error=True)
                    self.host.quit(C.EXIT_BAD_ARG)

            ## flags

            elif type_ == u'ignore-case':
                ignore_case = value
            elif type_ == u'invert':
                invert = value
                # we need to continue, else loop would end here
                continue
            elif type_ == u'dotall':
                dotall = value
            elif type_ == u'only-matching':
                only_matching = value
            else:
                raise exceptions.InternalError(_(u"unknown filter type {type}").format(type=type_))

            if invert:
                keep = not keep
            if not keep:
                return False, item

        return True, item

    def doItemAction(self, item, metadata):
        """called when item has been kepts and the action need to be done

        @param item(unicode): accepted item
        """
        action = self.args.action
        if action == u'print' or self.host.verbosity > 0:
            try:
                self.output(item)
            except self.etree.XMLSyntaxError:
                # item is not valid XML, but a string
                # can happen when --only-matching is used
                self.disp(item)
        if action in self.EXEC_ACTIONS:
            item_elt = self.parseXml(item)
            if action == u'exec':
                use = {'service': metadata[u'service'],
                       'node': metadata[u'node'],
                       'item': item_elt.get('id'),
                       'profile': self.profile
                       }
                # we need to send a copy of self.args.command
                # else it would be modified
                parser_args, use_args = arg_tools.get_use_args(self.host,
                    self.args.command,
                    use,
                    verbose=self.host.verbosity > 1
                    )
                cmd_args = sys.argv[0:1] + parser_args + use_args
            else:
                cmd_args = self.args.command


            self.disp(u'COMMAND: {command}'.format(
                command = u' '.join([arg_tools.escape(a) for a in cmd_args])), 2)
            if action == u'exec':
                ret = subprocess.call(cmd_args)
            else:
                p = subprocess.Popen(cmd_args, stdin=subprocess.PIPE)
                p.communicate(item.encode('utf-8'))
                ret = p.wait()
            if ret != 0:
                self.disp(A.color(C.A_FAILURE, _(u"executed command failed with exit code {code}").format(code=ret)))

    def search(self, items_data, depth):
        """callback of getItems

        this method filters items, get sub nodes if needed,
        do the requested action, and exit the command when everything is done
        @param items_data(tuple): result of getItems
        @param depth(int): current depth level
            0 for first node, 1 for first children, and so on
        """
        items, metadata = items_data
        for item in items:
            if depth < self.args.max_depth:
                self.getSubNodes(item, depth)
            keep, item = self.filter(item)
            if not keep:
                continue
            self.doItemAction(item, metadata)

        # we check if we got all getItems results
        self.to_get -= 1
        if self.to_get == 0:
            # yes, we can quit
            self.host.quit()
        assert self.to_get > 0

    def start(self):
        if self.args.command:
            if self.args.action not in self.EXEC_ACTIONS:
                self.parser.error(_(u"Command can only be used with {actions} actions").format(
                    actions=u', '.join(self.EXEC_ACTIONS)))
        else:
            if self.args.action in self.EXEC_ACTIONS:
                self.parser.error(_(u"you need to specify a command to execute"))
        if not self.args.node:
            # TODO: handle get service affiliations when node is not set
            self.parser.error(_(u"empty node is not handled yet"))
        # to_get is increased on each get and decreased on each answer
        # when it reach 0 again, the command is finished
        self.to_get = 0
        self._etree = None
        if self.args.filters is None:
            self.args.filters = []
        self.args.namespace = dict(self.args.namespace + [('pubsub', "http://jabber.org/protocol/pubsub")])
        self.getItems(0, self.args.service, self.args.node, self.args.items)


class Uri(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'uri', use_profile=False, use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'build URI'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument("-p", "--profile", type=base.unicode_decoder, default=C.PROF_KEY_DEFAULT, help=_(u"profile (used when no server is specified)"))

    def display_uri(self, jid_):
        uri_args = {}
        if not self.args.service:
            self.args.service = jid.JID(jid_).bare

        for key in ('node', 'service', 'item'):
            value = getattr(self.args, key)
            if key == 'service':
                key = 'path'
            if value:
                uri_args[key] = value
        self.disp(uri.buildXMPPUri(u'pubsub', **uri_args))
        self.host.quit()

    def start(self):
        if not self.args.service:
            self.host.bridge.asyncGetParamA(
                u'JabberID',
                u'Connection',
                profile_key=self.args.profile,
                callback=self.display_uri,
                errback=partial(self.errback,
                                msg=_(u"can't retrieve jid: {}"),
                                exit_code=C.EXIT_BRIDGE_ERRBACK))
        else:
            self.display_uri(None)


class HookCreate(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'create a Pubsub hook'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument('-t', '--type', default=u'python', choices=('python', 'python_file', 'python_code'), help=_(u"hook type"))
        self.parser.add_argument('-P', '--persistent', action='store_true', help=_(u"make hook persistent across restarts"))
        self.parser.add_argument("hook_arg", type=base.unicode_decoder, help=_(u"argument of the hook (depend of the type)"))

    @staticmethod
    def checkArgs(self):
        if self.args.type == u'python_file':
            self.args.hook_arg = os.path.abspath(self.args.hook_arg)
            if not os.path.isfile(self.args.hook_arg):
                self.parser.error(_(u"{path} is not a file").format(path=self.args.hook_arg))

    def start(self):
        self.checkArgs(self)
        self.host.bridge.psHookAdd(
            self.args.service,
            self.args.node,
            self.args.type,
            self.args.hook_arg,
            self.args.persistent,
            self.profile,
            callback=self.host.quit,
            errback=partial(self.errback,
                            msg=_(u"can't create hook: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class HookDelete(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'delete', use_pubsub=True, pubsub_flags={C.NODE}, help=_(u'delete a Pubsub hook'))
        self.need_loop=True

    def add_parser_options(self):
        self.parser.add_argument('-t', '--type', default=u'', choices=('', 'python', 'python_file', 'python_code'), help=_(u"hook type to remove, empty to remove all (default: remove all)"))
        self.parser.add_argument('-a', '--arg', dest='hook_arg', type=base.unicode_decoder, default=u'', help=_(u"argument of the hook to remove, empty to remove all (default: remove all)"))

    def psHookRemoveCb(self, nb_deleted):
        self.disp(_(u'{nb_deleted} hook(s) have been deleted').format(
            nb_deleted = nb_deleted))
        self.host.quit()

    def start(self):
        HookCreate.checkArgs(self)
        self.host.bridge.psHookRemove(
            self.args.service,
            self.args.node,
            self.args.type,
            self.args.hook_arg,
            self.profile,
            callback=self.psHookRemoveCb,
            errback=partial(self.errback,
                            msg=_(u"can't delete hook: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class HookList(base.CommandBase):

    def __init__(self, host):
        base.CommandBase.__init__(self, host, 'list', use_output=C.OUTPUT_LIST_DICT, help=_(u'list hooks of a profile'))
        self.need_loop = True

    def add_parser_options(self):
        pass

    def psHookListCb(self, data):
        if not data:
            self.disp(_(u'No hook found.'))
        self.output(data)
        self.host.quit()

    def start(self):
        self.host.bridge.psHookList(
            self.profile,
            callback=self.psHookListCb,
            errback=partial(self.errback,
                            msg=_(u"can't list hooks: {}"),
                            exit_code=C.EXIT_BRIDGE_ERRBACK))


class Hook(base.CommandBase):
    subcommands = (HookCreate, HookDelete, HookList)

    def __init__(self, host):
        super(Hook, self).__init__(host, 'hook', use_profile=False, use_verbose=True, help=_('trigger action on Pubsub notifications'))


class Pubsub(base.CommandBase):
    subcommands = (Set, Get, Delete, Edit, Subscribe, Unsubscribe, Subscriptions, Node, Affiliations, Search, Hook, Uri)

    def __init__(self, host):
        super(Pubsub, self).__init__(host, 'pubsub', use_profile=False, help=_('PubSub nodes/items management'))