diff sat_frontends/jp/cmd_file.py @ 3040:fee60f17ebac

jp: jp asyncio port: /!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\ This patch implements the port of jp to asyncio, so it is now correctly using the bridge asynchronously, and it can be used with bridges like `pb`. This also simplify the code, notably for things which were previously implemented with many callbacks (like pagination with RSM). During the process, some behaviours have been modified/fixed, in jp and backends, check diff for details.
author Goffi <goffi@goffi.org>
date Wed, 25 Sep 2019 08:56:41 +0200
parents ab2696e34d29
children e189ceca7e8b
line wrap: on
line diff
--- a/sat_frontends/jp/cmd_file.py	Wed Sep 25 08:53:38 2019 +0200
+++ b/sat_frontends/jp/cmd_file.py	Wed Sep 25 08:56:41 2019 +0200
@@ -31,7 +31,6 @@
 from sat.tools.common.ansi import ANSI as A
 import tempfile
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
-from functools import partial
 import json
 
 __commands__ = ["File"]
@@ -46,7 +45,6 @@
             use_verbose=True,
             help=_("send a file to a contact"),
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -75,23 +73,19 @@
             help=("name to use (DEFAULT: use source file name)"),
         )
 
-    def start(self):
-        """Send files to jabber contact"""
-        self.send_files()
-
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File copy started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File sent successfully"), 2)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         if error_msg == C.PROGRESS_ERROR_DECLINED:
             self.disp(_("The file has been refused by your contact"))
         else:
             self.disp(_("Error while sending file: {}").format(error_msg), error=True)
 
