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)
This commit is contained in:
Goffi 2020-11-12 14:53:15 +01:00
commit 46c763cbf7
2 changed files with 102 additions and 79 deletions

View file

@ -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 @@ class FilePlugin:
method=self._fileSend,
async_=True,
)
self._file_callbacks = []
self._file_managers = []
host.importMenu(
(D_("Action"), D_("send file")),
self._fileSendMenu,
@ -85,12 +88,11 @@ class FilePlugin:
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 @@ class FilePlugin:
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 @@ class FilePlugin:
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 @@ class FilePlugin:
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

View file

@ -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
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
import mimetypes
from collections import namedtuple
from zope.interface import implementer
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid
from twisted.python import failure
@ -35,11 +27,19 @@ from twisted.words.protocols.jabber.xmlstream import XMPPHandler
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"
PLUGIN_INFO = {
@ -58,10 +58,13 @@ EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
"""
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 @@ class XEP_0234(object):
# bridge methods
def _fileJingleSend(
def _fileSend(
self,
peer_jid,
filepath,
@ -296,17 +304,16 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
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 @@ class XEP_0234(object):
}
],
)
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,