comparison libervia/backend/plugins/plugin_xep_0447.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 4b842c1fb686
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
64 self._u = host.plugins["XEP-0103"] 64 self._u = host.plugins["XEP-0103"]
65 self._hints = host.plugins["XEP-0334"] 65 self._hints = host.plugins["XEP-0334"]
66 self._m = host.plugins["XEP-0446"] 66 self._m = host.plugins["XEP-0446"]
67 self._http_upload = host.plugins.get("XEP-0363") 67 self._http_upload = host.plugins.get("XEP-0363")
68 self._attach = host.plugins["ATTACH"] 68 self._attach = host.plugins["ATTACH"]
69 self._attach.register( 69 self._attach.register(self.can_handle_attachment, self.attach, priority=1000)
70 self.can_handle_attachment, self.attach, priority=1000
71 )
72 self.register_source_handler( 70 self.register_source_handler(
73 self._u.namespace, "url-data", self._u.parse_url_data_elt 71 self._u.namespace, "url-data", self._u.parse_url_data_elt
74 ) 72 )
75 host.plugins["DOWNLOAD"].register_download_handler(self._u.namespace, self.download) 73 host.plugins["DOWNLOAD"].register_download_handler(
74 self._u.namespace, self.download
75 )
76 host.trigger.add("message_received", self._message_received_trigger) 76 host.trigger.add("message_received", self._message_received_trigger)
77 77
78 def register_source_handler( 78 def register_source_handler(
79 self, namespace: str, element_name: str, 79 self,
80 namespace: str,
81 element_name: str,
80 callback: Callable[[domish.Element], Dict[str, Any]], 82 callback: Callable[[domish.Element], Dict[str, Any]],
81 encrypted: bool = False 83 encrypted: bool = False,
82 ) -> None: 84 ) -> None:
83 """Register a handler for file source 85 """Register a handler for file source
84 86
85 @param namespace: namespace of the element supported 87 @param namespace: namespace of the element supported
86 @param element_name: name of the element supported 88 @param element_name: name of the element supported
101 self, 103 self,
102 client: SatXMPPEntity, 104 client: SatXMPPEntity,
103 attachment: Dict[str, Any], 105 attachment: Dict[str, Any],
104 source: Dict[str, Any], 106 source: Dict[str, Any],
105 dest_path: Union[Path, str], 107 dest_path: Union[Path, str],
106 extra: Optional[Dict[str, Any]] = None 108 extra: Optional[Dict[str, Any]] = None,
107 ) -> Tuple[str, defer.Deferred]: 109 ) -> Tuple[str, defer.Deferred]:
108 # TODO: handle url-data headers 110 # TODO: handle url-data headers
109 if extra is None: 111 if extra is None:
110 extra = {} 112 extra = {}
111 try: 113 try:
112 download_url = source["url"] 114 download_url = source["url"]
113 except KeyError: 115 except KeyError:
114 raise ValueError(f"{source} has missing URL") 116 raise ValueError(f"{source} has missing URL")
115 117
116 if extra.get('ignore_tls_errors', False): 118 if extra.get("ignore_tls_errors", False):
117 log.warning( 119 log.warning("TLS certificate check disabled, this is highly insecure")
118 "TLS certificate check disabled, this is highly insecure"
119 )
120 treq_client = treq_client_no_ssl 120 treq_client = treq_client_no_ssl
121 else: 121 else:
122 treq_client = treq 122 treq_client = treq
123 123
124 try: 124 try:
125 file_size = int(attachment["size"]) 125 file_size = int(attachment["size"])
126 except (KeyError, ValueError): 126 except (KeyError, ValueError):
127 head_data = await treq_client.head(download_url) 127 head_data = await treq_client.head(download_url)
128 file_size = int(head_data.headers.getRawHeaders('content-length')[0]) 128 file_size = int(head_data.headers.getRawHeaders("content-length")[0])
129 129
130 file_obj = stream.SatFile( 130 file_obj = stream.SatFile(
131 self.host, 131 self.host,
132 client, 132 client,
133 dest_path, 133 dest_path,
134 mode="wb", 134 mode="wb",
135 size = file_size, 135 size=file_size,
136 ) 136 )
137 137
138 progress_id = file_obj.uid 138 progress_id = file_obj.uid
139 139
140 resp = await treq_client.get(download_url, unbuffered=True) 140 resp = await treq_client.get(download_url, unbuffered=True)
155 return False 155 return False
156 else: 156 else:
157 return True 157 return True
158 158
159 def get_sources_elt( 159 def get_sources_elt(
160 self, 160 self, children: Optional[List[domish.Element]] = None
161 children: Optional[List[domish.Element]] = None
162 ) -> domish.Element: 161 ) -> domish.Element:
163 """Generate <sources> element""" 162 """Generate <sources> element"""
164 sources_elt = domish.Element((NS_SFS, "sources")) 163 sources_elt = domish.Element((NS_SFS, "sources"))
165 if children: 164 if children:
166 for child in children: 165 for child in children:
209 ) 208 )
210 sources_elt = self.get_sources_elt() 209 sources_elt = self.get_sources_elt()
211 file_sharing_elt.addChild(sources_elt) 210 file_sharing_elt.addChild(sources_elt)
212 for source_data in sources: 211 for source_data in sources:
213 if "url" in source_data: 212 if "url" in source_data:
214 sources_elt.addChild( 213 sources_elt.addChild(self._u.get_url_data_elt(**source_data))
215 self._u.get_url_data_elt(**source_data)
216 )
217 else: 214 else:
218 raise NotImplementedError( 215 raise NotImplementedError(f"source data not implemented: {source_data}")
219 f"source data not implemented: {source_data}"
220 )
221 216
222 return file_sharing_elt 217 return file_sharing_elt
223 218
224 def parse_sources_elt( 219 def parse_sources_elt(self, sources_elt: domish.Element) -> List[Dict[str, Any]]:
225 self,
226 sources_elt: domish.Element
227 ) -> List[Dict[str, Any]]:
228 """Parse <sources/> element 220 """Parse <sources/> element
229 221
230 @param sources_elt: <sources/> element, or a direct parent element 222 @param sources_elt: <sources/> element, or a direct parent element
231 @return: list of found sources data 223 @return: list of found sources data
232 @raise: exceptions.NotFound: Can't find <sources/> element 224 @raise: exceptions.NotFound: Can't find <sources/> element
234 if sources_elt.name != "sources" or sources_elt.uri != NS_SFS: 226 if sources_elt.name != "sources" or sources_elt.uri != NS_SFS:
235 try: 227 try:
236 sources_elt = next(sources_elt.elements(NS_SFS, "sources")) 228 sources_elt = next(sources_elt.elements(NS_SFS, "sources"))
237 except StopIteration: 229 except StopIteration:
238 raise exceptions.NotFound( 230 raise exceptions.NotFound(
239 f"<sources/> element is missing: {sources_elt.toXml()}") 231 f"<sources/> element is missing: {sources_elt.toXml()}"
232 )
240 sources = [] 233 sources = []
241 for elt in sources_elt.elements(): 234 for elt in sources_elt.elements():
242 if not elt.uri: 235 if not elt.uri:
243 log.warning("ignoring source element {elt.toXml()}") 236 log.warning("ignoring source element {elt.toXml()}")
244 continue 237 continue
255 if "type" not in source_data: 248 if "type" not in source_data:
256 source_data["type"] = elt.uri 249 source_data["type"] = elt.uri
257 sources.append(source_data) 250 sources.append(source_data)
258 return sources 251 return sources
259 252
260 def parse_file_sharing_elt( 253 def parse_file_sharing_elt(self, file_sharing_elt: domish.Element) -> Dict[str, Any]:
261 self,
262 file_sharing_elt: domish.Element
263 ) -> Dict[str, Any]:
264 """Parse <file-sharing/> element and return file-sharing data 254 """Parse <file-sharing/> element and return file-sharing data
265 255
266 @param file_sharing_elt: <file-sharing/> element 256 @param file_sharing_elt: <file-sharing/> element
267 @return: file-sharing data. It a dict whose keys correspond to 257 @return: file-sharing data. It a dict whose keys correspond to
268 [get_file_sharing_elt] parameters 258 [get_file_sharing_elt] parameters
269 """ 259 """
270 if file_sharing_elt.name != "file-sharing" or file_sharing_elt.uri != NS_SFS: 260 if file_sharing_elt.name != "file-sharing" or file_sharing_elt.uri != NS_SFS:
271 try: 261 try:
272 file_sharing_elt = next( 262 file_sharing_elt = next(file_sharing_elt.elements(NS_SFS, "file-sharing"))
273 file_sharing_elt.elements(NS_SFS, "file-sharing")
274 )
275 except StopIteration: 263 except StopIteration:
276 raise exceptions.NotFound 264 raise exceptions.NotFound
277 try: 265 try:
278 data = self._m.parse_file_metadata_elt(file_sharing_elt) 266 data = self._m.parse_file_metadata_elt(file_sharing_elt)
279 except exceptions.NotFound: 267 except exceptions.NotFound:
287 raise ValueError(str(e)) 275 raise ValueError(str(e))
288 276
289 return data 277 return data
290 278
291 def _add_file_sharing_attachments( 279 def _add_file_sharing_attachments(
292 self, 280 self, client: SatXMPPEntity, message_elt: domish.Element, data: Dict[str, Any]
293 client: SatXMPPEntity,
294 message_elt: domish.Element,
295 data: Dict[str, Any]
296 ) -> Dict[str, Any]: 281 ) -> Dict[str, Any]:
297 """Check <message> for a shared file, and add it as an attachment""" 282 """Check <message> for a shared file, and add it as an attachment"""
298 # XXX: XEP-0447 doesn't support several attachments in a single message, for now 283 # XXX: XEP-0447 doesn't support several attachments in a single message, for now
299 # however that should be fixed in future version, and so we accept several 284 # however that should be fixed in future version, and so we accept several
300 # <file-sharing> element in a message. 285 # <file-sharing> element in a message.
301 for file_sharing_elt in message_elt.elements(NS_SFS, "file-sharing"): 286 for file_sharing_elt in message_elt.elements(NS_SFS, "file-sharing"):
302 attachment = self.parse_file_sharing_elt(message_elt) 287 attachment = self.parse_file_sharing_elt(message_elt)
303 288
304 if any( 289 if any(
305 s.get(C.MESS_KEY_ENCRYPTED, False) 290 s.get(C.MESS_KEY_ENCRYPTED, False) for s in attachment["sources"]
306 for s in attachment["sources"]
307 ) and client.encryption.isEncrypted(data): 291 ) and client.encryption.isEncrypted(data):
308 # we don't add the encrypted flag if the message itself is not encrypted, 292 # we don't add the encrypted flag if the message itself is not encrypted,
309 # because the decryption key is part of the link, so sending it over 293 # because the decryption key is part of the link, so sending it over
310 # unencrypted channel is like having no encryption at all. 294 # unencrypted channel is like having no encryption at all.
311 attachment[C.MESS_KEY_ENCRYPTED] = True 295 attachment[C.MESS_KEY_ENCRYPTED] = True
312 296
313 attachments = data['extra'].setdefault(C.KEY_ATTACHMENTS, []) 297 attachments = data["extra"].setdefault(C.KEY_ATTACHMENTS, [])
314 attachments.append(attachment) 298 attachments.append(attachment)
315 299
316 return data 300 return data
317 301
318 async def attach(self, client, data): 302 async def attach(self, client, data):
319 # XXX: for now, XEP-0447 only allow to send one file per <message/>, thus we need 303 # XXX: for now, XEP-0447 only allow to send one file per <message/>, thus we need
320 # to send each file in a separate message 304 # to send each file in a separate message
321 attachments = data["extra"][C.KEY_ATTACHMENTS] 305 attachments = data["extra"][C.KEY_ATTACHMENTS]
322 if not data['message'] or data['message'] == {'': ''}: 306 if not data["message"] or data["message"] == {"": ""}:
323 extra_attachments = attachments[1:] 307 extra_attachments = attachments[1:]
324 del attachments[1:] 308 del attachments[1:]
325 else: 309 else:
326 # we have a message, we must send first attachment separately 310 # we have a message, we must send first attachment separately
327 extra_attachments = attachments[:] 311 extra_attachments = attachments[:]
344 [{"url": attachment["url"]}], 328 [{"url": attachment["url"]}],
345 name=attachment.get("name"), 329 name=attachment.get("name"),
346 size=attachment.get("size"), 330 size=attachment.get("size"),
347 desc=attachment.get("desc"), 331 desc=attachment.get("desc"),
348 media_type=attachment.get("media_type"), 332 media_type=attachment.get("media_type"),
349 file_hash=file_hash 333 file_hash=file_hash,
350 ) 334 )
351 data["xml"].addChild(file_sharing_elt) 335 data["xml"].addChild(file_sharing_elt)
352 336
353 for attachment in extra_attachments: 337 for attachment in extra_attachments:
354 # we send all remaining attachment in a separate message 338 # we send all remaining attachment in a separate message
355 await client.sendMessage( 339 await client.sendMessage(
356 to_jid=data['to'], 340 to_jid=data["to"],
357 message={'': ''}, 341 message={"": ""},
358 subject=data['subject'], 342 subject=data["subject"],
359 mess_type=data['type'], 343 mess_type=data["type"],
360 extra={C.KEY_ATTACHMENTS: [attachment]}, 344 extra={C.KEY_ATTACHMENTS: [attachment]},
361 ) 345 )
362 346
363 if ((not data['extra'] 347 if (
364 and (not data['message'] or data['message'] == {'': ''}) 348 not data["extra"]
365 and not data['subject'])): 349 and (not data["message"] or data["message"] == {"": ""})
350 and not data["subject"]
351 ):
366 # nothing left to send, we can cancel the message 352 # nothing left to send, we can cancel the message
367 raise exceptions.CancelError("Cancelled by XEP_0447 attachment handling") 353 raise exceptions.CancelError("Cancelled by XEP_0447 attachment handling")
368 354
369 def _message_received_trigger(self, client, message_elt, post_treat): 355 def _message_received_trigger(self, client, message_elt, post_treat):
370 # we use a post_treat callback instead of "message_parse" trigger because we need 356 # we use a post_treat callback instead of "message_parse" trigger because we need