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 """