changeset 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 1dfc5516dead
children 8d82a62fa098
files frontends/src/jp/base.py frontends/src/jp/cmd_blog.py frontends/src/jp/cmd_event.py frontends/src/jp/cmd_forums.py frontends/src/jp/cmd_merge_request.py frontends/src/jp/cmd_pubsub.py frontends/src/jp/common.py frontends/src/jp/constants.py
diffstat 8 files changed, 267 insertions(+), 295 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/jp/base.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/base.py	Wed Mar 21 19:13:22 2018 +0100
@@ -34,9 +34,11 @@
 from sat_frontends.tools.jid import JID
 from sat.tools import config
 from sat.tools.common import dynamic_import
+from sat.tools.common import uri
 from sat.core import exceptions
 import sat_frontends.jp
 from sat_frontends.jp.constants import Const as C
+from sat_frontends.tools import misc
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
 import shlex
 from collections import OrderedDict
@@ -316,14 +318,46 @@
         verbose_parent = self.parents['verbose'] = argparse.ArgumentParser(add_help=False)
         verbose_parent.add_argument('--verbose', '-v', action='count', default=0, help=_(u"Add a verbosity level (can be used multiple times)"))
 
-        for parent_name in ('pubsub', 'pubsub_node_req'):
-            parent = self.parents[parent_name] = argparse.ArgumentParser(add_help=False)
-            parent.add_argument("-s", "--service", type=unicode_decoder, default=u'',
-                                help=_(u"JID of the PubSub service (default: PEP service)"))
-            if parent_name == 'pubsub_node_req':
-                parent.add_argument("node", type=unicode_decoder, help=_(u"node to request"))
-            else:
-                parent.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=_(u"node to request"))
+        draft_parent = self.parents['draft'] = argparse.ArgumentParser(add_help=False)
+        draft_group = draft_parent.add_argument_group(_('draft handling'))
+        draft_group.add_argument("-D", "--current", action="store_true", help=_(u"load current draft"))
+        draft_group.add_argument("-F", "--draft-path", type=unicode_decoder, help=_(u"path to a draft file to retrieve"))
+
+
+    def make_pubsub_group(self, flags):
+        """generate pubsub options according to flags
+
+        @param flags(iterable[unicode]): see [CommandBase.__init__]
+        @return (ArgumentParser): parser to add
+        """
+        flags = misc.FlagsHandler(flags)
+        parent = argparse.ArgumentParser(add_help=False)
+        pubsub_group = parent.add_argument_group('pubsub')
+        pubsub_group.add_argument("-u", "--pubsub-url", type=unicode_decoder,
+                                  help=_(u"Pubsub URL (xmpp or http)"))
+
+        service_help = _(u"JID of the PubSub service")
+        if not flags.service:
+            service_help += _(u" (default: PEP service)")
+        pubsub_group.add_argument("-s", "--service", type=unicode_decoder, default=u'',
+                                  help=service_help)
+
+        node_help = _(u"node to request")
+        if not flags.node:
+            node_help += _(u" (DEFAULT: standard node)")
+        pubsub_group.add_argument("-n", "--node", type=unicode_decoder, default=u'', help=node_help)
+
+        if flags.single_item:
+            pubsub_group.add_argument("-i", "--item", type=unicode_decoder, help=_(u"item to retrieve"))
+            pubsub_group.add_argument("-L", "--last-item", action='store_true', help=_(u'retrieve last item'))
+        elif flags.multi_items:
+            # mutiple items
+            pubsub_group.add_argument("-i", "--item", type=unicode_decoder, action='append', dest='items', default=[], help=_(u"items to retrieve (DEFAULT: all)"))
+
+        if flags:
+            raise exceptions.InternalError('unknowns flags: {flags}'.format(flags=u', '.join(flags)))
+
+        return parent
 
     def add_parser_options(self):
         self.parser.add_argument('--version', action='version', version=("%(name)s %(version)s %(copyleft)s" % {'name': PROG_NAME, 'version': self.version, 'copyleft': COPYLEFT}))
@@ -402,10 +436,89 @@
                 cls = getattr(module, class_name)
                 cls(self)
 
