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

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 79c8a70e1813
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
61 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), 61 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
62 } 62 }
63 63
64 # TODO: use a Pydantic model for extra 64 # TODO: use a Pydantic model for extra
65 EXTRA_ALLOWED = { 65 EXTRA_ALLOWED = {
66 "path", "namespace", "file_desc", "file_hash", "hash_algo", "webrtc", "call_data", 66 "path",
67 "size", "media_type" 67 "namespace",
68 "file_desc",
69 "file_hash",
70 "hash_algo",
71 "webrtc",
72 "call_data",
73 "size",
74 "media_type",
68 } 75 }
69 Range = namedtuple("Range", ("offset", "length")) 76 Range = namedtuple("Range", ("offset", "length"))
70 77
71 78
72 class XEP_0234(BaseApplicationHandler): 79 class XEP_0234(BaseApplicationHandler):
123 return True 130 return True
124 131
125 # generic methods 132 # generic methods
126 133
127 def build_file_element( 134 def build_file_element(
128 self, client, name=None, file_hash=None, hash_algo=None, size=None, 135 self,
129 mime_type=None, desc=None, modified=None, transfer_range=None, path=None, 136 client,
130 namespace=None, file_elt=None, **kwargs): 137 name=None,
138 file_hash=None,
139 hash_algo=None,
140 size=None,
141 mime_type=None,
142 desc=None,
143 modified=None,
144 transfer_range=None,
145 path=None,
146 namespace=None,
147 file_elt=None,
148 **kwargs,
149 ):
131 """Generate a <file> element with available metadata 150 """Generate a <file> element with available metadata
132 151
133 @param file_hash(unicode, None): hash of the file 152 @param file_hash(unicode, None): hash of the file
134 empty string to set <hash-used/> element 153 empty string to set <hash-used/> element
135 @param hash_algo(unicode, None): hash algorithm used 154 @param hash_algo(unicode, None): hash algorithm used
181 else: 200 else:
182 file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo)) 201 file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo))
183 elif hash_algo is not None: 202 elif hash_algo is not None:
184 file_elt.addChild(self._hash.build_hash_used_elt(hash_algo)) 203 file_elt.addChild(self._hash.build_hash_used_elt(hash_algo))
185 self.host.trigger.point( 204 self.host.trigger.point(
186 "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs) 205 "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs
206 )
187 if kwargs: 207 if kwargs:
188 for kw in kwargs: 208 for kw in kwargs:
189 log.debug("ignored keyword: {}".format(kw)) 209 log.debug("ignored keyword: {}".format(kw))
190 return file_elt 210 return file_elt
191 211
207 return self.build_file_element(client, **file_data) 227 return self.build_file_element(client, **file_data)
208 228
209 async def parse_file_element( 229 async def parse_file_element(
210 self, 230 self,
211 client: SatXMPPEntity, 231 client: SatXMPPEntity,
212 file_elt: domish.Element|None, 232 file_elt: domish.Element | None,
213 file_data: dict | None = None, 233 file_data: dict | None = None,
214 given: bool = False, 234 given: bool = False,
215 parent_elt: domish.Element | None = None, 235 parent_elt: domish.Element | None = None,
216 keep_empty_range: bool = False 236 keep_empty_range: bool = False,
217 ) -> dict: 237 ) -> dict:
218 """Parse a <file> element and updates file dictionary accordingly. 238 """Parse a <file> element and updates file dictionary accordingly.
219 239
220 @param client: The SatXMPPEntity instance. 240 @param client: The SatXMPPEntity instance.
221 @param file_elt: The file element to parse. 241 @param file_elt: The file element to parse.
297 if name is None: 317 if name is None:
298 file_data["name"] = "unnamed" 318 file_data["name"] = "unnamed"
299 elif name == "..": 319 elif name == "..":
300 # we don't want to go to parent dir when joining to a path 320 # we don't want to go to parent dir when joining to a path
301 file_data["name"] = "--" 321 file_data["name"] = "--"
302 elif "/" in name or "\\" in name: 322 elif "/" in name or "\\" in name:
303 file_data["name"] = regex.path_escape(name) 323 file_data["name"] = regex.path_escape(name)
304 324
305 try: 325 try:
306 file_data["mime_type"] = str( 326 file_data["mime_type"] = str(
307 next(file_elt.elements(NS_JINGLE_FT, "media-type")) 327 next(file_elt.elements(NS_JINGLE_FT, "media-type"))
308 ) 328 )
309 except StopIteration: 329 except StopIteration:
310 pass 330 pass
311 331
312 try: 332 try:
313 file_data["size"] = int( 333 file_data["size"] = int(str(next(file_elt.elements(NS_JINGLE_FT, "size"))))
314 str(next(file_elt.elements(NS_JINGLE_FT, "size")))
315 )
316 except StopIteration: 334 except StopIteration:
317 pass 335 pass
318 336
319 try: 337 try:
320 file_data["modified"] = date_utils.date_parse( 338 file_data["modified"] = date_utils.date_parse(
357 extra_s: str, 375 extra_s: str,
358 profile: str, 376 profile: str,
359 ) -> defer.Deferred[str]: 377 ) -> defer.Deferred[str]:
360 client = self.host.get_client(profile) 378 client = self.host.get_client(profile)
361 extra = data_format.deserialise(extra_s) 379 extra = data_format.deserialise(extra_s)
362 d = defer.ensureDeferred(self.file_send( 380 d = defer.ensureDeferred(
363 client, 381 self.file_send(
364 jid.JID(peer_jid_s), 382 client,
365 filepath, 383 jid.JID(peer_jid_s),
366 name or None, 384 filepath,
367 file_desc or None, 385 name or None,
368 extra, 386 file_desc or None,
369 )) 387 extra,
388 )
389 )
370 d.addCallback(data_format.serialise) 390 d.addCallback(data_format.serialise)
371 return d 391 return d
372 392
373 async def file_send( 393 async def file_send(
374 self, 394 self,
375 client: SatXMPPEntity, 395 client: SatXMPPEntity,
376 peer_jid: jid.JID, 396 peer_jid: jid.JID,
377 filepath: str, 397 filepath: str,
378 name: str|None, 398 name: str | None,
379 file_desc: str|None = None, 399 file_desc: str | None = None,
380 extra: dict|None = None 400 extra: dict | None = None,
381 ) -> dict: 401 ) -> dict:
382 """Send a file using jingle file transfer 402 """Send a file using jingle file transfer
383 403
384 @param peer_jid: destinee jid 404 @param peer_jid: destinee jid
385 @param filepath: absolute path of the file 405 @param filepath: absolute path of the file
404 "progress_id_d": progress_id_d, 424 "progress_id_d": progress_id_d,
405 }, 425 },
406 } 426 }
407 427
408 await self.host.trigger.async_point( 428 await self.host.trigger.async_point(
409 "XEP-0234_file_jingle_send", 429 "XEP-0234_file_jingle_send", client, peer_jid, content
410 client, peer_jid, content
411 ) 430 )
412 431
413 session_id = await self._j.initiate( 432 session_id = await self._j.initiate(
414 client, 433 client, peer_jid, [content], encrypted=encrypted
415 peer_jid,
416 [content],
417 encrypted = encrypted
418 ) 434 )
419 progress_id = await progress_id_d 435 progress_id = await progress_id_d
420 return { 436 return {"progress": progress_id, "session_id": session_id}
421 "progress": progress_id,
422 "session_id": session_id
423 }
424 437
425 def _file_jingle_request( 438 def _file_jingle_request(
426 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, 439 self,
427 profile=C.PROF_KEY_NONE): 440 peer_jid,
441 filepath,
442 name="",
443 file_hash="",
444 hash_algo="",
445 extra=None,
446 profile=C.PROF_KEY_NONE,
447 ):
428 client = self.host.get_client(profile) 448 client = self.host.get_client(profile)
429 return defer.ensureDeferred(self.file_jingle_request( 449 return defer.ensureDeferred(
430 client, 450 self.file_jingle_request(
431 jid.JID(peer_jid), 451 client,
432 filepath, 452 jid.JID(peer_jid),
433 name or None, 453 filepath,
434 file_hash or None, 454 name or None,
435 hash_algo or None, 455 file_hash or None,
436 extra or None, 456 hash_algo or None,
437 )) 457 extra or None,
458 )
459 )
438 460
439 async def file_jingle_request( 461 async def file_jingle_request(
440 self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None, 462 self,
441 extra=None): 463 client,
464 peer_jid,
465 filepath,
466 name=None,
467 file_hash=None,
468 hash_algo=None,
469 extra=None,
470 ):
442 """Request a file using jingle file transfer 471 """Request a file using jingle file transfer
443 472
444 @param peer_jid(jid.JID): destinee jid 473 @param peer_jid(jid.JID): destinee jid
445 @param filepath(str): absolute path where the file will be downloaded 474 @param filepath(str): absolute path where the file will be downloaded
446 @param name(unicode, None): name of the file 475 @param name(unicode, None): name of the file
478 return await progress_id_d 507 return await progress_id_d
479 508
480 # jingle callbacks 509 # jingle callbacks
481 510
482 def _get_confirm_msg( 511 def _get_confirm_msg(
483 self, 512 self, client: SatXMPPEntity, peer_jid: jid.JID, file_data: dict
484 client: SatXMPPEntity,
485 peer_jid: jid.JID,
486 file_data: dict
487 ) -> tuple[bool, str, str]: 513 ) -> tuple[bool, str, str]:
488 """Get confirmation message to display to user. 514 """Get confirmation message to display to user.
489 515
490 This is the message to show when a file sending request is received.""" 516 This is the message to show when a file sending request is received."""
491 file_name = file_data.get('name') 517 file_name = file_data.get("name")
492 file_size = file_data.get('size') 518 file_size = file_data.get("size")
493 519
494 if file_name: 520 if file_name:
495 file_name_msg = D_('wants to send you the file "{file_name}"').format( 521 file_name_msg = D_('wants to send you the file "{file_name}"').format(
496 file_name=file_name 522 file_name=file_name
497 ) 523 )
498 else: 524 else:
499 file_name_msg = D_('wants to send you an unnamed file') 525 file_name_msg = D_("wants to send you an unnamed file")
500 526
501 if file_size is not None: 527 if file_size is not None:
502 file_size_msg = D_("which has a size of {file_size_human}").format( 528 file_size_msg = D_("which has a size of {file_size_human}").format(
503 file_size_human=get_human_size(file_size) 529 file_size_human=get_human_size(file_size)
504 ) 530 )
505 else: 531 else:
506 file_size_msg = D_("which has an unknown size") 532 file_size_msg = D_("which has an unknown size")
507 533
508 file_description = file_data.get('desc') 534 file_description = file_data.get("desc")
509 if file_description: 535 if file_description:
510 description_msg = " Description: {}.".format(file_description) 536 description_msg = " Description: {}.".format(file_description)
511 else: 537 else:
512 description_msg = "" 538 description_msg = ""
513 539
514 if client.roster and peer_jid.userhostJID() not in client.roster: 540 if client.roster and peer_jid.userhostJID() not in client.roster:
515 is_in_roster = False 541 is_in_roster = False
516 confirm_msg = D_( 542 confirm_msg = D_(
517 "Somebody not in your contact list ({peer_jid}) {file_name_msg} {file_size_msg}.{description_msg} " 543 "Somebody not in your contact list ({peer_jid}) {file_name_msg} {file_size_msg}.{description_msg} "
518 "Accepting this could leak your presence and possibly your IP address. Do you accept?" 544 "Accepting this could leak your presence and possibly your IP address. Do you accept?"
519 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg) 545 ).format(
546 peer_jid=peer_jid,
547 file_name_msg=file_name_msg,
548 file_size_msg=file_size_msg,
549 description_msg=description_msg,
550 )
520 confirm_title = D_("File sent from an unknown contact") 551 confirm_title = D_("File sent from an unknown contact")
521 else: 552 else:
522 is_in_roster = True 553 is_in_roster = True
523 confirm_msg = D_( 554 confirm_msg = D_(
524 "{peer_jid} {file_name_msg} {file_size_msg}.{description_msg} Do you " 555 "{peer_jid} {file_name_msg} {file_size_msg}.{description_msg} Do you "
525 "accept?" 556 "accept?"
526 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg) 557 ).format(
558 peer_jid=peer_jid,
559 file_name_msg=file_name_msg,
560 file_size_msg=file_size_msg,
561 description_msg=description_msg,
562 )
527 confirm_title = D_("File Proposed") 563 confirm_title = D_("File Proposed")
528 564
529 return (is_in_roster, confirm_msg, confirm_title) 565 return (is_in_roster, confirm_msg, confirm_title)
530 566
531 async def jingle_preflight( 567 async def jingle_preflight(
532 self, 568 self, client: SatXMPPEntity, session: dict, description_elt: domish.Element
533 client: SatXMPPEntity,
534 session: dict,
535 description_elt: domish.Element
536 ) -> None: 569 ) -> None:
537 """Perform preflight checks for an incoming call session. 570 """Perform preflight checks for an incoming call session.
538 571
539 Check if the calls is audio only or audio/video, then, prompts the user for 572 Check if the calls is audio only or audio/video, then, prompts the user for
540 confirmation. 573 confirmation.
566 599
567 action_extra = { 600 action_extra = {
568 "type": action_type, 601 "type": action_type,
569 "session_id": session_id, 602 "session_id": session_id,
570 "from_jid": peer_jid.full(), 603 "from_jid": peer_jid.full(),
571 "file_data": file_data 604 "file_data": file_data,
572 } 605 }
573 action_extra["subtype"] = C.META_TYPE_FILE 606 action_extra["subtype"] = C.META_TYPE_FILE
574 accepted = await xml_tools.defer_confirm( 607 accepted = await xml_tools.defer_confirm(
575 self.host, 608 self.host,
576 confirm_msg, 609 confirm_msg,
577 confirm_title, 610 confirm_title,
578 profile=client.profile, 611 profile=client.profile,
579 action_extra=action_extra 612 action_extra=action_extra,
580 ) 613 )
581 if accepted: 614 if accepted:
582 session["pre_accepted"] = True 615 session["pre_accepted"] = True
583 return accepted 616 return accepted
584 617
585 async def jingle_preflight_info( 618 async def jingle_preflight_info(
586 self, 619 self,
587 client: SatXMPPEntity, 620 client: SatXMPPEntity,
588 session: dict, 621 session: dict,
589 info_type: str, 622 info_type: str,
590 info_data: dict|None = None 623 info_data: dict | None = None,
591 ) -> None: 624 ) -> None:
592 pass 625 pass
593 626
594 async def jingle_preflight_cancel( 627 async def jingle_preflight_cancel(
595 self, 628 self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError
596 client: SatXMPPEntity,
597 session: dict,
598 cancel_error: exceptions.CancelError
599 ) -> None: 629 ) -> None:
600 pass 630 pass
601 631
602 def jingle_description_elt( 632 def jingle_description_elt(
603 self, client, session, content_name, filepath, name, extra, progress_id_d 633 self, client, session, content_name, filepath, name, extra, progress_id_d
624 transport_data["webrtc"] = True 654 transport_data["webrtc"] = True
625 assert "file_path" not in application_data 655 assert "file_path" not in application_data
626 application_data["file_path"] = filepath 656 application_data["file_path"] = filepath
627 file_data = application_data["file_data"] = {} 657 file_data = application_data["file_data"] = {}
628 desc_elt = self.jingle_description_elt( 658 desc_elt = self.jingle_description_elt(
629 client, session, content_name, filepath, name, extra, progress_id_d) 659 client, session, content_name, filepath, name, extra, progress_id_d
660 )
630 file_elt = desc_elt.addElement("file") 661 file_elt = desc_elt.addElement("file")
631 662
632 if content_data["senders"] == self._j.ROLE_INITIATOR: 663 if content_data["senders"] == self._j.ROLE_INITIATOR:
633 # we send a file 664 # we send a file
634 if name is None: 665 if name is None:
648 if "namespace" in extra: 679 if "namespace" in extra:
649 file_data["namespace"] = extra["namespace"] 680 file_data["namespace"] = extra["namespace"]
650 if "path" in extra: 681 if "path" in extra:
651 file_data["path"] = extra["path"] 682 file_data["path"] = extra["path"]
652 self.build_file_element_from_dict( 683 self.build_file_element_from_dict(
653 client, file_data, file_elt=file_elt, file_hash="") 684 client, file_data, file_elt=file_elt, file_hash=""
685 )
654 else: 686 else:
655 # we request a file 687 # we request a file
656 file_hash = extra.pop("file_hash", "") 688 file_hash = extra.pop("file_hash", "")
657 if not name and not file_hash: 689 if not name and not file_hash:
658 raise ValueError(_("you need to provide at least name or file hash")) 690 raise ValueError(_("you need to provide at least name or file hash"))
738 client: SatXMPPEntity, 770 client: SatXMPPEntity,
739 session: dict, 771 session: dict,
740 content_data: dict, 772 content_data: dict,
741 content_name: str, 773 content_name: str,
742 file_data: dict, 774 file_data: dict,
743 file_elt: domish.Element 775 file_elt: domish.Element,
744 ) -> bool: 776 ) -> bool:
745 """parse file_elt, and handle user permission/file opening""" 777 """parse file_elt, and handle user permission/file opening"""
746 transport_data = content_data["transport_data"] 778 transport_data = content_data["transport_data"]
747 webrtc = transport_data.get("webrtc", False) 779 webrtc = transport_data.get("webrtc", False)
748 # file may have been already accepted in preflight 780 # file may have been already accepted in preflight
779 confirmed = await xml_tools.defer_confirm( 811 confirmed = await xml_tools.defer_confirm(
780 self.host, 812 self.host,
781 confirm_msg, 813 confirm_msg,
782 confirm_title, 814 confirm_title,
783 profile=client.profile, 815 profile=client.profile,
784 action_extra=action_extra 816 action_extra=action_extra,
785 ) 817 )
786 else: 818 else:
787 819
788 if hash_algo is not None: 820 if hash_algo is not None:
789 file_data["hash_algo"] = hash_algo 821 file_data["hash_algo"] = hash_algo
804 ) 836 )
805 837
806 if confirmed: 838 if confirmed:
807 await self.host.trigger.async_point( 839 await self.host.trigger.async_point(
808 "XEP-0234_file_receiving_request_conf", 840 "XEP-0234_file_receiving_request_conf",
809 client, session, content_data, file_elt 841 client,
842 session,
843 content_data,
844 file_elt,
810 ) 845 )
811 args = [client, session, content_name, content_data] 846 args = [client, session, content_name, content_data]
812 finished_d.addCallbacks( 847 finished_d.addCallbacks(
813 self._finished_cb, self._finished_eb, args, None, args 848 self._finished_cb, self._finished_eb, args, None, args
814 ) 849 )
868 except Exception as e: 903 except Exception as e:
869 self.host.bridge.progress_error( 904 self.host.bridge.progress_error(
870 progress_id, C.PROGRESS_ERROR_FAILED, client.profile 905 progress_id, C.PROGRESS_ERROR_FAILED, client.profile
871 ) 906 )
872 await self._j.terminate( 907 await self._j.terminate(
873 client, self._j.REASON_FAILED_APPLICATION, session) 908 client, self._j.REASON_FAILED_APPLICATION, session
909 )
874 raise e 910 raise e
875 else: 911 else:
876 # we are sending the file 912 # we are sending the file
877 size = file_data["size"] 913 size = file_data["size"]
878 # XXX: hash security is not critical here, so we just take the higher 914 # XXX: hash security is not critical here, so we just take the higher
887 data_cb=lambda data: hasher.update(data), 923 data_cb=lambda data: hasher.update(data),
888 ) 924 )
889 925
890 finished_d = content_data["finished_d"] = defer.Deferred() 926 finished_d = content_data["finished_d"] = defer.Deferred()
891 args = [client, session, content_name, content_data] 927 args = [client, session, content_name, content_data]
892 finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args) 928 finished_d.addCallbacks(
929 self._finished_cb, self._finished_eb, args, None, args
930 )
893 await self.host.trigger.async_point( 931 await self.host.trigger.async_point(
894 "XEP-0234_jingle_handler", 932 "XEP-0234_jingle_handler", client, session, content_data, desc_elt
895 client, session, content_data, desc_elt
896 ) 933 )
897 else: 934 else:
898 log.warning("FIXME: unmanaged action {}".format(action)) 935 log.warning("FIXME: unmanaged action {}".format(action))
899 return desc_elt 936 return desc_elt
900 937
957 reason = first_child.name 994 reason = first_child.name
958 if reason_elt.text is not None: 995 if reason_elt.text is not None:
959 reason = f"{reason} - {reason_elt.text}" 996 reason = f"{reason} - {reason_elt.text}"
960 else: 997 else:
961 reason = C.PROGRESS_ERROR_FAILED 998 reason = C.PROGRESS_ERROR_FAILED
962 self.host.bridge.progress_error( 999 self.host.bridge.progress_error(progress_id, reason, client.profile)
963 progress_id, reason, client.profile
964 )
965 1000
966 def _send_check_sum(self, client, session, content_name, content_data): 1001 def _send_check_sum(self, client, session, content_name, content_data):
967 """Send the session-info with the hash checksum""" 1002 """Send the session-info with the hash checksum"""
968 file_data = content_data["application_data"]["file_data"] 1003 file_data = content_data["application_data"]["file_data"]
969 hasher = file_data["hash_hasher"] 1004 hasher = file_data["hash_hasher"]