-    def gotId(self, data, file_):
+    async def gotId(self, data, file_):
         """Called when a progress id has been received
 
         @param pid(unicode): progress id
@@ -100,7 +94,7 @@
         # FIXME: this show progress only for last progress_id
         self.disp(_("File request sent to {jid}".format(jid=self.full_dest_jid)), 1)
         try:
-            self.progress_id = data["progress"]
+            await self.set_progress_id(data["progress"])
         except KeyError:
             # TODO: if 'xmlui' key is present, manage xmlui message display
             self.disp(
@@ -108,27 +102,18 @@
             )
             self.host.quit(2)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to send a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def send_files(self):
+    async def start(self):
         for file_ in self.args.files:
             if not os.path.exists(file_):
-                self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
-                self.host.quit(1)
+                self.disp(_(f"file {file_!r} doesn't exist!"), error=True)
+                self.host.quit(C.EXIT_BAD_ARG)
             if not self.args.bz2 and os.path.isdir(file_):
                 self.disp(
-                    _(
-                        "[{}] is a dir ! Please send files inside or use compression"
-                    ).format(file_)
+                    _(f"{file_!r} is a dir! Please send files inside or use compression")
                 )
-                self.host.quit(1)
+                self.host.quit(C.EXIT_BAD_ARG)
 
-        self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
         extra = {}
         if self.args.path:
             extra["path"] = self.args.path
@@ -152,29 +137,37 @@
                 bz2.close()
                 self.disp(_("Done !"), 1)
 
-                self.host.bridge.fileSend(
-                    self.full_dest_jid,
-                    buf.name,
-                    self.args.name or archive_name,
-                    "",
-                    extra,
-                    self.profile,
-                    callback=lambda pid, file_=buf.name: self.gotId(pid, file_),
-                    errback=self.error,
-                )
+                try:
+                    send_data = await self.host.bridge.fileSend(
+                        self.full_dest_jid,
+                        buf.name,
+                        self.args.name or archive_name,
+                        "",
+                        extra,
+                        self.profile,
+                    )
+                except Exception as e:
+                    self.disp(f"can't send file: {e}", error=True)
+                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+                else:
+                    await self.gotId(send_data, file_)
         else:
             for file_ in self.args.files:
                 path = os.path.abspath(file_)
-                self.host.bridge.fileSend(
-                    self.full_dest_jid,
-                    path,
-                    self.args.name,
-                    "",
-                    extra,
-                    self.profile,
-                    callback=lambda pid, file_=file_: self.gotId(pid, file_),
-                    errback=self.error,
-                )
+                try:
+                    send_data = await self.host.bridge.fileSend(
+                        self.full_dest_jid,
+                        path,
+                        self.args.name,
+                        "",
+                        extra,
+                        self.profile,
+                    )
+                except Exception as e:
+                    self.disp(f"can't send file {file_!r}: {e}", error=True)
+                    self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+                else:
+                    await self.gotId(send_data, file_)
 
 
 class Request(base.CommandBase):
@@ -186,7 +179,6 @@
             use_verbose=True,
             help=_("request a file from a contact"),
         )
-        self.need_loop = True
 
     @property
     def filename(self):
@@ -200,7 +192,8 @@
             "-D",
             "--dest",
             help=_(
-                "destination path where the file will be saved (default: [current_dir]/[name|hash])"
+                "destination path where the file will be saved (default: "
+                "[current_dir]/[name|hash])"
             ),
         )
         self.parser.add_argument(
@@ -238,36 +231,21 @@
             help=_("overwrite existing file without confirmation"),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File copy started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File received successfully"), 2)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         if error_msg == C.PROGRESS_ERROR_DECLINED:
             self.disp(_("The file request has been refused"))
         else:
             self.disp(_("Error while requesting file: {}").format(error_msg), error=True)
 
-    def gotId(self, progress_id):
-        """Called when a progress id has been received
-
-        @param progress_id(unicode): progress id
-        """
-        self.progress_id = progress_id
-
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to send a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         if not self.args.name and not self.args.hash:
             self.parser.error(_("at least one of --name or --hash must be provided"))
-        #  extra = dict(self.args.extra)
         if self.args.dest:
             path = os.path.abspath(os.path.expanduser(self.args.dest))
             if os.path.isdir(path):
@@ -276,35 +254,30 @@
             path = os.path.abspath(self.filename)
 
         if os.path.exists(path) and not self.args.force:
-            message = _("File {path} already exists! Do you want to overwrite?").format(
-                path=path
-            )
-            confirm = input("{} (y/N) ".format(message).encode("utf-8"))
-            if confirm not in ("y", "Y"):
-                self.disp(_("file request cancelled"))
-                self.host.quit(2)
+            message = _(f"File {path} already exists! Do you want to overwrite?")
+            await self.host.confirmOrQuit(message, _("file request cancelled"))
 
-        self.full_dest_jid = self.host.get_full_jid(self.args.jid)
+        self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
         extra = {}
         if self.args.path:
             extra["path"] = self.args.path
         if self.args.namespace:
             extra["namespace"] = self.args.namespace
-        self.host.bridge.fileJingleRequest(
-            self.full_dest_jid,
-            path,
-            self.args.name,
-            self.args.hash,
-            self.args.hash_algo if self.args.hash else "",
-            extra,
-            self.profile,
-            callback=self.gotId,
-            errback=partial(
-                self.errback,
-                msg=_("can't request file: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            progress_id = await self.host.bridge.fileJingleRequest(
+                self.full_dest_jid,
+                path,
+                self.args.name,
+                self.args.hash,
+                self.args.hash_algo if self.args.hash else "",
+                extra,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(msg=_(f"can't request file: {e}"), error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.set_progress_id(progress_id)
 
 
 class Receive(base.CommandAnswering):
@@ -322,89 +295,6 @@
             C.META_TYPE_OVERWRITE: self.onOverwriteAction,
         }
 
-    def onProgressStarted(self, metadata):
-        self.disp(_("File copy started"), 2)
-
-    def onProgressFinished(self, metadata):
-        self.disp(_("File received successfully"), 2)
-        if metadata.get("hash_verified", False):
-            try:
-                self.disp(
-                    _("hash checked: {algo}:{checksum}").format(
-                        algo=metadata["hash_algo"], checksum=metadata["hash"]
-                    ),
-                    1,
-                )
-            except KeyError:
-                self.disp(_("hash is checked but hash value is missing", 1), error=True)
-        else:
-            self.disp(_("hash can't be verified"), 1)
-
-    def onProgressError(self, error_msg):
-        self.disp(_("Error while receiving file: {}").format(error_msg), error=True)
-
-    def getXmluiId(self, action_data):
-        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
-        #        should be available in the futur
-        # TODO: XMLUI module
-        try:
-            xml_ui = action_data["xmlui"]
-        except KeyError:
-            self.disp(_("Action has no XMLUI"), 1)
-        else:
-            ui = ET.fromstring(xml_ui.encode("utf-8"))
-            xmlui_id = ui.get("submit")
-            if not xmlui_id:
-                self.disp(_("Invalid XMLUI received"), error=True)
-            return xmlui_id
-
-    def onFileAction(self, action_data, action_id, security_limit, profile):
-        xmlui_id = self.getXmluiId(action_data)
-        if xmlui_id is None:
-            return self.host.quitFromSignal(1)
-        try:
-            from_jid = jid.JID(action_data["meta_from_jid"])
-        except KeyError:
-            self.disp(_("Ignoring action without from_jid data"), 1)
-            return
-        try:
-            progress_id = action_data["meta_progress_id"]
-        except KeyError:
-            self.disp(_("ignoring action without progress id"), 1)
-            return
-
-        if not self.bare_jids or from_jid.bare in self.bare_jids:
-            if self._overwrite_refused:
-                self.disp(_("File refused because overwrite is needed"), error=True)
-                self.host.bridge.launchAction(
-                    xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile
-                )
-                return self.host.quitFromSignal(2)
-            self.progress_id = progress_id
-            xmlui_data = {"path": self.path}
-            self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
-
-    def onOverwriteAction(self, action_data, action_id, security_limit, profile):
-        xmlui_id = self.getXmluiId(action_data)
-        if xmlui_id is None:
-            return self.host.quitFromSignal(1)
-        try:
-            progress_id = action_data["meta_progress_id"]
-        except KeyError:
-            self.disp(_("ignoring action without progress id"), 1)
-            return
-        self.disp(_("Overwriting needed"), 1)
-
-        if progress_id == self.progress_id:
-            if self.args.force:
-                self.disp(_("Overwrite accepted"), 2)
-            else:
-                self.disp(_("Refused to overwrite"), 2)
-                self._overwrite_refused = True
-
-            xmlui_data = {"answer": C.boolConst(self.args.force)}
-            self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
-
     def add_parser_options(self):
         self.parser.add_argument(
             "jids",
@@ -432,15 +322,95 @@
             help=_("destination path (default: working directory)"),
         )
 
-    def start(self):
+    async def onProgressStarted(self, metadata):
+        self.disp(_("File copy started"), 2)
+
+    async def onProgressFinished(self, metadata):
+        self.disp(_("File received successfully"), 2)
+        if metadata.get("hash_verified", False):
+            try:
+                self.disp(_(
+                    f"hash checked: {metadata['hash_algo']}:{metadata['hash']}"), 1)
+            except KeyError:
+                self.disp(_("hash is checked but hash value is missing", 1), error=True)
+        else:
+            self.disp(_("hash can't be verified"), 1)
+
+    async def onProgressError(self, e):
+        self.disp(_(f"Error while receiving file: {e}"), error=True)
+
+    def getXmluiId(self, action_data):
+        # FIXME: we temporarily use ElementTree, but a real XMLUI managing module
+        #        should be available in the futur
+        # TODO: XMLUI module
+        try:
+            xml_ui = action_data["xmlui"]
+        except KeyError:
+            self.disp(_("Action has no XMLUI"), 1)
+        else:
+            ui = ET.fromstring(xml_ui.encode("utf-8"))
+            xmlui_id = ui.get("submit")
+            if not xmlui_id:
+                self.disp(_("Invalid XMLUI received"), error=True)
+            return xmlui_id
+
+    async def onFileAction(self, action_data, action_id, security_limit, profile):
+        xmlui_id = self.getXmluiId(action_data)
+        if xmlui_id is None:
+            return self.host.quitFromSignal(1)
+        try:
+            from_jid = jid.JID(action_data["meta_from_jid"])
+        except KeyError:
+            self.disp(_("Ignoring action without from_jid data"), 1)
+            return
+        try:
+            progress_id = action_data["meta_progress_id"]
+        except KeyError:
+            self.disp(_("ignoring action without progress id"), 1)
+            return
+
+        if not self.bare_jids or from_jid.bare in self.bare_jids:
+            if self._overwrite_refused:
+                self.disp(_("File refused because overwrite is needed"), error=True)
+                await self.host.bridge.launchAction(
+                    xmlui_id, {"cancelled": C.BOOL_TRUE}, profile_key=profile
+                )
+                return self.host.quitFromSignal(2)
+            await self.set_progress_id(progress_id)
+            xmlui_data = {"path": self.path}
+            await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
+
+    async def onOverwriteAction(self, action_data, action_id, security_limit, profile):
+        xmlui_id = self.getXmluiId(action_data)
+        if xmlui_id is None:
+            return self.host.quitFromSignal(1)
+        try:
+            progress_id = action_data["meta_progress_id"]
+        except KeyError:
+            self.disp(_("ignoring action without progress id"), 1)
+            return
+        self.disp(_("Overwriting needed"), 1)
+
+        if progress_id == self.progress_id:
+            if self.args.force:
+                self.disp(_("Overwrite accepted"), 2)
+            else:
+                self.disp(_("Refused to overwrite"), 2)
+                self._overwrite_refused = True
+
+            xmlui_data = {"answer": C.boolConst(self.args.force)}
+            await self.host.bridge.launchAction(xmlui_id, xmlui_data, profile_key=profile)
+
+    async def start(self):
         self.bare_jids = [jid.JID(jid_).bare for jid_ in self.args.jids]
         self.path = os.path.abspath(self.args.path)
         if not os.path.isdir(self.path):
             self.disp(_("Given path is not a directory !", error=True))
-            self.host.quit(2)
+            self.host.quit(C.EXIT_BAD_ARG)
         if self.args.multiple:
             self.host.quit_on_progress_end = False
         self.disp(_("waiting for incoming file request"), 2)
+        await self.start_answering()
 
 
 class Upload(base.CommandBase):
@@ -448,7 +418,6 @@
         super(Upload, self).__init__(
             host, "upload", use_progress=True, use_verbose=True, help=_("upload a file")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument("file", type=str, help=_("file to upload"))
@@ -463,10 +432,10 @@
             help=_("ignore invalide TLS certificate"),
         )
 
-    def onProgressStarted(self, metadata):
+    async def onProgressStarted(self, metadata):
         self.disp(_("File upload started"), 2)
 
-    def onProgressFinished(self, metadata):
+    async def onProgressFinished(self, metadata):
         self.disp(_("File uploaded successfully"), 2)
         try:
             url = metadata["url"]
@@ -477,55 +446,54 @@
             # XXX: url is display alone on a line to make parsing easier
             self.disp(url)
 
-    def onProgressError(self, error_msg):
+    async def onProgressError(self, error_msg):
         self.disp(_("Error while uploading file: {}").format(error_msg), error=True)
 
-    def gotId(self, data, file_):
+    async def gotId(self, data, file_):
         """Called when a progress id has been received
 
         @param pid(unicode): progress id
         @param file_(str): file path
         """
         try:
-            self.progress_id = data["progress"]
+            await self.set_progress_id(data["progress"])
         except KeyError:
             # TODO: if 'xmlui' key is present, manage xmlui message display
             self.disp(_("Can't upload file"), error=True)
-            self.host.quit(2)
+            self.host.quit(C.EXIT_ERROR)
 
-    def error(self, failure):
-        self.disp(
-            _("Error while trying to upload a file: {reason}").format(reason=failure),
-            error=True,
-        )
-        self.host.quit(1)
-
-    def start(self):
+    async def start(self):
         file_ = self.args.file
         if not os.path.exists(file_):
-            self.disp(_("file [{}] doesn't exist !").format(file_), error=True)
-            self.host.quit(1)
+            self.disp(_(f"file {file_!r} doesn't exist !"), error=True)
+            self.host.quit(C.EXIT_BAD_ARG)
         if os.path.isdir(file_):
-            self.disp(_("[{}] is a dir! Can't upload a dir").format(file_))
-            self.host.quit(1)
+            self.disp(_(f"{file_!r} is a dir! Can't upload a dir"))
+            self.host.quit(C.EXIT_BAD_ARG)
 
-        self.full_dest_jid = (
-            self.host.get_full_jid(self.args.jid) if self.args.jid is not None else ""
-        )
+        if self.args.jid is None:
+            self.full_dest_jid = None
+        else:
+            self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
+
         options = {}
         if self.args.ignore_tls_errors:
             options["ignore_tls_errors"] = C.BOOL_TRUE
 
         path = os.path.abspath(file_)
-        self.host.bridge.fileUpload(
-            path,
-            "",
-            self.full_dest_jid,
-            options,
-            self.profile,
-            callback=lambda pid, file_=file_: self.gotId(pid, file_),
-            errback=self.error,
-        )
+        try:
+            upload_data = await self.host.bridge.fileUpload(
+                path,
+                "",
+                self.full_dest_jid,
+                options,
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't while trying to upload a file: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.gotId(upload_data, file_)
 
 
 class ShareList(base.CommandBase):
@@ -539,7 +507,6 @@
             help=_("retrieve files shared by an entity"),
             use_verbose=True,
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -555,12 +522,6 @@
             help=_("jid of sharing entity (nothing to check our own jid)"),
         )
 
-    def file_gen(self, files_data):
-        for file_data in files_data:
-            yield file_data["name"]
-            yield file_data.get("size", "")
-            yield file_data.get("hash", "")
-
     def _name_filter(self, name, row):
         if row.type == C.FILE_TYPE_DIRECTORY:
             return A.color(C.A_DIRECTORY, name)
@@ -588,38 +549,37 @@
         files_data.sort(key=lambda d: d["name"].lower())
         show_header = False
         if self.verbosity == 0:
-            headers = ("name", "type")
+            keys = headers = ("name", "type")
         elif self.verbosity == 1:
-            headers = ("name", "type", "size")
+            keys = headers = ("name", "type", "size")
         elif self.verbosity > 1:
             show_header = True
+            keys = ("name", "type", "size", "file_hash")
             headers = ("name", "type", "size", "hash")
         table = common.Table.fromDict(
             self.host,
             files_data,
-            headers,
+            keys=keys,
+            headers=headers,
             filters={"name": self._name_filter, "size": self._size_filter},
-            defaults={"size": "", "hash": ""},
+            defaults={"size": "", "file_hash": ""},
         )
         table.display_blank(show_header=show_header, hide_cols=["type"])
 
-    def _FISListCb(self, files_data):
-        self.output(files_data)
-        self.host.quit()
+    async def start(self):
+        try:
+            files_data = await self.host.bridge.FISList(
+                self.args.jid,
+                self.args.path,
+                {},
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't retrieve shared files: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
 
-    def start(self):
-        self.host.bridge.FISList(
-            self.args.jid,
-            self.args.path,
-            {},
-            self.profile,
-            callback=self._FISListCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't retrieve shared files: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        await self.output(files_data)
+        self.host.quit()
 
 
 class SharePath(base.CommandBase):
@@ -627,7 +587,6 @@
         super(SharePath, self).__init__(
             host, "path", help=_("share a file or directory"), use_verbose=True
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -640,6 +599,7 @@
         perm_group.add_argument(
             "-j",
             "--jid",
+            metavar="JID",
             action="append",
             dest="jids",
             default=[],
@@ -649,7 +609,8 @@
             "--public",
             action="store_true",
             help=_(
-                "share publicly the file(s) (/!\\ *everybody* will be able to access them)"
+                r"share publicly the file(s) (/!\ *everybody* will be able to access "
+                r"them)"
             ),
         )
         self.parser.add_argument(
@@ -657,13 +618,7 @@
             help=_("path to a file or directory to share"),
         )
 
-    def _FISSharePathCb(self, name):
-        self.disp(
-            _('{path} shared under the name "{name}"').format(path=self.path, name=name)
-        )
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         self.path = os.path.abspath(self.args.path)
         if self.args.public:
             access = {"read": {"type": "public"}}
@@ -673,18 +628,19 @@
                 access = {"read": {"type": "whitelist", "jids": jids}}
             else:
                 access = {}
-        self.host.bridge.FISSharePath(
-            self.args.name,
-            self.path,
-            json.dumps(access, ensure_ascii=False),
-            self.profile,
-            callback=self._FISSharePathCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't share path: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            name = await self.host.bridge.FISSharePath(
+                self.args.name,
+                self.path,
+                json.dumps(access, ensure_ascii=False),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't share path: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(_(f'{self.path} shared under the name "{name}"'))
+            self.host.quit()
 
 
 class ShareInvite(base.CommandBase):
@@ -692,7 +648,6 @@
         super(ShareInvite, self).__init__(
             host, "invite", help=_("send invitation for a shared repository")
         )
-        self.need_loop = True
 
     def add_parser_options(self):
         self.parser.add_argument(
@@ -733,13 +688,7 @@
             help=_("jid of the person to invite"),
         )
 
-    def _FISInviteCb(self):
-        self.disp(
-            _('invitation sent to {entity}').format(entity=self.args.jid)
-        )
-        self.host.quit()
-
-    def start(self):
+    async def start(self):
         self.path = os.path.normpath(self.args.path) if self.args.path else ""
         extra = {}
         if self.args.thumbnail is not None:
@@ -747,22 +696,25 @@
                 self.parser.error(_("only http(s) links are allowed with --thumbnail"))
             else:
                 extra['thumb_url'] = self.args.thumbnail
-        self.host.bridge.FISInvite(
-            self.args.jid,
-            self.args.service,
-            self.args.type,
-            self.args.namespace,
-            self.path,
-            self.args.name,
-            data_format.serialise(extra),
-            self.profile,
-            callback=self._FISInviteCb,
-            errback=partial(
-                self.errback,
-                msg=_("can't send invitation: {}"),
-                exit_code=C.EXIT_BRIDGE_ERRBACK,
-            ),
-        )
+        try:
+            await self.host.bridge.FISInvite(
+                self.args.jid,
+                self.args.service,
+                self.args.type,
+                self.args.namespace,
+                self.path,
+                self.args.name,
+                data_format.serialise(extra),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"can't send invitation: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            self.disp(
+                _(f'invitation sent to {self.args.jid}')
+            )
+            self.host.quit()
 
 
 class Share(base.CommandBase):