+    def get_xmpp_uri_from_http(self, http_url):
+        """parse HTML page at http(s) URL, and looks for xmpp: uri"""
+        if http_url.startswith('https'):
+            scheme = u'https'
+        elif http_url.startswith('http'):
+            scheme = u'http'
+        else:
+            raise exceptions.InternalError(u'An HTTP scheme is expected in this method')
+        self.disp(u"{scheme} URL found, trying to find associated xmpp: URI".format(scheme=scheme.upper()),1)
+        # HTTP URL, we try to find xmpp: links
+        try:
+            from lxml import etree
+        except ImportError:
+            self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
+            self.host.quit(1)
+        import urllib2
+        parser = etree.HTMLParser()
+        try:
+            root = etree.parse(urllib2.urlopen(http_url), parser)
+        except etree.XMLSyntaxError as e:
+            self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
+            links = []
+        else:
+            links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
+        if not links:
+            self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
+            self.host.quit(1)
+        xmpp_uri = links[0].get('href')
+        return xmpp_uri
+
+    def parse_pubsub_args(self):
+        if self.args.pubsub_url is not None:
+            url = self.args.pubsub_url
+
+            if url.startswith('http'):
+                # http(s) URL, we try to retrieve xmpp one from there
+                url = self.get_xmpp_uri_from_http(url)
+
+            try:
+                uri_data = uri.parseXMPPUri(url)
+            except ValueError:
+                self.parser.error(_(u'invalid XMPP URL: {url}').format(url=url))
+            else:
+                if uri_data[u'type'] == 'pubsub':
+                    # URL is alright, we only set data not already set by other options
+                    if not self.args.service:
+                        self.args.service = uri_data[u'path']
+                    if not self.args.node:
+                        self.args.node = uri_data[u'node']
+                    uri_item = uri_data.get(u'item')
+                    if uri_item:
+                        try:
+                            item, item_magic = self.args.item, self.args.item_magic
+                        except AttributeError:
+                            if not self.args.items:
+                                self.args.items = [uri_item]
+                        else:
+                            if not item and not item_magic:
+                                self.args.item = uri_item
+                else:
+                    self.parser.error(_(u'XMPP URL is not a pubsub one: {url}').format(url=url))
+        flags = self.args._cmd._pubsub_flags
+        # we check required arguments here instead of using add_arguments' required option
+        # because the required argument can be set in URL
+        if C.SERVICE in flags and not self.args.service:
+            self.parser.error(_(u"argument -s/--service is required"))
+        if C.NODE in flags and not self.args.node:
+            self.parser.error(_(u"argument -n/--node is required"))
+
+        # FIXME: mutually groups can't be nested in a group and don't support title
+        #        so we check conflict here. This may be fixed in Python 3, to be checked
+        try:
+            if self.args.item and self.args.item_last:
+                self.parser.error(_(u"--item and --item-last can't be used at the same time"))
+        except AttributeError:
+            pass
+
     def run(self, args=None, namespace=None):
         self.args = self.parser.parse_args(args, namespace=None)
+        if self.args._cmd._use_pubsub:
+            self.parse_pubsub_args()
         try:
-            self.args.func()
+            self.args._cmd.run()
             if self._need_loop or self._auto_loop:
                 self._start_loop()
         except KeyboardInterrupt:
@@ -582,6 +695,14 @@
             - use_progress(bool): if True, add progress bar activation option
                 progress* signals will be handled
             - use_verbose(bool): if True, add verbosity option
