diff sat/plugins/plugin_misc_file.py @ 3403:404d4b29de52

plugin file, XEP-0234: registering is now done by class + use of async: - instead of registering a callback, a file sending manager now register itself and must implement some well known method (`fileSend`, `canHandleFileSend`) and optionally a `name` attribute - `utils.asDeferred` is now used for callbacks, so all type of methods including coroutines can be used. - feature checking is now handled by `canHandleFileSend` method instead of simple namespace check, this allows to use a method when namespace can't be checked (this is the case when a file is sent to a bare jid with jingle)
author Goffi <goffi@goffi.org>
date Thu, 12 Nov 2020 14:53:15 +0100
parents f2bb57348587
children be6d91572633
line wrap: on
line diff
--- a/sat/plugins/plugin_misc_file.py	Thu Nov 12 14:53:15 2020 +0100
+++ b/sat/plugins/plugin_misc_file.py	Thu Nov 12 14:53:15 2020 +0100
@@ -17,18 +17,21 @@
 # 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 os
+import os.path
+from functools import partial
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
 from sat.core.i18n import _, D_
 from sat.core.constants import Const as C
 from sat.core.log import getLogger
-
-log = getLogger(__name__)
 from sat.core import exceptions
 from sat.tools import xml_tools
 from sat.tools import stream
-from twisted.internet import defer
-from twisted.words.protocols.jabber import jid
-import os
-import os.path
+from sat.tools import utils
+
+
+log = getLogger(__name__)
 
 
 PLUGIN_INFO = {
@@ -73,7 +76,7 @@
             method=self._fileSend,
             async_=True,
         )
-        self._file_callbacks = []
+        self._file_managers = []
         host.importMenu(
             (D_("Action"), D_("send file")),
             self._fileSendMenu,
@@ -85,12 +88,11 @@
     def _fileSend(self, peer_jid_s, filepath, name="", file_desc="", extra=None,
                   profile=C.PROF_KEY_NONE):
         client = self.host.getClient(profile)
-        return self.fileSend(
+        return defer.ensureDeferred(self.fileSend(
             client, jid.JID(peer_jid_s), filepath, name or None, file_desc or None, extra
-        )
+        ))
 
-    @defer.inlineCallbacks
-    def fileSend(
+    async def fileSend(
         self, client, peer_jid, filepath, filename=None, file_desc=None, extra=None
     ):
         """Send a file using best available method
@@ -107,29 +109,45 @@
             raise exceptions.DataError("The given path doesn't link to a file")
         if not filename:
             filename = os.path.basename(filepath) or "_"
-        for namespace, callback, priority, method_name in self._file_callbacks:
-            has_feature = yield self.host.hasFeature(client, namespace, peer_jid)
-            if has_feature:
+        for manager, priority in self._file_managers:
+            if await utils.asDeferred(manager.canHandleFileSend,
+                                      client, peer_jid, filepath):
+                try:
+                    method_name = manager.name
+                except AttributeError:
+                    method_name = manager.__class__.__name__
                 log.info(
-                    "{name} method will be used to send the file".format(
+                    _("{name} method will be used to send the file").format(
                         name=method_name
                     )
                 )
-                progress_id = yield callback(
-                    client, peer_jid, filepath, filename, file_desc, extra
-                )
-                defer.returnValue({"progress": progress_id})
+                try:
+                    progress_id = await utils.asDeferred(
+                        manager.fileSend, client, peer_jid, filepath, filename, file_desc,
+                        extra
+                    )
+                except Exception as e:
+                    log.warning(
+                        _("Can't send {filepath} to {peer_jid} with {method_name}: "
+                          "{reason}").format(
+                              filepath=filepath,
+                              peer_jid=peer_jid,
+                              method_name=method_name,
+                              reason=e
+                          )
+                    )
+                    continue
+                return {"progress": progress_id}
         msg = "Can't find any method to send file to {jid}".format(jid=peer_jid.full())
         log.warning(msg)
-        defer.returnValue(
-            {
-                "xmlui": xml_tools.note(
-                    "Can't transfer file", msg, C.XMLUI_DATA_LVL_WARNING
-                ).toXml()
-            }
-        )
+        return {
+            "xmlui": xml_tools.note(
+                "Can't transfer file", msg, C.XMLUI_DATA_LVL_WARNING
+            ).toXml()
+        }
 
-    def _onFileChoosed(self, client, peer_jid, data):
+    def _onFileChoosed(self, peer_jid, data, profile):
+        client = self.host.getClient(profile)
         cancelled = C.bool(data.get("cancelled", C.BOOL_FALSE))
         if cancelled:
             return
@@ -147,9 +165,7 @@
             raise exceptions.DataError(_("Invalid JID"))
 
         file_choosed_id = self.host.registerCallback(
-            lambda data, profile: self._onFileChoosed(
-                self.host.getClient(profile), jid_, data
-            ),
+            partial(self._onFileChoosed, jid_),
             with_data=True,
             one_shot=True,
         )
@@ -165,30 +181,31 @@
 
         return {"xmlui": xml_ui.toXml()}
 
-    def register(self, namespace, callback, priority=0, method_name=None):
-        """Register a fileSending method
+    def register(self, manager, priority: int = 0) -> None:
+        """Register a fileSending manager
 
-        @param namespace(unicode): XEP namespace
-        @param callback(callable): method to call (must have the same signature as [fileSend])
-        @param priority(int): pririoty of this method, the higher available will be used
-        @param method_name(unicode): short name for the method, namespace will be used if None
+        @param manager: object implementing canHandleFileSend, and fileSend methods
+        @param priority: pririoty of this manager, the higher available will be used
         """
-        for data in self._file_callbacks:
-            if namespace == data[0]:
-                raise exceptions.ConflictError(
-                    "A method with this namespace is already registered"
-                )
-        self._file_callbacks.append(
-            (namespace, callback, priority, method_name or namespace)
-        )
-        self._file_callbacks.sort(key=lambda data: data[2], reverse=True)
+        m_data = (manager, priority)
+        if m_data in self._file_managers:
+            raise exceptions.ConflictError(
+                f"Manager {manager} is already registered"
+            )
+        if not hasattr(manager, "canHandleFileSend") or not hasattr(manager, "fileSend"):
+            raise ValueError(
+                f'{manager} must have both "canHandleFileSend" and "fileSend" methods to '
+                'be registered')
+        self._file_managers.append(m_data)
+        self._file_managers.sort(key=lambda m: m[1], reverse=True)
 
-    def unregister(self, namespace):
-        for idx, data in enumerate(self._file_callbacks):
-            if data[0] == namespace:
-                del [idx]
-                return
-        raise exceptions.NotFound("The namespace to unregister doesn't exist")
+    def unregister(self, manager):
+        for idx, data in enumerate(self._file_managers):
+            if data[0] == manager:
+                break
+        else:
+            raise exceptions.NotFound("The file manager {manager} is not registered")
+        del self._file_managers[idx]
 
     # Dialogs with user
     # the overwrite check is done here