Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0363.py @ 3922:0ff265725489
plugin XEP-0447: handle attachment and download:
- plugin XEP-0447 can now be used in message attachments and to retrieve an attachment
- plugin attach: `attachment` being processed is added to `extra` so the handler can inspect it
- plugin attach: `size` is added to attachment
- plugin download: a whole attachment dict is now used in `download` and
`file_download`/`file_download_complete`. `download_uri` can be used as a shortcut when
just a URI is used. In addition to URI scheme handler, whole attachment handlers can now
be registered with `register_download_handler`
- plugin XEP-0363: `file_http_upload` `XEP-0363_upload_size` triggers have been renamed to
`XEP-0363_upload_pre_slot` and is now using a dict with arguments, allowing for the size
but also the filename to be modified, which is necessary for encryption (filename may
be hidden from URL this way).
- plugin XEP-0446: fix wrong element name
- plugin XEP-0447: source handler can now be registered (`url-data` is registered by
default)
- plugin XEP-0447: source parsing has been put in a separated `parse_sources_elt` method,
as it may be useful to do it independently (notably with XEP-0448)
- plugin XEP-0447: parse received message and complete attachments when suitable
- plugin XEP-0447: can now be used with message attachments
- plugin XEP-0447: can now be used with attachments download
- renamed `options` arguments to `extra` for consistency
- some style change (progressive move from legacy camelCase to PEP8 snake_case)
- some typing
rel 379
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 06 Oct 2022 16:02:05 +0200 |
parents | e84ffb48acd4 |
children | 412b99c29d83 |
comparison
equal
deleted
inserted
replaced
3921:cc2705225778 | 3922:0ff265725489 |
---|---|
14 # GNU Affero General Public License for more details. | 14 # GNU Affero General Public License for more details. |
15 | 15 |
16 # You should have received a copy of the GNU Affero General Public License | 16 # You should have received a copy of the GNU Affero General Public License |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 from dataclasses import dataclass | |
20 import mimetypes | |
19 import os.path | 21 import os.path |
20 import mimetypes | 22 from pathlib import Path |
21 from typing import NamedTuple, Callable, Optional | 23 from typing import Callable, NamedTuple, Optional, Tuple |
22 from dataclasses import dataclass | |
23 from urllib import parse | 24 from urllib import parse |
24 from wokkel import disco, iwokkel | 25 |
25 from zope.interface import implementer | |
26 from twisted.words.protocols.jabber import jid, xmlstream, error | |
27 from twisted.words.xish import domish | |
28 from twisted.internet import reactor | 26 from twisted.internet import reactor |
29 from twisted.internet import defer | 27 from twisted.internet import defer |
30 from twisted.web import client as http_client | 28 from twisted.web import client as http_client |
31 from twisted.web import http_headers | 29 from twisted.web import http_headers |
30 from twisted.words.protocols.jabber import error, jid, xmlstream | |
31 from twisted.words.xish import domish | |
32 from wokkel import disco, iwokkel | |
33 from zope.interface import implementer | |
34 | |
35 from sat.core import exceptions | |
36 from sat.core.constants import Const as C | |
37 from sat.core.core_types import SatXMPPEntity | |
32 from sat.core.i18n import _ | 38 from sat.core.i18n import _ |
39 from sat.core.log import getLogger | |
33 from sat.core.xmpp import SatXMPPComponent | 40 from sat.core.xmpp import SatXMPPComponent |
34 from sat.core.constants import Const as C | 41 from sat.tools import utils, web as sat_web |
35 from sat.core.log import getLogger | |
36 from sat.core import exceptions | |
37 from sat.tools import web as sat_web, utils | |
38 | 42 |
39 | 43 |
40 log = getLogger(__name__) | 44 log = getLogger(__name__) |
41 | 45 |
42 PLUGIN_INFO = { | 46 PLUGIN_INFO = { |
85 host.bridge.addMethod( | 89 host.bridge.addMethod( |
86 "fileHTTPUpload", | 90 "fileHTTPUpload", |
87 ".plugin", | 91 ".plugin", |
88 in_sign="sssbs", | 92 in_sign="sssbs", |
89 out_sign="", | 93 out_sign="", |
90 method=self._fileHTTPUpload, | 94 method=self._file_http_upload, |
91 ) | 95 ) |
92 host.bridge.addMethod( | 96 host.bridge.addMethod( |
93 "fileHTTPUploadGetSlot", | 97 "fileHTTPUploadGetSlot", |
94 ".plugin", | 98 ".plugin", |
95 in_sign="sisss", | 99 in_sign="sisss", |
96 out_sign="(ssaa{ss})", | 100 out_sign="(ssaa{ss})", |
97 method=self._getSlot, | 101 method=self._getSlot, |
98 async_=True, | 102 async_=True, |
99 ) | 103 ) |
100 host.plugins["UPLOAD"].register( | 104 host.plugins["UPLOAD"].register( |
101 "HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload | 105 "HTTP Upload", self.getHTTPUploadEntity, self.file_http_upload |
102 ) | 106 ) |
103 # list of callbacks used when a request is done to a component | 107 # list of callbacks used when a request is done to a component |
104 self.handlers = [] | 108 self.handlers = [] |
105 # XXX: there is not yet official short name, so we use "http_upload" | 109 # XXX: there is not yet official short name, so we use "http_upload" |
106 host.registerNamespace("http_upload", NS_HTTP_UPLOAD) | 110 host.registerNamespace("http_upload", NS_HTTP_UPLOAD) |
149 if entity is None: | 153 if entity is None: |
150 raise exceptions.NotFound("No HTTP upload entity found") | 154 raise exceptions.NotFound("No HTTP upload entity found") |
151 | 155 |
152 return entity | 156 return entity |
153 | 157 |
154 def _fileHTTPUpload(self, filepath, filename="", upload_jid="", | 158 def _file_http_upload(self, filepath, filename="", upload_jid="", |
155 ignore_tls_errors=False, profile=C.PROF_KEY_NONE): | 159 ignore_tls_errors=False, profile=C.PROF_KEY_NONE): |
156 assert os.path.isabs(filepath) and os.path.isfile(filepath) | 160 assert os.path.isabs(filepath) and os.path.isfile(filepath) |
157 client = self.host.getClient(profile) | 161 client = self.host.getClient(profile) |
158 progress_id_d, __ = defer.ensureDeferred(self.fileHTTPUpload( | 162 return defer.ensureDeferred(self.file_http_upload( |
159 client, | 163 client, |
160 filepath, | 164 filepath, |
161 filename or None, | 165 filename or None, |
162 jid.JID(upload_jid) if upload_jid else None, | 166 jid.JID(upload_jid) if upload_jid else None, |
163 {"ignore_tls_errors": ignore_tls_errors}, | 167 {"ignore_tls_errors": ignore_tls_errors}, |
164 )) | 168 )) |
165 return progress_id_d | 169 |
166 | 170 async def file_http_upload( |
167 async def fileHTTPUpload( | 171 self, |
168 self, client, filepath, filename=None, upload_jid=None, options=None): | 172 client: SatXMPPEntity, |
173 filepath: Path, | |
174 filename: Optional[str] = None, | |
175 upload_jid: Optional[jid.JID] = None, | |
176 extra: Optional[dict] = None | |
177 ) -> Tuple[str, defer.Deferred]: | |
169 """Upload a file through HTTP | 178 """Upload a file through HTTP |
170 | 179 |
171 @param filepath(str): absolute path of the file | 180 @param filepath: absolute path of the file |
172 @param filename(None, unicode): name to use for the upload | 181 @param filename: name to use for the upload |
173 None to use basename of the path | 182 None to use basename of the path |
174 @param upload_jid(jid.JID, None): upload capable entity jid, | 183 @param upload_jid: upload capable entity jid, |
175 or None to use autodetected, if possible | 184 or None to use autodetected, if possible |
176 @param options(dict): options where key can be: | 185 @param extra: options where key can be: |
177 - ignore_tls_errors(bool): if True, SSL certificate will not be checked | 186 - ignore_tls_errors(bool): if True, SSL certificate will not be checked |
187 - attachment(dict): file attachment data | |
178 @param profile: %(doc_profile)s | 188 @param profile: %(doc_profile)s |
179 @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire | 189 @return: progress id and Deferred which fire download URL |
180 download URL | 190 """ |
181 """ | 191 if extra is None: |
182 if options is None: | 192 extra = {} |
183 options = {} | 193 ignore_tls_errors = extra.get("ignore_tls_errors", False) |
184 ignore_tls_errors = options.get("ignore_tls_errors", False) | 194 file_metadata = { |
185 filename = filename or os.path.basename(filepath) | 195 "filename": filename or os.path.basename(filepath), |
186 size = os.path.getsize(filepath) | 196 "filepath": filepath, |
187 | 197 "size": os.path.getsize(filepath), |
188 size_adjust = [] | 198 } |
189 #: this trigger can be used to modify the requested size, it is notably useful | 199 |
190 #: with encryption. The size_adjust is a list which can be filled by int to add | 200 #: this trigger can be used to modify the filename or size requested when geting |
191 #: to the initial size | 201 #: the slot, it is notably useful with encryption. |
192 self.host.trigger.point( | 202 self.host.trigger.point( |
193 "XEP-0363_upload_size", client, options, filepath, size, size_adjust, | 203 "XEP-0363_upload_pre_slot", client, extra, file_metadata, |
194 triggers_no_cancel=True) | 204 triggers_no_cancel=True |
195 if size_adjust: | 205 ) |
196 size = sum([size, *size_adjust]) | |
197 try: | 206 try: |
198 slot = await self.getSlot(client, filename, size, upload_jid=upload_jid) | 207 slot = await self.getSlot( |
208 client, file_metadata["filename"], file_metadata["size"], | |
209 upload_jid=upload_jid | |
210 ) | |
199 except Exception as e: | 211 except Exception as e: |
200 log.warning(_("Can't get upload slot: {reason}").format(reason=e)) | 212 log.warning(_("Can't get upload slot: {reason}").format(reason=e)) |
201 raise e | 213 raise e |
202 else: | 214 else: |
203 log.debug(f"Got upload slot: {slot}") | 215 log.debug(f"Got upload slot: {slot}") |
204 sat_file = self.host.plugins["FILE"].File( | 216 sat_file = self.host.plugins["FILE"].File( |
205 self.host, client, filepath, uid=options.get("progress_id"), size=size, | 217 self.host, client, filepath, uid=extra.get("progress_id"), |
218 size=file_metadata["size"], | |
206 auto_end_signals=False | 219 auto_end_signals=False |
207 ) | 220 ) |
208 progress_id = sat_file.uid | 221 progress_id = sat_file.uid |
209 | 222 |
210 file_producer = http_client.FileBodyProducer(sat_file) | 223 file_producer = http_client.FileBodyProducer(sat_file) |
221 value = value.encode('utf-8') | 234 value = value.encode('utf-8') |
222 headers[name] = value | 235 headers[name] = value |
223 | 236 |
224 | 237 |
225 await self.host.trigger.asyncPoint( | 238 await self.host.trigger.asyncPoint( |
226 "XEP-0363_upload", client, options, sat_file, file_producer, slot, | 239 "XEP-0363_upload", client, extra, sat_file, file_producer, slot, |
227 triggers_no_cancel=True) | 240 triggers_no_cancel=True) |
228 | 241 |
229 download_d = agent.request( | 242 download_d = agent.request( |
230 b"PUT", | 243 b"PUT", |
231 slot.put.encode("utf-8"), | 244 slot.put.encode("utf-8"), |
232 http_headers.Headers(headers), | 245 http_headers.Headers(headers), |
233 file_producer, | 246 file_producer, |
234 ) | 247 ) |
235 download_d.addCallbacks( | 248 download_d.addCallbacks( |
236 self._uploadCb, | 249 self._upload_cb, |
237 self._uploadEb, | 250 self._upload_eb, |
238 (sat_file, slot), | 251 (sat_file, slot), |
239 None, | 252 None, |
240 (sat_file,), | 253 (sat_file,), |
241 ) | 254 ) |
242 | 255 |
243 return progress_id, download_d | 256 return progress_id, download_d |
244 | 257 |
245 def _uploadCb(self, __, sat_file, slot): | 258 def _upload_cb(self, __, sat_file, slot): |
246 """Called once file is successfully uploaded | 259 """Called once file is successfully uploaded |
247 | 260 |
248 @param sat_file(SatFile): file used for the upload | 261 @param sat_file(SatFile): file used for the upload |
249 should be closed, but it is needed to send the progressFinished signal | 262 should be closed, but it is needed to send the progressFinished signal |
250 @param slot(Slot): put/get urls | 263 @param slot(Slot): put/get urls |
251 """ | 264 """ |
252 log.info(f"HTTP upload finished ({slot.get})") | 265 log.info(f"HTTP upload finished ({slot.get})") |
253 sat_file.progressFinished({"url": slot.get}) | 266 sat_file.progressFinished({"url": slot.get}) |
254 return slot.get | 267 return slot.get |
255 | 268 |
256 def _uploadEb(self, failure_, sat_file): | 269 def _upload_eb(self, failure_, sat_file): |
257 """Called on unsuccessful upload | 270 """Called on unsuccessful upload |
258 | 271 |
259 @param sat_file(SatFile): file used for the upload | 272 @param sat_file(SatFile): file used for the upload |
260 should be closed, be is needed to send the progressError signal | 273 should be closed, be is needed to send the progressError signal |
261 """ | 274 """ |