+            - use_pubsub(bool): if True, add pubsub options
+                mandatory arguments are controlled by pubsub_req
+            - use_draft(bool): if True, add draft handling options
+            ** other arguments **
+            - pubsub_flags(iterable[unicode]): tuple of flags to set pubsub options, can be:
+                C.SERVICE: service is required
+                C.NODE: node is required
+                C.SINGLE_ITEM: only one item is allowed
         @attribute need_loop(bool): to set by commands when loop is needed
         """
         self.need_loop = False # to be set by commands when loop is needed
@@ -632,9 +753,11 @@
         else:
             assert extra_outputs is None
 
-        if 'use_pubsub' in kwargs and 'use_pubsub_node_req' in kwargs:
-            raise exceptions.InternalError(u"use_pubsub and use_pubsub_node_req can't be used at the same time."
-                                           u"Use the later one when node is required (else an empty string is used as default)")
+        self._use_pubsub = kwargs.pop('use_pubsub', False)
+        if self._use_pubsub:
+            flags = kwargs.pop('pubsub_flags', None)
+            parents.add(self.host.make_pubsub_group(flags))
+            self._pubsub_flags = flags
 
         # other common options
         use_opts = {k:v for k,v in kwargs.iteritems() if k.startswith('use_')}
@@ -650,7 +773,7 @@
         if hasattr(self, "subcommands"):
             self.subparsers = self.parser.add_subparsers()
         else:
-            self.parser.set_defaults(func=self.run)
+            self.parser.set_defaults(_cmd=self)
         self.add_parser_options()
 
     @property
--- a/frontends/src/jp/cmd_blog.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/cmd_blog.py	Wed Mar 21 19:13:22 2018 +0100
@@ -76,38 +76,28 @@
 OUTPUT_OPT_NO_HEADER = u'no-header'
 
 
-class BlogCommon(object):
-
-    def __init__(self, host):
-        self.host = host
-
-    def addServiceNodeArgs(self):
-        self.parser.add_argument("-n", "--node", type=base.unicode_decoder, default=u'', help=_(u"PubSub node to request (default: microblog namespace)"))
-        self.parser.add_argument("-s", "--service", type=base.unicode_decoder, default=u'', help=_(u"JID of the PubSub service (default: request profile own blog)"))
-
-    def guessSyntaxFromPath(self, sat_conf, path):
-        """Return syntax guessed according to filename extension
+def guessSyntaxFromPath(host, sat_conf, path):
+    """Return syntax guessed according to filename extension
 
-        @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
-        @param path(str): path to the content file
-        @return(unicode): syntax to use
-        """
-        # we first try to guess syntax with extension
-        ext = os.path.splitext(path)[1][1:] # we get extension without the '.'
-        if ext:
-            for k,v in SYNTAX_EXT.iteritems():
-                if k and ext == v:
-                    return k
+    @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
+    @param path(str): path to the content file
+    @return(unicode): syntax to use
+    """
+    # we first try to guess syntax with extension
+    ext = os.path.splitext(path)[1][1:] # we get extension without the '.'
+    if ext:
+        for k,v in SYNTAX_EXT.iteritems():
+            if k and ext == v:
+                return k
 
-        # if not found, we use current syntax
-        return self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile)
+    # if not found, we use current syntax
+    return host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
 
 
 class BlogPublishCommon(object):
     """handle common option for publising commands (Set and Edit)"""
 
     def add_parser_options(self):
-        self.addServiceNodeArgs()
         self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item"))
         self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item"))
         self.parser.add_argument("-C", "--comments", action='store_true', help=_(u"enable comments"))
@@ -134,24 +124,22 @@
             mb_data['title'] = self.args.title
 
 
-class Set(base.CommandBase, BlogCommon, BlogPublishCommon):
+class Set(base.CommandBase, BlogPublishCommon):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', help=_(u'publish a new blog item or update an existing one'))
-        BlogCommon.__init__(self, self.host)
+        base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM},
+                                  help=_(u'publish a new blog item or update an existing one'))
         BlogPublishCommon.__init__(self)
         self.need_loop=True
 
     def add_parser_options(self):
         BlogPublishCommon.add_parser_options(self)
-        self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=None, help=_(u"id of the item to publish"))
 
     def mbSendCb(self):
         self.disp(u"Item published")
         self.host.quit(C.EXIT_OK)
 
     def start(self):
-        common.checkURI(self.args)
         self.pubsub_item = self.args.item
         mb_data = {}
         self.setMbDataFromArgs(mb_data)
@@ -169,20 +157,17 @@
                             exit_code=C.EXIT_BRIDGE_ERRBACK))
 
 
-class Get(base.CommandBase, BlogCommon):
+class Get(base.CommandBase):
     TEMPLATE = u"blog/articles.html"
 
     def __init__(self, host):
         extra_outputs = {'default': self.default_output,
                          'fancy': self.fancy_output}
-        base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)'))
-        BlogCommon.__init__(self, self.host)
+        base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS},
+                                  use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)'))
         self.need_loop=True
 
     def add_parser_options(self):
-        self.addServiceNodeArgs()
-        self.parser.add_argument("-i", "--item", type=base.unicode_decoder, action='append', default=[], dest='items',
-                                 help=_(u"item(s) id(s) to get (default: request all items)"))
         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
         self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys',
@@ -339,7 +324,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.mbGet(
             self.args.service,
             self.args.node,
@@ -351,19 +335,23 @@
             errback=self.mbGetEb)
 
 
-class Edit(base.CommandBase, BlogCommon, BlogPublishCommon, common.BaseEdit):
+class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post'))
-        BlogCommon.__init__(self, self.host)
+        base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM},
+                                  use_draft=True, use_verbose=True, help=_(u'edit an existing or new blog post'))
         BlogPublishCommon.__init__(self)
         common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True)
 
+    @property
+    def current_syntax(self):
+        if self._current_syntax is None:
+            self._current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile)
+        return self._current_syntax
+
     def add_parser_options(self):
         BlogPublishCommon.add_parser_options(self)
         self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel"))
-        self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword"))
-        common.BaseEdit.add_parser_options(self)
 
     def buildMetadataFile(self, content_file_path, mb_data=None):
         """Build a metadata file using json
@@ -436,8 +424,6 @@
 
     def getTmpSuff(self):
         # we get current syntax to determine file extension
-        if self.current_syntax is None:
-            self.current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile)
         return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT[''])
 
     def getItemData(self, service, node, item):
@@ -466,26 +452,26 @@
     def start(self):
         # if there are user defined extension, we use them
         SYNTAX_EXT.update(config.getConfig(self.sat_conf, 'jp', CONF_SYNTAX_EXT, {}))
-        self.current_syntax = self.args.syntax
-        if self.current_syntax is not None:
+        self._current_syntax = self.args.syntax
+        if self._current_syntax is not None:
             try:
-                self.current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax)
+                self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax)
             except Exception as e:
                 if "NotFound" in unicode(e):  # FIXME: there is not good way to check bridge errors
                     self.parser.error(_(u"unknown syntax requested ({syntax})").format(syntax=self.args.syntax))
                 else:
                     raise e
 
-        self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath(self.args.item)
+        self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath()
 
         self.edit(content_file_path, content_file_obj, mb_data=mb_data)
 
 
-class Preview(base.CommandBase, BlogCommon):
+class Preview(base.CommandBase):
+    # TODO: need to be rewritten with template output
 
     def __init__(self, host):
         base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content'))
-        BlogCommon.__init__(self, self.host)
 
     def add_parser_options(self):
         self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview"))
@@ -626,7 +612,7 @@
 
 class Import(base.CommandAnswering):
     def __init__(self, host):
-        super(Import, self).__init__(host, 'import', use_progress=True, help=_(u'import an external blog'))
+        super(Import, self).__init__(host, 'import', use_pubsub=True, use_progress=True, help=_(u'import an external blog'))
         self.need_loop=True
 
     def add_parser_options(self):
