comparison 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
comparison
equal deleted inserted replaced
3402:08a3e34aead1 3403:404d4b29de52
15 # GNU Affero General Public License for more details. 15 # GNU Affero General Public License for more details.
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 import os
21 import os.path
22 from functools import partial
23 from twisted.internet import defer
24 from twisted.words.protocols.jabber import jid
20 from sat.core.i18n import _, D_ 25 from sat.core.i18n import _, D_
21 from sat.core.constants import Const as C 26 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 27 from sat.core.log import getLogger
23
24 log = getLogger(__name__)
25 from sat.core import exceptions 28 from sat.core import exceptions
26 from sat.tools import xml_tools 29 from sat.tools import xml_tools
27 from sat.tools import stream 30 from sat.tools import stream
28 from twisted.internet import defer 31 from sat.tools import utils
29 from twisted.words.protocols.jabber import jid 32
30 import os 33
31 import os.path 34 log = getLogger(__name__)
32 35
33 36
34 PLUGIN_INFO = { 37 PLUGIN_INFO = {
35 C.PI_NAME: "File Tansfer", 38 C.PI_NAME: "File Tansfer",
36 C.PI_IMPORT_NAME: "FILE", 39 C.PI_IMPORT_NAME: "FILE",
71 in_sign="ssssa{ss}s", 74 in_sign="ssssa{ss}s",
72 out_sign="a{ss}", 75 out_sign="a{ss}",
73 method=self._fileSend, 76 method=self._fileSend,
74 async_=True, 77 async_=True,
75 ) 78 )
76 self._file_callbacks = [] 79 self._file_managers = []
77 host.importMenu( 80 host.importMenu(
78 (D_("Action"), D_("send file")), 81 (D_("Action"), D_("send file")),
79 self._fileSendMenu, 82 self._fileSendMenu,
80 security_limit=10, 83 security_limit=10,
81 help_string=D_("Send a file"), 84 help_string=D_("Send a file"),
83 ) 86 )
84 87
85 def _fileSend(self, peer_jid_s, filepath, name="", file_desc="", extra=None, 88 def _fileSend(self, peer_jid_s, filepath, name="", file_desc="", extra=None,
86 profile=C.PROF_KEY_NONE): 89 profile=C.PROF_KEY_NONE):
87 client = self.host.getClient(profile) 90 client = self.host.getClient(profile)
88 return self.fileSend( 91 return defer.ensureDeferred(self.fileSend(
89 client, jid.JID(peer_jid_s), filepath, name or None, file_desc or None, extra 92 client, jid.JID(peer_jid_s), filepath, name or None, file_desc or None, extra
90 ) 93 ))
91 94
92 @defer.inlineCallbacks 95 async def fileSend(
93 def fileSend(
94 self, client, peer_jid, filepath, filename=None, file_desc=None, extra=None 96 self, client, peer_jid, filepath, filename=None, file_desc=None, extra=None
95 ): 97 ):
96 """Send a file using best available method 98 """Send a file using best available method
97 99
98 @param peer_jid(jid.JID): jid of the destinee 100 @param peer_jid(jid.JID): jid of the destinee
105 """ 107 """
106 if not os.path.isfile(filepath): 108 if not os.path.isfile(filepath):
107 raise exceptions.DataError("The given path doesn't link to a file") 109 raise exceptions.DataError("The given path doesn't link to a file")
108 if not filename: 110 if not filename:
109 filename = os.path.basename(filepath) or "_" 111 filename = os.path.basename(filepath) or "_"
110 for namespace, callback, priority, method_name in self._file_callbacks: 112 for manager, priority in self._file_managers:
111 has_feature = yield self.host.hasFeature(client, namespace, peer_jid) 113 if await utils.asDeferred(manager.canHandleFileSend,
112 if has_feature: 114 client, peer_jid, filepath):
115 try:
116 method_name = manager.name
117 except AttributeError:
118 method_name = manager.__class__.__name__
113 log.info( 119 log.info(
114 "{name} method will be used to send the file".format( 120 _("{name} method will be used to send the file").format(
115 name=method_name 121 name=method_name
116 ) 122 )
117 ) 123 )
118 progress_id = yield callback( 124 try:
119 client, peer_jid, filepath, filename, file_desc, extra 125 progress_id = await utils.asDeferred(
120 ) 126 manager.fileSend, client, peer_jid, filepath, filename, file_desc,
121 defer.returnValue({"progress": progress_id}) 127 extra
128 )
129 except Exception as e:
130 log.warning(
131 _("Can't send {filepath} to {peer_jid} with {method_name}: "
132 "{reason}").format(
133 filepath=filepath,
134 peer_jid=peer_jid,
135 method_name=method_name,
136 reason=e
137 )
138 )
139 continue
140 return {"progress": progress_id}
122 msg = "Can't find any method to send file to {jid}".format(jid=peer_jid.full()) 141 msg = "Can't find any method to send file to {jid}".format(jid=peer_jid.full())
123 log.warning(msg) 142 log.warning(msg)
124 defer.returnValue( 143 return {
125 { 144 "xmlui": xml_tools.note(
126 "xmlui": xml_tools.note( 145 "Can't transfer file", msg, C.XMLUI_DATA_LVL_WARNING
127 "Can't transfer file", msg, C.XMLUI_DATA_LVL_WARNING 146 ).toXml()
128 ).toXml() 147 }
129 } 148
130 ) 149 def _onFileChoosed(self, peer_jid, data, profile):
131 150 client = self.host.getClient(profile)
132 def _onFileChoosed(self, client, peer_jid, data):
133 cancelled = C.bool(data.get("cancelled", C.BOOL_FALSE)) 151 cancelled = C.bool(data.get("cancelled", C.BOOL_FALSE))
134 if cancelled: 152 if cancelled:
135 return 153 return
136 path = data["path"] 154 path = data["path"]
137 return self.fileSend(client, peer_jid, path) 155 return self.fileSend(client, peer_jid, path)
145 jid_ = jid.JID(data["jid"]) 163 jid_ = jid.JID(data["jid"])
146 except RuntimeError: 164 except RuntimeError:
147 raise exceptions.DataError(_("Invalid JID")) 165 raise exceptions.DataError(_("Invalid JID"))
148 166
149 file_choosed_id = self.host.registerCallback( 167 file_choosed_id = self.host.registerCallback(
150 lambda data, profile: self._onFileChoosed( 168 partial(self._onFileChoosed, jid_),
151 self.host.getClient(profile), jid_, data
152 ),
153 with_data=True, 169 with_data=True,
154 one_shot=True, 170 one_shot=True,
155 ) 171 )
156 xml_ui = xml_tools.XMLUI( 172 xml_ui = xml_tools.XMLUI(
157 C.XMLUI_DIALOG, 173 C.XMLUI_DIALOG,
163 submit_id=file_choosed_id, 179 submit_id=file_choosed_id,
164 ) 180 )
165 181
166 return {"xmlui": xml_ui.toXml()} 182 return {"xmlui": xml_ui.toXml()}
167 183
168 def register(self, namespace, callback, priority=0, method_name=None): 184 def register(self, manager, priority: int = 0) -> None:
169 """Register a fileSending method 185 """Register a fileSending manager
170 186
171 @param namespace(unicode): XEP namespace 187 @param manager: object implementing canHandleFileSend, and fileSend methods
172 @param callback(callable): method to call (must have the same signature as [fileSend]) 188 @param priority: pririoty of this manager, the higher available will be used
173 @param priority(int): pririoty of this method, the higher available will be used 189 """
174 @param method_name(unicode): short name for the method, namespace will be used if None 190 m_data = (manager, priority)
175 """ 191 if m_data in self._file_managers:
176 for data in self._file_callbacks: 192 raise exceptions.ConflictError(
177 if namespace == data[0]: 193 f"Manager {manager} is already registered"
178 raise exceptions.ConflictError( 194 )
179 "A method with this namespace is already registered" 195 if not hasattr(manager, "canHandleFileSend") or not hasattr(manager, "fileSend"):
180 ) 196 raise ValueError(
181 self._file_callbacks.append( 197 f'{manager} must have both "canHandleFileSend" and "fileSend" methods to '
182 (namespace, callback, priority, method_name or namespace) 198 'be registered')
183 ) 199 self._file_managers.append(m_data)
184 self._file_callbacks.sort(key=lambda data: data[2], reverse=True) 200 self._file_managers.sort(key=lambda m: m[1], reverse=True)
185 201
186 def unregister(self, namespace): 202 def unregister(self, manager):
187 for idx, data in enumerate(self._file_callbacks): 203 for idx, data in enumerate(self._file_managers):
188 if data[0] == namespace: 204 if data[0] == manager:
189 del [idx] 205 break
190 return 206 else:
191 raise exceptions.NotFound("The namespace to unregister doesn't exist") 207 raise exceptions.NotFound("The file manager {manager} is not registered")
208 del self._file_managers[idx]
192 209
193 # Dialogs with user 210 # Dialogs with user
194 # the overwrite check is done here 211 # the overwrite check is done here
195 212
196 def openFileWrite(self, client, file_path, transfer_data, file_data, stream_object): 213 def openFileWrite(self, client, file_path, transfer_data, file_data, stream_object):