view sat_frontends/jp/cmd_pubsub.py @ 2776:838f53730ce4

plugin pubsub admin: pubsub administrator first draft: This experimental protocol allows to publish items where publishers can be specified.
author Goffi <goffi@goffi.org>
date Tue, 15 Jan 2019 08:51:54 +0100
parents b7974814dd5b
children ff1b40823b07
line wrap: on
line source

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

# jp: a SàT command line tool
# Copyright (C) 2009-2019 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)
            self.host.confirmOrQuit(message, _(u"node deletion cancelled"))

        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"),
        )
        #  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.getPubsubExtra(),
            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
            )
            self.host.confirmOrQuit(message, _(u"item deletion cancelled"))
        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 do a search without using MAM

    This commands checks every items it finds 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):
        # FIXME: C.NO_MAX is not needed here, and this can be globally removed from consts
        #        the only interest is to change the help string, but this can be explained
        #        extensively in man pages (max is for each node found)
        base.CommandBase.__init__(
            self,
            host,
            "search",
            use_output=C.OUTPUT_XML,
            use_pubsub=True,
            pubsub_flags={C.MULTI_ITEMS, C.NO_MAX},
            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",
            "--node-max",
            type=int,
            default=30,
            help=_(u"maximum number of items to get per node ({} to get all items, "
                   u"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(
            "-k",
            "--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.node_max,
            items,
            "",
            self.getPubsubExtra(),
            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")
        )