@@ -637,10 +623,6 @@
         self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate for uploads"))
         self.parser.add_argument('-o', '--option', action='append', nargs=2, default=[], metavar=(u'NAME', u'VALUE'),
             help=_(u"importer specific options (see importer description)"))
-        self.parser.add_argument('--service', type=base.unicode_decoder, default=u'', metavar=u'PUBSUB_SERVICE',
-            help=_(u"PubSub service where the items must be uploaded (default: server)"))
-        self.parser.add_argument('-n', '--node', type=base.unicode_decoder, default=u'', metavar=u'PUBSUB_NODE',
-            help=_(u"PubSub node where the items must be uploaded (default: tickets' defaults)"))
         self.parser.add_argument("location", type=base.unicode_decoder, nargs='?',
             help=_(u"importer data location (see importer description), nothing to show importer description"))
 
--- a/frontends/src/jp/cmd_event.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/cmd_event.py	Wed Mar 21 19:13:22 2018 +0100
@@ -42,13 +42,13 @@
                                   host,
                                   'get',
                                   use_output=C.OUTPUT_DICT,
-                                  use_pubsub_node_req=True,
+                                  use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM},
                                   use_verbose=True,
                                   help=_(u'get event data'))
         self.need_loop=True
 
     def add_parser_options(self):
-        self.parser.add_argument("-i", "--item", type=base.unicode_decoder, default=u'', help=_(u"ID of the PubSub Item"))
+        pass
 
     def eventInviteeGetCb(self, result):
         event_date, event_data = result
@@ -57,7 +57,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.eventGet(
             self.args.service,
             self.args.node,
@@ -100,7 +99,7 @@
 
 class Create(EventBase, base.CommandBase):
     def __init__(self, host):
-        super(Create, self).__init__(host, 'create', use_pubsub_node_req=True, help=_('create or replace event'))
+        super(Create, self).__init__(host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_('create or replace event'))
         EventBase.__init__(self)
         self.need_loop=True
 
@@ -126,12 +125,11 @@
 
 class Modify(EventBase, base.CommandBase):
     def __init__(self, host):
-        super(Modify, self).__init__(host, 'modify', use_pubsub_node_req=True, help=_('modify an existing event'))
+        super(Modify, self).__init__(host, 'modify', use_pubsub=True, pubsub_flags={C.NODE}, help=_('modify an existing event'))
         EventBase.__init__(self)
         self.need_loop=True
 
     def start(self):
-        common.checkURI(self.args)
         fields = self.parseFields()
         date = 0 if not self.args.date else self.parseDate()
         self.host.bridge.eventModify(
@@ -154,7 +152,7 @@
                                   host,
                                   'get',
                                   use_output=C.OUTPUT_DICT,
-                                  use_pubsub_node_req=True,
+                                  use_pubsub=True, pubsub_flags={C.NODE},
                                   use_verbose=True,
                                   help=_(u'get event attendance'))
         self.need_loop=True
@@ -167,7 +165,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.eventInviteeGet(
             self.args.service,
             self.args.node,
@@ -180,7 +177,7 @@
 
 class InviteeSet(base.CommandBase):
     def __init__(self, host):
-        super(InviteeSet, self).__init__(host, 'set', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, help=_('set event attendance'))
+        super(InviteeSet, self).__init__(host, 'set', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_('set event attendance'))
         self.need_loop=True
 
     def add_parser_options(self):
@@ -188,7 +185,6 @@
                                  metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set"))
 
     def start(self):
-        common.checkURI(self.args)
         fields = dict(self.args.fields) if self.args.fields else {}
         self.host.bridge.eventInviteeSet(
             self.args.service,
@@ -210,7 +206,7 @@
                                   'list',
                                   use_output=C.OUTPUT_DICT_DICT,
                                   extra_outputs=extra_outputs,
-                                  use_pubsub_node_req=True,
+                                  use_pubsub=True, pubsub_flags={C.NODE},
                                   use_verbose=True,
                                   help=_(u'get event attendance'))
         self.need_loop=True
@@ -355,7 +351,6 @@
     def start(self):
         if self.args.no_rsvp and not self.args.missing:
             self.parser.error(_(u"you need to use --missing if you use --no-rsvp"))
-        common.checkURI(self.args)
         if self.args.missing:
             self.host.bridge.psNodeAffiliationsGet(
                 self.args.service,
@@ -372,21 +367,19 @@
 class InviteeInvite(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'invite', use_pubsub_node_req=True, help=_(u'invite someone to the event through email'))
+        base.CommandBase.__init__(self, host, 'invite', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'invite someone to the event through email'))
         self.need_loop=True
 
     def add_parser_options(self):
-        self.parser.add_argument("-i", "--item", type=base.unicode_decoder, default=u'', help=_(u"ID of the PubSub Item"))
         self.parser.add_argument("-e", "--email", action="append", type=base.unicode_decoder, default=[], help='email(s) to send the invitation to')
-        self.parser.add_argument("-n", "--name", type=base.unicode_decoder, default='', help='name of the invitee')
-        self.parser.add_argument("-N", "--host-name", type=base.unicode_decoder, default='', help='name of the host')
+        self.parser.add_argument("-N", "--name", type=base.unicode_decoder, default='', help='name of the invitee')
+        self.parser.add_argument("-H", "--host-name", type=base.unicode_decoder, default='', help='name of the host')
         self.parser.add_argument("-l", "--lang", type=base.unicode_decoder, default='', help='main language spoken by the invitee')
-        self.parser.add_argument("-u", "--url", type=base.unicode_decoder, default='', help='template to construct the URL')
+        self.parser.add_argument("-U", "--url-template", type=base.unicode_decoder, default='', help='template to construct the URL')
         self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, default='', help='subject of the invitation email (default: generic subject)')
         self.parser.add_argument("-b", "--body", type=base.unicode_decoder, default='', help='body of the invitation email (default: generic body)')
 
     def start(self):
-        common.checkURI(self.args)
         email = self.args.email[0] if self.args.email else None
         emails_extra = self.args.email[1:]
 
@@ -399,7 +392,7 @@
             self.args.name,
             self.args.host_name,
             self.args.lang,
-            self.args.url,
+            self.args.url_template,
             self.args.subject,
             self.args.body,
             self.args.profile,
--- a/frontends/src/jp/cmd_forums.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/cmd_forums.py	Wed Mar 21 19:13:22 2018 +0100
@@ -35,12 +35,11 @@
     use_items=False
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, use_verbose=True, help=_(u'edit forums'))
+        base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, use_draft=True, use_verbose=True, help=_(u'edit forums'))
         common.BaseEdit.__init__(self, self.host, FORUMS_TMP_DIR)
         self.need_loop=True
 
     def add_parser_options(self):
