changeset 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 08a3e34aead1
children 26a0af6e32c1
files sat/plugins/plugin_misc_file.py sat/plugins/plugin_xep_0234.py
diffstat 2 files changed, 101 insertions(+), 78 deletions(-) [+]
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
--- a/sat/plugins/plugin_xep_0234.py	Thu Nov 12 14:53:15 2020 +0100
+++ b/sat/plugins/plugin_xep_0234.py	Thu Nov 12 14:53:15 2020 +0100
@@ -16,18 +16,10 @@
 # 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/>.
 
-from sat.core.i18n import _
-from sat.core.constants import Const as C
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-from sat.core import exceptions
-from wokkel import disco, iwokkel
+import os.path
+import mimetypes
+from collections import namedtuple
 from zope.interface import implementer
-from sat.tools import utils
-from sat.tools import stream
-from sat.tools.common import date_utils
-import os.path
 from twisted.words.xish import domish
 from twisted.words.protocols.jabber import jid
 from twisted.python import failure
@@ -35,10 +27,18 @@
 from twisted.internet import defer
 from twisted.internet import reactor
 from twisted.internet import error as internet_error
-from collections import namedtuple
+from wokkel import disco, iwokkel
+from sat.core.i18n import _, D_
+from sat.core.constants import Const as C
+from sat.core.log import getLogger
+from sat.core import exceptions
+from sat.tools import utils
+from sat.tools import stream
+from sat.tools.common import date_utils
 from sat.tools.common import regex
-import mimetypes
+
 
+log = getLogger(__name__)
 
 NS_JINGLE_FT = "urn:xmpp:jingle:apps:file-transfer:5"
 
@@ -58,10 +58,13 @@
 Range = namedtuple("Range", ("offset", "length"))
 
 
-class XEP_0234(object):
+class XEP_0234:
     # TODO: assure everything is closed when file is sent or session terminate is received
-    # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
+    # TODO: call self._f.unregister when unloading order will be managing (i.e. when
+    #   dependencies will be unloaded at the end)
     Range = Range  # we copy the class here, so it can be used by other plugins
+    name = PLUGIN_INFO[C.PI_NAME]
+    human_name = D_("file transfer")
 
     def __init__(self, host):
         log.info(_("plugin Jingle File Transfer initialization"))
@@ -70,16 +73,14 @@
         self._j = host.plugins["XEP-0166"]  # shortcut to access jingle
         self._j.registerApplication(NS_JINGLE_FT, self)
         self._f = host.plugins["FILE"]
-        self._f.register(
-            NS_JINGLE_FT, self.fileJingleSend, priority=10000, method_name="Jingle"
-        )
+        self._f.register(self, priority=10000)
         self._hash = self.host.plugins["XEP-0300"]
         host.bridge.addMethod(
             "fileJingleSend",
             ".plugin",
             in_sign="ssssa{ss}s",
             out_sign="",
-            method=self._fileJingleSend,
+            method=self._fileSend,
             async_=True,
         )
         host.bridge.addMethod(
@@ -103,6 +104,13 @@
         """
         return "{}_{}".format(session["id"], content_name)
 
+    async def canHandleFileSend(self, client, peer_jid, filepath):
+        if peer_jid.resource:
+            return await self.host.hasFeature(client, NS_JINGLE_FT, peer_jid)
+        else:
+            # if we have a bare jid, Jingle Message Initiation will be tried
+            return True
+
     # generic methods
 
     def buildFileElement(
@@ -286,7 +294,7 @@
 
     # bridge methods
 
-    def _fileJingleSend(
+    def _fileSend(
         self,
         peer_jid,
         filepath,
@@ -296,17 +304,16 @@
         profile=C.PROF_KEY_NONE,
     ):
         client = self.host.getClient(profile)
-        return self.fileJingleSend(
+        return defer.ensureDeferred(self.fileSend(
             client,
             jid.JID(peer_jid),
             filepath,
             name or None,
             file_desc or None,
             extra or None,
-        )
+        ))
 
-    @defer.inlineCallbacks
-    def fileJingleSend(
+    async def fileSend(
         self, client, peer_jid, filepath, name, file_desc=None, extra=None
     ):
         """Send a file using jingle file transfer
@@ -322,7 +329,7 @@
             extra = {}
         if file_desc is not None:
             extra["file_desc"] = file_desc
-        yield self._j.initiate(
+        await self._j.initiate(
             client,
             peer_jid,
             [
@@ -338,8 +345,7 @@
                 }
             ],
         )
-        progress_id = yield progress_id_d
-        defer.returnValue(progress_id)
+        return await progress_id_d
 
     def _fileJingleRequest(
             self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,