-        common.BaseEdit.add_parser_options(self)
         self.parser.add_argument("-k", "--key", type=base.unicode_decoder, default=u'',
                                  help=_(u"forum key (DEFAULT: default forums)"))
 
@@ -84,7 +83,6 @@
                          exit_code=C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.forumsGet(
             self.args.service,
             self.args.node,
@@ -146,7 +144,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.forumsGet(
             self.args.service,
             self.args.node,
--- a/frontends/src/jp/cmd_merge_request.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/cmd_merge_request.py	Wed Mar 21 19:13:22 2018 +0100
@@ -20,7 +20,6 @@
 
 import base
 from sat.core.i18n import _
-from sat_frontends.jp import common
 from sat_frontends.jp.constants import Const as C
 from functools import partial
 import os.path
@@ -46,7 +45,6 @@
         self.host.quit(C.EXIT_OK)
 
     def start(self):
-        common.checkURI(self.args)
         repository = os.path.expanduser(os.path.abspath(self.args.repository))
         extra = {'update': 'true'} if self.args.item else {}
         self.host.bridge.mergeRequestSet(
--- a/frontends/src/jp/cmd_pubsub.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/cmd_pubsub.py	Wed Mar 21 19:13:22 2018 +0100
@@ -46,7 +46,7 @@
 class NodeInfo(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'info', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, help=_(u'retrieve node configuration'))
+        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):
@@ -71,7 +71,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psNodeConfigurationGet(
             self.args.service,
             self.args.node,
@@ -83,7 +82,7 @@
 class NodeCreate(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'create', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, use_verbose=True, help=_(u'create a node'))
+        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):
@@ -123,7 +122,7 @@
 class NodeDelete(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'delete', use_pubsub_node_req=True, help=_(u'delete a node'))
+        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):
@@ -134,7 +133,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         if not self.args.force:
             if not self.args.service:
                 message = _(u"Are you sure to delete pep node [{node_id}] ?").format(
@@ -161,7 +159,7 @@
 class NodeSet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, use_verbose=True, help=_(u'set node configuration'))
+        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):
@@ -184,7 +182,6 @@
             return k
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psNodeConfigurationSet(
             self.args.service,
             self.args.node,
@@ -197,7 +194,7 @@
 class NodeAffiliationsGet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, help=_(u'retrieve node affiliations (for node owner)'))
+        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):
@@ -213,7 +210,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psNodeAffiliationsGet(
             self.args.service,
             self.args.node,
@@ -225,7 +221,7 @@
 class NodeAffiliationsSet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', use_pubsub_node_req=True, use_verbose=True, help=_(u'set affiliations (for node owner)'))
+        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):
@@ -251,7 +247,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         affiliations = dict(self.args.affiliations)
         self.host.bridge.psNodeAffiliationsSet(
             self.args.service,
@@ -272,7 +267,7 @@
 class NodeSubscriptionsGet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_DICT, use_pubsub_node_req=True, help=_(u'retrieve node subscriptions (for node owner)'))
+        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):
@@ -288,7 +283,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psNodeSubscriptionsGet(
             self.args.service,
             self.args.node,
@@ -320,7 +314,7 @@
 class NodeSubscriptionsSet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', use_pubsub_node_req=True, use_verbose=True, help=_(u'set/modify subscriptions (for node owner)'))
+        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):
@@ -347,7 +341,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psNodeSubscriptionsSet(
             self.args.service,
             self.args.node,
@@ -367,7 +360,7 @@
 class NodeSchemaSet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', use_pubsub_node_req=True, use_verbose=True, help=_(u'set/replace a schema'))
+        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):
@@ -393,12 +386,12 @@
     use_items=False
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'edit', use_pubsub_node_req=True, use_verbose=True, help=_(u'edit a schema'))
+        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):
-        common.BaseEdit.add_parser_options(self)
+        pass
 
     def psSchemaSetCb(self):
         self.disp(_(u'schema has been set'), 1)
@@ -431,7 +424,6 @@
         self.runEditor("pubsub_schema_editor_args", content_file_path, content_file_obj)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psSchemaGet(
             self.args.service,
             self.args.node,
@@ -445,7 +437,7 @@
 class NodeSchemaGet(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_XML, use_pubsub_node_req=True, use_verbose=True, help=_(u'get schema'))
+        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):
@@ -459,7 +451,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psSchemaGet(
             self.args.service,
             self.args.node,
@@ -487,7 +478,7 @@
 class Set(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'set', use_pubsub_node_req=True, help=_(u'publish a new item or update an existing one'))
+        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):
@@ -506,7 +497,6 @@
         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)
-        common.checkURI(self.args)
         try:
             element = etree.parse(sys.stdin).getroot()
         except Exception as e:
@@ -532,12 +522,10 @@
 class Get(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'get', use_output=C.OUTPUT_LIST_XML, use_pubsub_node_req=True, help=_(u'get pubsub item(s)'))
+        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("-i", "--item", type=base.unicode_decoder, action='append', default=[], dest='items',
-                                 help=_(u"item(s) id(s) to get (default: request all items)"))
         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)))
@@ -555,7 +543,6 @@
         self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
     def start(self):
-        common.checkURI(self.args)
         self.host.bridge.psItemsGet(
             self.args.service,
             self.args.node,
@@ -570,20 +557,18 @@
 class Delete(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'delete', use_pubsub_node_req=True, help=_(u'delete an item'))
+        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("item", nargs='?', type=base.unicode_decoder, help=_(u"item to delete"))
         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"))
+        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):
-        common.checkURI(self.args)
         if not self.args.item:
             self.parser.error(_(u"You need to specify an item to delete"))
         if not self.args.force:
@@ -607,12 +592,12 @@
 class Edit(base.CommandBase, common.BaseEdit):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'edit', use_verbose=True, use_pubsub_node_req=True, help=_(u'edit an existing or new pubsub item'))
+        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):
-        self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword"))
-        common.BaseEdit.add_parser_options(self)
+        pass
 
     def edit(self, content_file_path, content_file_obj):
         # we launch editor
@@ -644,15 +629,14 @@
         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.args.item)
-
+        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_node_req=True, use_verbose=True, help=_(u'subscribe to a node'))
+        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):
@@ -680,7 +664,7 @@
     # TODO: voir pourquoi NodeNotFound sur subscribe juste après unsubscribe
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'unsubscribe', use_pubsub_node_req=True, use_verbose=True, help=_(u'unsubscribe from a node'))
+        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):
@@ -758,7 +742,8 @@
     EXEC_ACTIONS = (u'exec', u'external')
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'search', use_output=C.OUTPUT_XML, use_pubsub=True, use_verbose=True, help=_(u'search items corresponding to filters'))
+        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
@@ -778,7 +763,6 @@
         return (type_, value)
 
     def add_parser_options(self):
-        self.parser.add_argument("-i", "--item", action="append", default=[], dest='items', type=base.unicode_decoder, help=_(u"item id(s)"))
         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=[],
@@ -1082,18 +1066,16 @@
         if self.args.filters is None:
             self.args.filters = []
         self.args.namespace = dict(self.args.namespace + [('pubsub', "http://jabber.org/protocol/pubsub")])
-        common.checkURI(self.args)
         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_node_req=True, help=_(u'build URI'))
+        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("-i", "--item", type=base.unicode_decoder, help=_(u"item to link"))
         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_):
@@ -1127,7 +1109,7 @@
 class HookCreate(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'create', use_pubsub_node_req=True, help=_(u'create a Pubsub hook'))
+        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):
@@ -1143,7 +1125,6 @@
                 self.parser.error(_(u"{path} is not a file").format(path=self.args.hook_arg))
 
     def start(self):
-        common.checkURI(self.args)
         self.checkArgs(self)
         self.host.bridge.psHookAdd(
             self.args.service,
@@ -1161,7 +1142,7 @@
 class HookDelete(base.CommandBase):
 
     def __init__(self, host):
-        base.CommandBase.__init__(self, host, 'delete', use_pubsub_node_req=True, help=_(u'delete a Pubsub hook'))
+        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):
@@ -1174,7 +1155,6 @@
         self.host.quit()
 
     def start(self):
-        common.checkURI(self.args)
         HookCreate.checkArgs(self)
         self.host.bridge.psHookRemove(
             self.args.service,
--- a/frontends/src/jp/common.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/common.py	Wed Mar 21 19:13:22 2018 +0100
@@ -21,7 +21,6 @@
 from sat.core.i18n import _
 from sat.core import exceptions
 from sat.tools.common import regex
-from sat.tools.common import uri
 from sat.tools.common.ansi import ANSI as A
 from sat.tools import config
 from ConfigParser import NoSectionError, NoOptionError
@@ -105,40 +104,12 @@
         return []
 
 
-def checkURI(args):
-    """check if args.node is an URI
-
-    if a valid xmpp: URI is found, args.service, args.node and args.item will be set
-    """
-    # FIXME: Q&D way to handle xmpp: uris, a generic way is needed
-    #        and it should be merged with code in BaseEdit
-    if not args.service and args.node.startswith('xmpp:'):
-        try:
-            uri_data = uri.parseXMPPUri(args.node)
-        except ValueError:
-            pass
-        else:
-            if uri_data[u'type'] == 'pubsub':
-                args.service = uri_data[u'path']
-                args.node = uri_data[u'node']
-                if u'item' in uri_data:
-                    try:
-                        item = getattr(uri_data, 'item')
-                    except AttributeError:
-                        pass
-                    else:
-                        if item is None:
-                            args.item = uri_data
-
-
 class BaseEdit(object):
     u"""base class for editing commands
 
     This class allows to edit file for PubSub or something else.
     It works with temporary files in SàT local_dir, in a "cat_dir" subdir
     """
-    # use_items(bool): True if items are used, will then add item related options
-    use_items=True
 
     def __init__(self, host, cat_dir, use_metadata=False):
         """
@@ -154,12 +125,6 @@
         self.cat_dir_str = cat_dir.encode('utf-8')
         self.use_metadata = use_metadata
 
-    def add_parser_options(self):
-        if self.use_items:
-            group = self.parser.add_mutually_exclusive_group()
-            group.add_argument("--force-item", action='store_true', help=_(u"don't use magic and take item argument as an actual item"))
-            group.add_argument("--last-item", action='store_true', help=_(u"take last item instead of creating a new one if no item id is found"))
-
     def secureUnlink(self, path):
         """Unlink given path after keeping it for a while
 
@@ -365,145 +330,73 @@
         """return suffix used for content file"""
         return u'xml'
 
-    def getItemPath(self, item):
+    def getItemPath(self):
         """retrieve item path (i.e. service and node) from item argument
 
         This method is obviously only useful for edition of PubSub based features
-        service, node and item must be named like this in args
-        @param item(unicode): item to get or url or magic keyword
-            item argument can be used to specify :
-                - HTTP(S) URL
-                - XMPP URL
-                - keyword, which can be:
-                    - new: create new item
-                    - last: retrieve last published item
-                    - current: continue current local draft
-                - file path
-                - item id
         """
-        force_item = self.args.force_item
-        if force_item and not item:
-            self.parser.error(_(u"an item id must be specified if you use --force-item"))
-        command = item.lower()
-        pubsub_service = self.args.service
-        pubsub_node = self.args.node
-        pubsub_item = None
+        service = self.args.service
+        node = self.args.node
+        item = self.args.item
+        last_item = self.args.last_item
 
-        if not force_item and command not in ('new', 'last', 'current'):
-            # we have probably an URL, we try to parse it
-            import urlparse
-            url = self.args.item
-            parsed_url = urlparse.urlsplit(url.encode('utf-8'))
-            if parsed_url.scheme.startswith('http'):
-                self.disp(u"{} URL found, trying to find associated xmpp: URI".format(parsed_url.scheme.upper()),1)
-                # HTTP URL, we try to find xmpp: links
-                try:
-                    from lxml import etree
-                except ImportError:
-                    self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
-                    self.host.quit(1)
-                import urllib2
-                parser = etree.HTMLParser()
-                try:
-                    root = etree.parse(urllib2.urlopen(url), parser)
-                except etree.XMLSyntaxError as e:
-                    self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
-                    links = []
-                else:
-                    links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
-                if not links:
-                    self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
-                    self.host.quit(1)
-                url = links[0].get('href')
-                parsed_url = urlparse.urlsplit(url)
-
-            if parsed_url.scheme == 'xmpp':
-                if self.args.service or self.args.node:
-                    self.parser.error(_(u"You can't use URI and --service or --node at the same time"))
-
-                self.disp(u"XMPP URI used: {}".format(url),2)
-                # XXX: if we have not xmpp: URI here, we'll take the data as a file path
-                pubsub_service = parsed_url.path.decode('utf-8')
-                pubsub_data = urlparse.parse_qs(parsed_url.query)
-                try:
-                    pubsub_node = pubsub_data['node'][0].decode('utf-8')
-                except KeyError:
-                    self.disp(u'No node found in xmpp: URI, can\'t retrieve item', error=True)
-                    self.host.quit(1)
-                pubsub_item = pubsub_data.get('item',[None])[0]
-                if pubsub_item is not None:
-                    pubsub_item = pubsub_item.decode('utf-8')
-                if pubsub_item is None and self.args.last_item:
-                    command = 'last'
-                elif pubsub_item is not None:
-                    command = 'edit' # XXX: edit command is only used internaly, it similar to last, but with the item given in the URL
-                else:
-                    command = 'new'
-
-        if self.args.last_item:
-            if pubsub_item is None:
-                command = 'last'
-            elif command != 'last':
-                self.parser.error(_(u"--last-item can't be used with a specified item"))
-
-        if not force_item and command in ('new', 'last', 'edit'):
+        if self.args.current:
+            # user wants to continue current draft
+            content_file_path = self.getCurrentFile(self.profile)
+            self.disp(u'Continuing edition of current draft', 2)
+            content_file_obj = open(content_file_path, 'r+b')
+            # we seek at the end of file in case of an item already exist
+            # this will write content of the existing item at the end of the draft.
+            # This way no data should be lost.
+            content_file_obj.seek(0, os.SEEK_END)
+        elif self.args.draft_path:
+            # there is an existing draft that we use
+            content_file_path = os.path.expanduser(self.args.item)
+            content_file_obj = open(content_file_path, 'r+b')
+            # we seek at the end for the same reason as above
+            content_file_obj.seek(0, os.SEEK_END)
+        else:
             # we need a temporary file
             content_file_obj, content_file_path = self.getTmpFile()
-            if command == 'new':
-                self.disp(u'Editing a new item', 2)
+
+        if item or last_item:
+            self.disp(u'Editing requested published item', 2)
+            try:
                 if self.use_metadata:
+                    content, metadata, item = self.getItemData(service, node, item)
+                else:
+                    content, item = self.getItemData(service, node, item)
+            except Exception as e:
+                # FIXME: ugly but we have not good may to check errors in bridge
+                if u'item-not-found' in unicode(e):
+                    # item doesn't exist, we create a new one with requested id
                     metadata = None
-            elif command in ('last', 'edit'):
-                self.disp(u'Editing requested published item', 2)
-                try:
-                    if self.use_metadata:
-                        content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item)
+                    if last_item:
+                        self.disp(_(u'no item found at all, we create a new one'), 2)
                     else:
-                        content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item)
-                except Exception as e:
-                    self.disp(u"Error while retrieving last item: {}".format(e))
-                    self.host.quit(1)
+                        self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=item), 2)
+                    content_file_obj.seek(0)
+                else:
+                    self.disp(u"Error while retrieving item: {}".format(e))
+                    self.host.quit(C.EXIT_ERROR)
+            else:
+                # item exists, we write content
+                if content_file_obj.tell() != 0:
+                    # we already have a draft,
+                    # we copy item content after it and add an indicator
+                    content_file_obj.write('\n*****\n')
                 content_file_obj.write(content.encode('utf-8'))
                 content_file_obj.seek(0)
+                self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=item), 2)
         else:
+            self.disp(u'Editing a new item', 2)
             if self.use_metadata:
                 metadata = None
-            if not force_item and command == 'current':
-                # user wants to continue current draft
-                content_file_path = self.getCurrentFile(self.profile)
-                self.disp(u'Continuing edition of current draft', 2)
-                content_file_obj = open(content_file_path, 'r+b')
-            elif not force_item and os.path.isfile(self.args.item):
-                # there is an existing draft that we use
-                content_file_path = os.path.expanduser(self.args.item)
-                content_file_obj = open(content_file_path, 'r+b')
-            else:
-                # last chance, it should be an item
-                content_file_obj, content_file_path = self.getTmpFile()
-                pubsub_item = self.args.item
-
-                try:
-                    # we try to get existing item
-                    if self.use_metadata:
-                        content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item)
-                    else:
-                        content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item)
-                except Exception as e:
-                    # FIXME: ugly but we have not good may to check errors in bridge
-                    if u'item-not-found' in unicode(e):
-                        # item doesn't exist, we create a new one with requested id
-                        metadata = None
-                        self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=pubsub_item), 2)
-                else:
-                    # item exists, we write content if content file
-                    content_file_obj.write(content.encode('utf-8'))
-                    content_file_obj.seek(0)
-                    self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=pubsub_item), 2)
 
         if self.use_metadata:
-            return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata
+            return service, node, item, content_file_path, content_file_obj, metadata
         else:
-            return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj
+            return service, node, item, content_file_path, content_file_obj
 
 
 class Table(object):
--- a/frontends/src/jp/constants.py	Wed Mar 21 19:07:06 2018 +0100
+++ b/frontends/src/jp/constants.py	Wed Mar 21 19:13:22 2018 +0100
@@ -36,6 +36,12 @@
     OUTPUT_LIST_XML = u'list_xml' # list of XML nodes (as unicode strings)
     OUTPUT_TYPES = (OUTPUT_TEXT, OUTPUT_DICT, OUTPUT_LIST, OUTPUT_LIST_DICT, OUTPUT_DICT_DICT, OUTPUT_COMPLEX, OUTPUT_XML, OUTPUT_LIST_XML)
 
+    # Pubsub options flags
+    SERVICE = u'service'  # service required
+    NODE = u'node'  # node required
+    SINGLE_ITEM = u'single_item'  # only one item is allowed
+    MULTI_ITEMS = u'multi_items'  # multiple items are allowed
+
     # ANSI
     A_HEADER = A.BOLD + A.FG_YELLOW
     A_SUBHEADER = A.BOLD + A.FG_RED