Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0277.py @ 4270:0d7bb4df2343
Reformatted code base using black.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 19 Jun 2024 18:44:57 +0200 |
parents | c86a22009c1f |
children |
comparison
equal
deleted
inserted
replaced
4269:64a85ce8be70 | 4270:0d7bb4df2343 |
---|---|
266 client: SatXMPPEntity, | 266 client: SatXMPPEntity, |
267 item_elt: domish.Element, | 267 item_elt: domish.Element, |
268 service: Optional[jid.JID], | 268 service: Optional[jid.JID], |
269 # FIXME: node is Optional until all calls to item_2_mb_data set properly service | 269 # FIXME: node is Optional until all calls to item_2_mb_data set properly service |
270 # and node. Once done, the Optional must be removed here | 270 # and node. Once done, the Optional must be removed here |
271 node: Optional[str] | 271 node: Optional[str], |
272 ) -> dict: | 272 ) -> dict: |
273 """Convert an XML Item to microblog data | 273 """Convert an XML Item to microblog data |
274 | 274 |
275 @param item_elt: microblog item element | 275 @param item_elt: microblog item element |
276 @param service: PubSub service where the item has been retrieved | 276 @param service: PubSub service where the item has been retrieved |
281 """ | 281 """ |
282 if service is None: | 282 if service is None: |
283 service = client.jid.userhostJID() | 283 service = client.jid.userhostJID() |
284 | 284 |
285 extra: Dict[str, Any] = {} | 285 extra: Dict[str, Any] = {} |
286 mb_data: Dict[str, Any] = { | 286 mb_data: Dict[str, Any] = {"service": service.full(), "extra": extra} |
287 "service": service.full(), | |
288 "extra": extra | |
289 } | |
290 | 287 |
291 def check_conflict(key, increment=False): | 288 def check_conflict(key, increment=False): |
292 """Check if key is already in microblog data | 289 """Check if key is already in microblog data |
293 | 290 |
294 @param key(unicode): key to check | 291 @param key(unicode): key to check |
333 _("Content of type XHTML must declare its namespace!") | 330 _("Content of type XHTML must declare its namespace!") |
334 ) | 331 ) |
335 ) | 332 ) |
336 key = check_conflict("{}_xhtml".format(elem.name)) | 333 key = check_conflict("{}_xhtml".format(elem.name)) |
337 data = data_elt.toXml() | 334 data = data_elt.toXml() |
338 mb_data[key] = yield self.host.plugins["TEXT_SYNTAXES"].clean_xhtml( | 335 mb_data[key] = yield self.host.plugins["TEXT_SYNTAXES"].clean_xhtml(data) |
339 data | |
340 ) | |
341 else: | 336 else: |
342 key = check_conflict(elem.name) | 337 key = check_conflict(elem.name) |
343 mb_data[key] = str(elem) | 338 mb_data[key] = str(elem) |
344 | 339 |
345 id_ = item_elt.getAttribute("id", "") # there can be no id for transient nodes | 340 id_ = item_elt.getAttribute("id", "") # there can be no id for transient nodes |
359 | 354 |
360 # uri | 355 # uri |
361 # FIXME: node should alway be set in the future, check FIXME in method signature | 356 # FIXME: node should alway be set in the future, check FIXME in method signature |
362 if node is not None: | 357 if node is not None: |
363 mb_data["node"] = node | 358 mb_data["node"] = node |
364 mb_data['uri'] = xmpp_uri.build_xmpp_uri( | 359 mb_data["uri"] = xmpp_uri.build_xmpp_uri( |
365 "pubsub", | 360 "pubsub", |
366 path=service.full(), | 361 path=service.full(), |
367 node=node, | 362 node=node, |
368 item=id_, | 363 item=id_, |
369 ) | 364 ) |
376 | 371 |
377 # atom:id | 372 # atom:id |
378 try: | 373 try: |
379 id_elt = next(entry_elt.elements(NS_ATOM, "id")) | 374 id_elt = next(entry_elt.elements(NS_ATOM, "id")) |
380 except StopIteration: | 375 except StopIteration: |
381 msg = ("No atom id found in the pubsub item {}, this is not standard !" | 376 msg = "No atom id found in the pubsub item {}, this is not standard !".format( |
382 .format(id_)) | 377 id_ |
378 ) | |
383 log.warning(msg) | 379 log.warning(msg) |
384 mb_data["atom_id"] = "" | 380 mb_data["atom_id"] = "" |
385 else: | 381 else: |
386 mb_data["atom_id"] = str(id_elt) | 382 mb_data["atom_id"] = str(id_elt) |
387 | 383 |
450 mb_data["published"] = calendar.timegm( | 446 mb_data["published"] = calendar.timegm( |
451 dateutil.parser.parse(str(published_elt)).utctimetuple() | 447 dateutil.parser.parse(str(published_elt)).utctimetuple() |
452 ) | 448 ) |
453 | 449 |
454 # links | 450 # links |
455 comments = mb_data['comments'] = [] | 451 comments = mb_data["comments"] = [] |
456 for link_elt in entry_elt.elements(NS_ATOM, "link"): | 452 for link_elt in entry_elt.elements(NS_ATOM, "link"): |
457 href = link_elt.getAttribute("href") | 453 href = link_elt.getAttribute("href") |
458 if not href: | 454 if not href: |
459 log.warning( | 455 log.warning(f"missing href in <link> element: {link_elt.toXml()}") |
460 f'missing href in <link> element: {link_elt.toXml()}' | |
461 ) | |
462 continue | 456 continue |
463 rel = link_elt.getAttribute("rel") | 457 rel = link_elt.getAttribute("rel") |
464 if (rel == "replies" and link_elt.getAttribute("title") == "comments"): | 458 if rel == "replies" and link_elt.getAttribute("title") == "comments": |
465 uri = href | 459 uri = href |
466 comments_data = { | 460 comments_data = { |
467 "uri": uri, | 461 "uri": uri, |
468 } | 462 } |
469 try: | 463 try: |
487 top_elt = top_elt.parent | 481 top_elt = top_elt.parent |
488 repeater_jid = jid.JID(top_elt["from"]) | 482 repeater_jid = jid.JID(top_elt["from"]) |
489 except (AttributeError, RuntimeError): | 483 except (AttributeError, RuntimeError): |
490 # we should always have either the "publisher" attribute or the | 484 # we should always have either the "publisher" attribute or the |
491 # stanza available | 485 # stanza available |
492 log.error( | 486 log.error(f"Can't find repeater of the post: {item_elt.toXml()}") |
493 f"Can't find repeater of the post: {item_elt.toXml()}" | |
494 ) | |
495 continue | 487 continue |
496 | 488 |
497 extra["repeated"] = { | 489 extra["repeated"] = {"by": repeater_jid.full(), "uri": href} |
498 "by": repeater_jid.full(), | |
499 "uri": href | |
500 } | |
501 elif rel in ("related", "enclosure"): | 490 elif rel in ("related", "enclosure"): |
502 attachment: Dict[str, Any] = { | 491 attachment: Dict[str, Any] = {"sources": [{"url": href}]} |
503 "sources": [{"url": href}] | |
504 } | |
505 if rel == "related": | 492 if rel == "related": |
506 attachment["external"] = True | 493 attachment["external"] = True |
507 for attr, key in ( | 494 for attr, key in ( |
508 ("type", "media_type"), | 495 ("type", "media_type"), |
509 ("title", "desc"), | 496 ("title", "desc"), |
531 log.warning( | 518 log.warning( |
532 f"Invalid or missing media type for alternate link: {href}" | 519 f"Invalid or missing media type for alternate link: {href}" |
533 ) | 520 ) |
534 extra.setdefault("alt_links", []).append(link_data) | 521 extra.setdefault("alt_links", []).append(link_data) |
535 else: | 522 else: |
536 log.warning( | 523 log.warning(f"Unmanaged link element: {link_elt.toXml()}") |
537 f"Unmanaged link element: {link_elt.toXml()}" | |
538 ) | |
539 | 524 |
540 # author | 525 # author |
541 publisher = item_elt.getAttribute("publisher") | 526 publisher = item_elt.getAttribute("publisher") |
542 try: | 527 try: |
543 author_elt = next(entry_elt.elements(NS_ATOM, "author")) | 528 author_elt = next(entry_elt.elements(NS_ATOM, "author")) |
556 author = mb_data["author"] = str(name_elt).strip() | 541 author = mb_data["author"] = str(name_elt).strip() |
557 # uri | 542 # uri |
558 try: | 543 try: |
559 uri_elt = next(author_elt.elements(NS_ATOM, "uri")) | 544 uri_elt = next(author_elt.elements(NS_ATOM, "uri")) |
560 except StopIteration: | 545 except StopIteration: |
561 log.debug( | 546 log.debug("No uri element found in author element of item {}".format(id_)) |
562 "No uri element found in author element of item {}".format(id_) | |
563 ) | |
564 if publisher: | 547 if publisher: |
565 mb_data["author_jid"] = publisher | 548 mb_data["author_jid"] = publisher |
566 else: | 549 else: |
567 uri = str(uri_elt) | 550 uri = str(uri_elt) |
568 if uri.startswith("xmpp:"): | 551 if uri.startswith("xmpp:"): |
569 uri = uri[5:] | 552 uri = uri[5:] |
570 mb_data["author_jid"] = uri | 553 mb_data["author_jid"] = uri |
571 else: | 554 else: |
572 mb_data["author_jid"] = ( | 555 mb_data["author_jid"] = item_elt.getAttribute("publisher") or "" |
573 item_elt.getAttribute("publisher") or "" | |
574 ) | |
575 if not author and mb_data["author_jid"]: | 556 if not author and mb_data["author_jid"]: |
576 # FIXME: temporary workaround for missing author name, would be | 557 # FIXME: temporary workaround for missing author name, would be |
577 # better to use directly JID's identity (to be done from frontends?) | 558 # better to use directly JID's identity (to be done from frontends?) |
578 try: | 559 try: |
579 mb_data["author"] = jid.JID(mb_data["author_jid"]).user | 560 mb_data["author"] = jid.JID(mb_data["author_jid"]).user |
580 except Exception as e: | 561 except Exception as e: |
581 log.warning(f"No author name found, and can't parse author jid: {e}") | 562 log.warning( |
563 f"No author name found, and can't parse author jid: {e}" | |
564 ) | |
582 | 565 |
583 if not publisher: | 566 if not publisher: |
584 log.debug("No publisher attribute, we can't verify author jid") | 567 log.debug("No publisher attribute, we can't verify author jid") |
585 mb_data["author_jid_verified"] = False | 568 mb_data["author_jid_verified"] = False |
586 elif jid.JID(publisher).userhostJID() == jid.JID(uri).userhostJID(): | 569 elif jid.JID(publisher).userhostJID() == jid.JID(uri).userhostJID(): |
618 ] | 601 ] |
619 mb_data["tags"] = categories | 602 mb_data["tags"] = categories |
620 | 603 |
621 ## the trigger ## | 604 ## the trigger ## |
622 # if other plugins have things to add or change | 605 # if other plugins have things to add or change |
623 yield self.host.trigger.point( | 606 yield self.host.trigger.point("XEP-0277_item2data", item_elt, entry_elt, mb_data) |
624 "XEP-0277_item2data", item_elt, entry_elt, mb_data | |
625 ) | |
626 | 607 |
627 defer.returnValue(mb_data) | 608 defer.returnValue(mb_data) |
628 | 609 |
629 async def mb_data_2_entry_elt(self, client, mb_data, item_id, service, node): | 610 async def mb_data_2_entry_elt(self, client, mb_data, item_id, service, node): |
630 """Convert a data dict to en entry usable to create an item | 611 """Convert a data dict to en entry usable to create an item |
653 if attr in mb_data: | 634 if attr in mb_data: |
654 elem = entry_elt.addElement(elem_name) | 635 elem = entry_elt.addElement(elem_name) |
655 if type_: | 636 if type_: |
656 if type_ == "_rich": # convert input from current syntax to XHTML | 637 if type_ == "_rich": # convert input from current syntax to XHTML |
657 xml_content = await synt.convert( | 638 xml_content = await synt.convert( |
658 mb_data[attr], synt.get_current_syntax(client.profile), "XHTML" | 639 mb_data[attr], |
640 synt.get_current_syntax(client.profile), | |
641 "XHTML", | |
659 ) | 642 ) |
660 if f"{elem_name}_xhtml" in mb_data: | 643 if f"{elem_name}_xhtml" in mb_data: |
661 raise failure.Failure( | 644 raise failure.Failure( |
662 exceptions.DataError( | 645 exceptions.DataError( |
663 _( | 646 _( |
720 for attachment in attachments: | 703 for attachment in attachments: |
721 try: | 704 try: |
722 url = attachment["url"] | 705 url = attachment["url"] |
723 except KeyError: | 706 except KeyError: |
724 try: | 707 try: |
725 url = next( | 708 url = next(s["url"] for s in attachment["sources"] if "url" in s) |
726 s['url'] for s in attachment["sources"] if 'url' in s | |
727 ) | |
728 except (StopIteration, KeyError): | 709 except (StopIteration, KeyError): |
729 log.warning( | 710 log.warning( |
730 f'"url" missing in attachment, ignoring: {attachment}' | 711 f'"url" missing in attachment, ignoring: {attachment}' |
731 ) | 712 ) |
732 continue | 713 continue |
744 # this is an attached file | 725 # this is an attached file |
745 link_elt["rel"] = "enclosure" | 726 link_elt["rel"] = "enclosure" |
746 for key, attr in ( | 727 for key, attr in ( |
747 ("media_type", "type"), | 728 ("media_type", "type"), |
748 ("desc", "title"), | 729 ("desc", "title"), |
749 ("size", "lenght") | 730 ("size", "lenght"), |
750 ): | 731 ): |
751 value = attachment.get(key) | 732 value = attachment.get(key) |
752 if value: | 733 if value: |
753 link_elt[attr] = str(value) | 734 link_elt[attr] = str(value) |
754 | 735 |
755 ## alternate links ## | 736 ## alternate links ## |
756 alt_links = extra.get("alt_links") | 737 alt_links = extra.get("alt_links") |
757 if alt_links: | 738 if alt_links: |
758 for link_data in alt_links: | 739 for link_data in alt_links: |
759 url_template = link_data["url"] | 740 url_template = link_data["url"] |
760 url = url_template.format( | 741 url = url_template.format( |
761 service=quote(service.full(), safe=""), | 742 service=quote(service.full(), safe=""), |
762 node=quote(node, safe=""), | 743 node=quote(node, safe=""), |
763 item=quote(item_id, safe="") | 744 item=quote(item_id, safe=""), |
764 ) | 745 ) |
765 | 746 |
766 link_elt = entry_elt.addElement("link") | 747 link_elt = entry_elt.addElement("link") |
767 link_elt["href"] = url | 748 link_elt["href"] = url |
768 link_elt["rel"] = "alternate" | 749 link_elt["rel"] = "alternate" |
798 pass | 779 pass |
799 | 780 |
800 ## published/updated time ## | 781 ## published/updated time ## |
801 current_time = time.time() | 782 current_time = time.time() |
802 entry_elt.addElement( | 783 entry_elt.addElement( |
803 "updated", content=utils.xmpp_date(float(mb_data.get("updated", current_time))) | 784 "updated", |
785 content=utils.xmpp_date(float(mb_data.get("updated", current_time))), | |
804 ) | 786 ) |
805 entry_elt.addElement( | 787 entry_elt.addElement( |
806 "published", | 788 "published", |
807 content=utils.xmpp_date(float(mb_data.get("published", current_time))), | 789 content=utils.xmpp_date(float(mb_data.get("published", current_time))), |
808 ) | 790 ) |
809 | 791 |
810 ## categories ## | 792 ## categories ## |
811 for tag in mb_data.get('tags', []): | 793 for tag in mb_data.get("tags", []): |
812 category_elt = entry_elt.addElement("category") | 794 category_elt = entry_elt.addElement("category") |
813 category_elt["term"] = tag | 795 category_elt["term"] = tag |
814 | 796 |
815 ## id ## | 797 ## id ## |
816 entry_id = mb_data.get( | 798 entry_id = mb_data.get( |
823 ), | 805 ), |
824 ) | 806 ) |
825 entry_elt.addElement("id", content=entry_id) # | 807 entry_elt.addElement("id", content=entry_id) # |
826 | 808 |
827 ## comments ## | 809 ## comments ## |
828 for comments_data in mb_data.get('comments', []): | 810 for comments_data in mb_data.get("comments", []): |
829 link_elt = entry_elt.addElement("link") | 811 link_elt = entry_elt.addElement("link") |
830 # XXX: "uri" is set in self._manage_comments if not already existing | 812 # XXX: "uri" is set in self._manage_comments if not already existing |
831 try: | 813 try: |
832 link_elt["href"] = comments_data["uri"] | 814 link_elt["href"] = comments_data["uri"] |
833 except KeyError: | 815 except KeyError: |
842 repeated = extra["repeated"] | 824 repeated = extra["repeated"] |
843 link_elt = entry_elt.addElement("link") | 825 link_elt = entry_elt.addElement("link") |
844 link_elt["rel"] = "via" | 826 link_elt["rel"] = "via" |
845 link_elt["href"] = repeated["uri"] | 827 link_elt["href"] = repeated["uri"] |
846 except KeyError as e: | 828 except KeyError as e: |
847 log.warning( | 829 log.warning(f"invalid repeated element({e}): {extra['repeated']}") |
848 f"invalid repeated element({e}): {extra['repeated']}" | |
849 ) | |
850 | 830 |
851 ## final item building ## | 831 ## final item building ## |
852 item_elt = pubsub.Item(id=item_id, payload=entry_elt) | 832 item_elt = pubsub.Item(id=item_id, payload=entry_elt) |
853 | 833 |
854 ## the trigger ## | 834 ## the trigger ## |
870 | 850 |
871 @param item_id: a comment node | 851 @param item_id: a comment node |
872 """ | 852 """ |
873 if not self.is_comment_node(item_id): | 853 if not self.is_comment_node(item_id): |
874 raise ValueError("This node is not a comment node") | 854 raise ValueError("This node is not a comment node") |
875 return item_id[len(NS_COMMENT_PREFIX):] | 855 return item_id[len(NS_COMMENT_PREFIX) :] |
876 | 856 |
877 def get_comments_node(self, item_id): | 857 def get_comments_node(self, item_id): |
878 """Generate comment node | 858 """Generate comment node |
879 | 859 |
880 @param item_id(unicode): id of the parent item | 860 @param item_id(unicode): id of the parent item |
906 | 886 |
907 return defer.succeed( | 887 return defer.succeed( |
908 client.pubsub_service if client.pubsub_service is not None else parent_service | 888 client.pubsub_service if client.pubsub_service is not None else parent_service |
909 ) | 889 ) |
910 | 890 |
911 async def _manage_comments(self, client, mb_data, service, node, item_id, access=None): | 891 async def _manage_comments( |
892 self, client, mb_data, service, node, item_id, access=None | |
893 ): | |
912 """Check comments keys in mb_data and create comments node if necessary | 894 """Check comments keys in mb_data and create comments node if necessary |
913 | 895 |
914 if a comments node metadata is set in the mb_data['comments'] list, it is used | 896 if a comments node metadata is set in the mb_data['comments'] list, it is used |
915 otherwise it is generated (if allow_comments is True). | 897 otherwise it is generated (if allow_comments is True). |
916 @param mb_data(dict): microblog mb_data | 898 @param mb_data(dict): microblog mb_data |
929 return | 911 return |
930 elif allow_comments == False: | 912 elif allow_comments == False: |
931 if "comments" in mb_data: | 913 if "comments" in mb_data: |
932 log.warning( | 914 log.warning( |
933 "comments are not allowed but there is already a comments node, " | 915 "comments are not allowed but there is already a comments node, " |
934 "it may be lost: {uri}".format( | 916 "it may be lost: {uri}".format(uri=mb_data["comments"]) |
935 uri=mb_data["comments"] | |
936 ) | |
937 ) | 917 ) |
938 del mb_data["comments"] | 918 del mb_data["comments"] |
939 return | 919 return |
940 | 920 |
941 # we have usually a single comment node, but the spec allow several, so we need to | 921 # we have usually a single comment node, but the spec allow several, so we need to |
942 # handle this in a list | 922 # handle this in a list |
943 if len(mb_data.setdefault('comments', [])) == 0: | 923 if len(mb_data.setdefault("comments", [])) == 0: |
944 # we need at least one comment node | 924 # we need at least one comment node |
945 mb_data['comments'].append({}) | 925 mb_data["comments"].append({}) |
946 | 926 |
947 if access is None: | 927 if access is None: |
948 # TODO: cache access models per service/node | 928 # TODO: cache access models per service/node |
949 try: | 929 try: |
950 parent_node_config = await self._p.getConfiguration(client, service, node) | 930 parent_node_config = await self._p.getConfiguration(client, service, node) |
951 except error.StanzaError as e: | 931 except error.StanzaError as e: |
952 log.debug(f"Can't get parent node configuration: {e}") | 932 log.debug(f"Can't get parent node configuration: {e}") |
953 access = self._p.ACCESS_OPEN | 933 access = self._p.ACCESS_OPEN |
954 else: | 934 else: |
955 access = parent_node_config.get(self._p.OPT_ACCESS_MODEL, self._p.ACCESS_OPEN) | 935 access = parent_node_config.get( |
936 self._p.OPT_ACCESS_MODEL, self._p.ACCESS_OPEN | |
937 ) | |
956 | 938 |
957 options = { | 939 options = { |
958 self._p.OPT_ACCESS_MODEL: access, | 940 self._p.OPT_ACCESS_MODEL: access, |
959 self._p.OPT_MAX_ITEMS: "max", | 941 self._p.OPT_MAX_ITEMS: "max", |
960 self._p.OPT_PERSIST_ITEMS: 1, | 942 self._p.OPT_PERSIST_ITEMS: 1, |
965 } | 947 } |
966 | 948 |
967 # if other plugins need to change the options | 949 # if other plugins need to change the options |
968 self.host.trigger.point("XEP-0277_comments", client, mb_data, options) | 950 self.host.trigger.point("XEP-0277_comments", client, mb_data, options) |
969 | 951 |
970 for comments_data in mb_data['comments']: | 952 for comments_data in mb_data["comments"]: |
971 uri = comments_data.get('uri') | 953 uri = comments_data.get("uri") |
972 comments_node = comments_data.get('node') | 954 comments_node = comments_data.get("node") |
973 try: | 955 try: |
974 comments_service = jid.JID(comments_data["service"]) | 956 comments_service = jid.JID(comments_data["service"]) |
975 except KeyError: | 957 except KeyError: |
976 comments_service = None | 958 comments_service = None |
977 | 959 |
978 if uri: | 960 if uri: |
979 uri_service, uri_node = self.parse_comment_url(uri) | 961 uri_service, uri_node = self.parse_comment_url(uri) |
980 if ((comments_node is not None and comments_node!=uri_node) | 962 if (comments_node is not None and comments_node != uri_node) or ( |
981 or (comments_service is not None and comments_service!=uri_service)): | 963 comments_service is not None and comments_service != uri_service |
964 ): | |
982 raise ValueError( | 965 raise ValueError( |
983 f"Incoherence between comments URI ({uri}) and comments_service " | 966 f"Incoherence between comments URI ({uri}) and comments_service " |
984 f"({comments_service}) or comments_node ({comments_node})") | 967 f"({comments_service}) or comments_node ({comments_node})" |
985 comments_data['service'] = comments_service = uri_service | 968 ) |
986 comments_data['node'] = comments_node = uri_node | 969 comments_data["service"] = comments_service = uri_service |
970 comments_data["node"] = comments_node = uri_node | |
987 else: | 971 else: |
988 if not comments_node: | 972 if not comments_node: |
989 comments_node = self.get_comments_node(item_id) | 973 comments_node = self.get_comments_node(item_id) |
990 comments_data['node'] = comments_node | 974 comments_data["node"] = comments_node |
991 if comments_service is None: | 975 if comments_service is None: |
992 comments_service = await self.get_comments_service(client, service) | 976 comments_service = await self.get_comments_service(client, service) |
993 if comments_service is None: | 977 if comments_service is None: |
994 comments_service = client.jid.userhostJID() | 978 comments_service = client.jid.userhostJID() |
995 comments_data['service'] = comments_service | 979 comments_data["service"] = comments_service |
996 | 980 |
997 comments_data['uri'] = xmpp_uri.build_xmpp_uri( | 981 comments_data["uri"] = xmpp_uri.build_xmpp_uri( |
998 "pubsub", | 982 "pubsub", |
999 path=comments_service.full(), | 983 path=comments_service.full(), |
1000 node=comments_node, | 984 node=comments_node, |
1001 ) | 985 ) |
1002 | 986 |
1029 | 1013 |
1030 def friendly_id(self, data): | 1014 def friendly_id(self, data): |
1031 """Generate a user friendly id from title or content""" | 1015 """Generate a user friendly id from title or content""" |
1032 # TODO: rich content should be converted to plain text | 1016 # TODO: rich content should be converted to plain text |
1033 id_base = regex.url_friendly_text( | 1017 id_base = regex.url_friendly_text( |
1034 data.get('title') | 1018 data.get("title") |
1035 or data.get('title_rich') | 1019 or data.get("title_rich") |
1036 or data.get('content') | 1020 or data.get("content") |
1037 or data.get('content_rich') | 1021 or data.get("content_rich") |
1038 or '' | 1022 or "" |
1039 ) | 1023 ) |
1040 if not data.get("user_friendly_id_suffix", True): | 1024 if not data.get("user_friendly_id_suffix", True): |
1041 return id_base | 1025 return id_base |
1042 else: | 1026 else: |
1043 return f"{id_base}-{token_urlsafe(3)}" | 1027 return f"{id_base}-{token_urlsafe(3)}" |
1052 async def send( | 1036 async def send( |
1053 self, | 1037 self, |
1054 client: SatXMPPEntity, | 1038 client: SatXMPPEntity, |
1055 data: dict, | 1039 data: dict, |
1056 service: Optional[jid.JID] = None, | 1040 service: Optional[jid.JID] = None, |
1057 node: Optional[str] = NS_MICROBLOG | 1041 node: Optional[str] = NS_MICROBLOG, |
1058 ) -> Optional[str]: | 1042 ) -> Optional[str]: |
1059 """Send XEP-0277's microblog data | 1043 """Send XEP-0277's microblog data |
1060 | 1044 |
1061 @param data: microblog data (must include at least a "content" or a "title" key). | 1045 @param data: microblog data (must include at least a "content" or a "title" key). |
1062 see http://wiki.goffi.org/wiki/Bridge_API_-_Microblogging/en for details | 1046 see http://wiki.goffi.org/wiki/Bridge_API_-_Microblogging/en for details |
1080 if not data.get("user_friendly_id_suffix", True): | 1064 if not data.get("user_friendly_id_suffix", True): |
1081 # we have no random suffix, which can lead to conflict, so we check if | 1065 # we have no random suffix, which can lead to conflict, so we check if |
1082 # the item doesn't already exist, and change ID if it's the case. | 1066 # the item doesn't already exist, and change ID if it's the case. |
1083 try: | 1067 try: |
1084 items, __ = await self._p.get_items( | 1068 items, __ = await self._p.get_items( |
1085 client, | 1069 client, service, node, item_ids=[item_id] |
1086 service, | |
1087 node, | |
1088 item_ids = [item_id] | |
1089 ) | 1070 ) |
1090 except exceptions.NotFound: | 1071 except exceptions.NotFound: |
1091 pass | 1072 pass |
1092 else: | 1073 else: |
1093 # the item already exists | 1074 # the item already exists |
1117 | 1098 |
1118 await self._p.publish(client, service, node, [item], extra=extra) | 1099 await self._p.publish(client, service, node, [item], extra=extra) |
1119 return item_id | 1100 return item_id |
1120 | 1101 |
1121 def _mb_repeat( | 1102 def _mb_repeat( |
1122 self, | 1103 self, service_s: str, node: str, item: str, extra_s: str, profile_key: str |
1123 service_s: str, | |
1124 node: str, | |
1125 item: str, | |
1126 extra_s: str, | |
1127 profile_key: str | |
1128 ) -> defer.Deferred: | 1104 ) -> defer.Deferred: |
1129 service = jid.JID(service_s) if service_s else None | 1105 service = jid.JID(service_s) if service_s else None |
1130 node = node if node else NS_MICROBLOG | 1106 node = node if node else NS_MICROBLOG |
1131 client = self.host.get_client(profile_key) | 1107 client = self.host.get_client(profile_key) |
1132 extra = data_format.deserialise(extra_s) | 1108 extra = data_format.deserialise(extra_s) |
1133 d = defer.ensureDeferred( | 1109 d = defer.ensureDeferred(self.repeat(client, item, service, node, extra)) |
1134 self.repeat(client, item, service, node, extra) | |
1135 ) | |
1136 # [repeat] can return None, and we always need a str | 1110 # [repeat] can return None, and we always need a str |
1137 d.addCallback(lambda ret: ret or "") | 1111 d.addCallback(lambda ret: ret or "") |
1138 return d | 1112 return d |
1139 | 1113 |
1140 async def repeat( | 1114 async def repeat( |
1152 """ | 1126 """ |
1153 if service is None: | 1127 if service is None: |
1154 service = client.jid.userhostJID() | 1128 service = client.jid.userhostJID() |
1155 | 1129 |
1156 # we first get the post to repeat | 1130 # we first get the post to repeat |
1157 items, __ = await self._p.get_items( | 1131 items, __ = await self._p.get_items(client, service, node, item_ids=[item]) |
1158 client, | |
1159 service, | |
1160 node, | |
1161 item_ids = [item] | |
1162 ) | |
1163 if not items: | 1132 if not items: |
1164 raise exceptions.NotFound( | 1133 raise exceptions.NotFound( |
1165 f"no item found at node {node!r} on {service} with ID {item!r}" | 1134 f"no item found at node {node!r} on {service} with ID {item!r}" |
1166 ) | 1135 ) |
1167 item_elt = items[0] | 1136 item_elt = items[0] |
1168 try: | 1137 try: |
1169 entry_elt = next(item_elt.elements(NS_ATOM, "entry")) | 1138 entry_elt = next(item_elt.elements(NS_ATOM, "entry")) |
1170 except StopIteration: | 1139 except StopIteration: |
1171 raise exceptions.DataError( | 1140 raise exceptions.DataError("post to repeat is not a XEP-0277 blog item") |
1172 "post to repeat is not a XEP-0277 blog item" | |
1173 ) | |
1174 | 1141 |
1175 # we want to be sure that we have an author element | 1142 # we want to be sure that we have an author element |
1176 try: | 1143 try: |
1177 author_elt = next(entry_elt.elements(NS_ATOM, "author")) | 1144 author_elt = next(entry_elt.elements(NS_ATOM, "author")) |
1178 except StopIteration: | 1145 except StopIteration: |
1196 link_elt["href"] = xmpp_uri.build_xmpp_uri( | 1163 link_elt["href"] = xmpp_uri.build_xmpp_uri( |
1197 "pubsub", path=service.full(), node=node, item=item | 1164 "pubsub", path=service.full(), node=node, item=item |
1198 ) | 1165 ) |
1199 | 1166 |
1200 return await self._p.send_item( | 1167 return await self._p.send_item( |
1201 client, | 1168 client, client.jid.userhostJID(), NS_MICROBLOG, entry_elt |
1202 client.jid.userhostJID(), | |
1203 NS_MICROBLOG, | |
1204 entry_elt | |
1205 ) | 1169 ) |
1206 | 1170 |
1207 def _mb_preview(self, service, node, data, profile_key): | 1171 def _mb_preview(self, service, node, data, profile_key): |
1208 service = jid.JID(service) if service else None | 1172 service = jid.JID(service) if service else None |
1209 node = node if node else NS_MICROBLOG | 1173 node = node if node else NS_MICROBLOG |
1216 async def preview( | 1180 async def preview( |
1217 self, | 1181 self, |
1218 client: SatXMPPEntity, | 1182 client: SatXMPPEntity, |
1219 data: dict, | 1183 data: dict, |
1220 service: Optional[jid.JID] = None, | 1184 service: Optional[jid.JID] = None, |
1221 node: Optional[str] = NS_MICROBLOG | 1185 node: Optional[str] = NS_MICROBLOG, |
1222 ) -> dict: | 1186 ) -> dict: |
1223 """Preview microblog data without publishing them | 1187 """Preview microblog data without publishing them |
1224 | 1188 |
1225 params are the same as for [send] | 1189 params are the same as for [send] |
1226 @return: microblog data as would be retrieved from published item | 1190 @return: microblog data as would be retrieved from published item |
1232 | 1196 |
1233 # we have to serialise then deserialise to be sure that all triggers are called | 1197 # we have to serialise then deserialise to be sure that all triggers are called |
1234 item_elt = await self.mb_data_2_entry_elt(client, data, item_id, service, node) | 1198 item_elt = await self.mb_data_2_entry_elt(client, data, item_id, service, node) |
1235 item_elt.uri = pubsub.NS_PUBSUB | 1199 item_elt.uri = pubsub.NS_PUBSUB |
1236 return await self.item_2_mb_data(client, item_elt, service, node) | 1200 return await self.item_2_mb_data(client, item_elt, service, node) |
1237 | |
1238 | 1201 |
1239 ## retract ## | 1202 ## retract ## |
1240 | 1203 |
1241 def _mb_retract(self, service_jid_s, nodeIdentifier, itemIdentifier, profile_key): | 1204 def _mb_retract(self, service_jid_s, nodeIdentifier, itemIdentifier, profile_key): |
1242 """Call self._p._retract_item, but use default node if node is empty""" | 1205 """Call self._p._retract_item, but use default node if node is empty""" |
1250 | 1213 |
1251 ## get ## | 1214 ## get ## |
1252 | 1215 |
1253 def _mb_get_serialise(self, data): | 1216 def _mb_get_serialise(self, data): |
1254 items, metadata = data | 1217 items, metadata = data |
1255 metadata['items'] = items | 1218 metadata["items"] = items |
1256 return data_format.serialise(metadata) | 1219 return data_format.serialise(metadata) |
1257 | 1220 |
1258 def _mb_get(self, service="", node="", max_items=10, item_ids=None, extra="", | 1221 def _mb_get( |
1259 profile_key=C.PROF_KEY_NONE): | 1222 self, |
1223 service="", | |
1224 node="", | |
1225 max_items=10, | |
1226 item_ids=None, | |
1227 extra="", | |
1228 profile_key=C.PROF_KEY_NONE, | |
1229 ): | |
1260 """ | 1230 """ |
1261 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit | 1231 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit |
1262 @param item_ids (list[unicode]): list of item IDs | 1232 @param item_ids (list[unicode]): list of item IDs |
1263 """ | 1233 """ |
1264 client = self.host.get_client(profile_key) | 1234 client = self.host.get_client(profile_key) |
1265 service = jid.JID(service) if service else None | 1235 service = jid.JID(service) if service else None |
1266 max_items = None if max_items == C.NO_LIMIT else max_items | 1236 max_items = None if max_items == C.NO_LIMIT else max_items |
1267 extra = self._p.parse_extra(data_format.deserialise(extra)) | 1237 extra = self._p.parse_extra(data_format.deserialise(extra)) |
1268 d = defer.ensureDeferred( | 1238 d = defer.ensureDeferred( |
1269 self.mb_get(client, service, node or None, max_items, item_ids, | 1239 self.mb_get( |
1270 extra.rsm_request, extra.extra) | 1240 client, |
1241 service, | |
1242 node or None, | |
1243 max_items, | |
1244 item_ids, | |
1245 extra.rsm_request, | |
1246 extra.extra, | |
1247 ) | |
1271 ) | 1248 ) |
1272 d.addCallback(self._mb_get_serialise) | 1249 d.addCallback(self._mb_get_serialise) |
1273 return d | 1250 return d |
1274 | 1251 |
1275 async def mb_get( | 1252 async def mb_get( |
1278 service: Optional[jid.JID] = None, | 1255 service: Optional[jid.JID] = None, |
1279 node: Optional[str] = None, | 1256 node: Optional[str] = None, |
1280 max_items: Optional[int] = 10, | 1257 max_items: Optional[int] = 10, |
1281 item_ids: Optional[List[str]] = None, | 1258 item_ids: Optional[List[str]] = None, |
1282 rsm_request: Optional[rsm.RSMRequest] = None, | 1259 rsm_request: Optional[rsm.RSMRequest] = None, |
1283 extra: Optional[Dict[str, Any]] = None | 1260 extra: Optional[Dict[str, Any]] = None, |
1284 ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: | 1261 ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: |
1285 """Get some microblogs | 1262 """Get some microblogs |
1286 | 1263 |
1287 @param service(jid.JID, None): jid of the publisher | 1264 @param service(jid.JID, None): jid of the publisher |
1288 None to get profile's PEP | 1265 None to get profile's PEP |
1307 item_ids=item_ids, | 1284 item_ids=item_ids, |
1308 rsm_request=rsm_request, | 1285 rsm_request=rsm_request, |
1309 extra=extra, | 1286 extra=extra, |
1310 ) | 1287 ) |
1311 mb_data_list, metadata = await self._p.trans_items_data_d( | 1288 mb_data_list, metadata = await self._p.trans_items_data_d( |
1312 items_data, partial(self.item_2_mb_data, client, service=service, node=node)) | 1289 items_data, partial(self.item_2_mb_data, client, service=service, node=node) |
1290 ) | |
1313 encrypted = metadata.pop("encrypted", None) | 1291 encrypted = metadata.pop("encrypted", None) |
1314 if encrypted is not None: | 1292 if encrypted is not None: |
1315 for mb_data in mb_data_list: | 1293 for mb_data in mb_data_list: |
1316 try: | 1294 try: |
1317 mb_data["encrypted"] = encrypted[mb_data["id"]] | 1295 mb_data["encrypted"] = encrypted[mb_data["id"]] |
1318 except KeyError: | 1296 except KeyError: |
1319 pass | 1297 pass |
1320 return (mb_data_list, metadata) | 1298 return (mb_data_list, metadata) |
1321 | 1299 |
1322 def _mb_rename(self, service, node, item_id, new_id, profile_key): | 1300 def _mb_rename(self, service, node, item_id, new_id, profile_key): |
1323 return defer.ensureDeferred(self.mb_rename( | 1301 return defer.ensureDeferred( |
1324 self.host.get_client(profile_key), | 1302 self.mb_rename( |
1325 jid.JID(service) if service else None, | 1303 self.host.get_client(profile_key), |
1326 node or None, | 1304 jid.JID(service) if service else None, |
1327 item_id, | 1305 node or None, |
1328 new_id | 1306 item_id, |
1329 )) | 1307 new_id, |
1308 ) | |
1309 ) | |
1330 | 1310 |
1331 async def mb_rename( | 1311 async def mb_rename( |
1332 self, | 1312 self, |
1333 client: SatXMPPEntity, | 1313 client: SatXMPPEntity, |
1334 service: Optional[jid.JID], | 1314 service: Optional[jid.JID], |
1335 node: Optional[str], | 1315 node: Optional[str], |
1336 item_id: str, | 1316 item_id: str, |
1337 new_id: str | 1317 new_id: str, |
1338 ) -> None: | 1318 ) -> None: |
1339 if not node: | 1319 if not node: |
1340 node = NS_MICROBLOG | 1320 node = NS_MICROBLOG |
1341 await self._p.rename_item(client, service, node, item_id, new_id) | 1321 await self._p.rename_item(client, service, node, item_id, new_id) |
1342 | 1322 |
1526 """convert items elements to list of microblog data in items_data""" | 1506 """convert items elements to list of microblog data in items_data""" |
1527 d = self._p.trans_items_data_d( | 1507 d = self._p.trans_items_data_d( |
1528 items_data, | 1508 items_data, |
1529 # FIXME: service and node should be used here | 1509 # FIXME: service and node should be used here |
1530 partial(self.item_2_mb_data, client), | 1510 partial(self.item_2_mb_data, client), |
1531 serialise=True | 1511 serialise=True, |
1532 ) | 1512 ) |
1533 d.addCallback(lambda serialised: ("", serialised)) | 1513 d.addCallback(lambda serialised: ("", serialised)) |
1534 return d | 1514 return d |
1535 | 1515 |
1536 d = self._p.get_rt_results( | 1516 d = self._p.get_rt_results( |
1550 ], | 1530 ], |
1551 ) | 1531 ) |
1552 ) | 1532 ) |
1553 return d | 1533 return d |
1554 | 1534 |
1555 def _mb_get_from_many(self, publishers_type, publishers, max_items=10, extra_dict=None, | 1535 def _mb_get_from_many( |
1556 profile_key=C.PROF_KEY_NONE): | 1536 self, |
1537 publishers_type, | |
1538 publishers, | |
1539 max_items=10, | |
1540 extra_dict=None, | |
1541 profile_key=C.PROF_KEY_NONE, | |
1542 ): | |
1557 """ | 1543 """ |
1558 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit | 1544 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit |
1559 """ | 1545 """ |
1560 max_items = None if max_items == C.NO_LIMIT else max_items | 1546 max_items = None if max_items == C.NO_LIMIT else max_items |
1561 publishers_type, publishers = self._check_publishers(publishers_type, publishers) | 1547 publishers_type, publishers = self._check_publishers(publishers_type, publishers) |
1567 extra.rsm_request, | 1553 extra.rsm_request, |
1568 extra.extra, | 1554 extra.extra, |
1569 profile_key, | 1555 profile_key, |
1570 ) | 1556 ) |
1571 | 1557 |
1572 def mb_get_from_many(self, publishers_type, publishers, max_items=None, rsm_request=None, | 1558 def mb_get_from_many( |
1573 extra=None, profile_key=C.PROF_KEY_NONE): | 1559 self, |
1560 publishers_type, | |
1561 publishers, | |
1562 max_items=None, | |
1563 rsm_request=None, | |
1564 extra=None, | |
1565 profile_key=C.PROF_KEY_NONE, | |
1566 ): | |
1574 """Get the published microblogs for a list of groups or jids | 1567 """Get the published microblogs for a list of groups or jids |
1575 | 1568 |
1576 @param publishers_type (str): type of the list of publishers (one of "GROUP" or | 1569 @param publishers_type (str): type of the list of publishers (one of "GROUP" or |
1577 "JID" or "ALL") | 1570 "JID" or "ALL") |
1578 @param publishers (list): list of publishers, according to publishers_type (list | 1571 @param publishers (list): list of publishers, according to publishers_type (list |
1605 for (service, node), (success, (failure_, (items_data, metadata))) in data_iter: | 1598 for (service, node), (success, (failure_, (items_data, metadata))) in data_iter: |
1606 items = [] | 1599 items = [] |
1607 for item, item_metadata in items_data: | 1600 for item, item_metadata in items_data: |
1608 item = data_format.serialise(item) | 1601 item = data_format.serialise(item) |
1609 items.append((item, item_metadata)) | 1602 items.append((item, item_metadata)) |
1610 ret.append(( | 1603 ret.append((service.full(), node, failure_, items, metadata)) |
1611 service.full(), | |
1612 node, | |
1613 failure_, | |
1614 items, | |
1615 metadata)) | |
1616 | 1604 |
1617 return data[0], ret | 1605 return data[0], ret |
1618 | 1606 |
1619 def _mb_get_from_many_with_comments_rt_result(self, session_id, | 1607 def _mb_get_from_many_with_comments_rt_result( |
1620 profile_key=C.PROF_KEY_DEFAULT): | 1608 self, session_id, profile_key=C.PROF_KEY_DEFAULT |
1609 ): | |
1621 """Get real-time results for [mb_get_from_many_with_comments] session | 1610 """Get real-time results for [mb_get_from_many_with_comments] session |
1622 | 1611 |
1623 @param session_id: id of the real-time deferred session | 1612 @param session_id: id of the real-time deferred session |
1624 @param return (tuple): (remaining, results) where: | 1613 @param return (tuple): (remaining, results) where: |
1625 - remaining is the number of still expected results | 1614 - remaining is the number of still expected results |
1641 profile = self.host.get_client(profile_key).profile | 1630 profile = self.host.get_client(profile_key).profile |
1642 d = self.rt_sessions.get_results(session_id, profile=profile) | 1631 d = self.rt_sessions.get_results(session_id, profile=profile) |
1643 d.addCallback(self._mb_get_from_many_with_comments_rt_result_serialise) | 1632 d.addCallback(self._mb_get_from_many_with_comments_rt_result_serialise) |
1644 return d | 1633 return d |
1645 | 1634 |
1646 def _mb_get_from_many_with_comments(self, publishers_type, publishers, max_items=10, | 1635 def _mb_get_from_many_with_comments( |
1647 max_comments=C.NO_LIMIT, extra_dict=None, | 1636 self, |
1648 extra_comments_dict=None, profile_key=C.PROF_KEY_NONE): | 1637 publishers_type, |
1638 publishers, | |
1639 max_items=10, | |
1640 max_comments=C.NO_LIMIT, | |
1641 extra_dict=None, | |
1642 extra_comments_dict=None, | |
1643 profile_key=C.PROF_KEY_NONE, | |
1644 ): | |
1649 """ | 1645 """ |
1650 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit | 1646 @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit |
1651 @param max_comments(int): maximum number of comments to get, C.NO_LIMIT for no | 1647 @param max_comments(int): maximum number of comments to get, C.NO_LIMIT for no |
1652 limit | 1648 limit |
1653 """ | 1649 """ |
1666 extra_comments.rsm_request, | 1662 extra_comments.rsm_request, |
1667 extra_comments.extra, | 1663 extra_comments.extra, |
1668 profile_key, | 1664 profile_key, |
1669 ) | 1665 ) |
1670 | 1666 |
1671 def mb_get_from_many_with_comments(self, publishers_type, publishers, max_items=None, | 1667 def mb_get_from_many_with_comments( |
1672 max_comments=None, rsm_request=None, extra=None, | 1668 self, |
1673 rsm_comments=None, extra_comments=None, | 1669 publishers_type, |
1674 profile_key=C.PROF_KEY_NONE): | 1670 publishers, |
1671 max_items=None, | |
1672 max_comments=None, | |
1673 rsm_request=None, | |
1674 extra=None, | |
1675 rsm_comments=None, | |
1676 extra_comments=None, | |
1677 profile_key=C.PROF_KEY_NONE, | |
1678 ): | |
1675 """Helper method to get the microblogs and their comments in one shot | 1679 """Helper method to get the microblogs and their comments in one shot |
1676 | 1680 |
1677 @param publishers_type (str): type of the list of publishers (one of "GROUP" or | 1681 @param publishers_type (str): type of the list of publishers (one of "GROUP" or |
1678 "JID" or "ALL") | 1682 "JID" or "ALL") |
1679 @param publishers (list): list of publishers, according to publishers_type (list | 1683 @param publishers (list): list of publishers, according to publishers_type (list |
1727 # then serialise | 1731 # then serialise |
1728 d.addCallback( | 1732 d.addCallback( |
1729 lambda items_data: self._p.trans_items_data_d( | 1733 lambda items_data: self._p.trans_items_data_d( |
1730 items_data, | 1734 items_data, |
1731 partial( | 1735 partial( |
1732 self.item_2_mb_data, client, service=service, node=node | 1736 self.item_2_mb_data, |
1737 client, | |
1738 service=service, | |
1739 node=node, | |
1733 ), | 1740 ), |
1734 serialise=True | 1741 serialise=True, |
1735 ) | 1742 ) |
1736 ) | 1743 ) |
1737 # with failure handling | 1744 # with failure handling |
1738 d.addCallback( | 1745 d.addCallback( |
1739 lambda serialised_items_data: ("",) + serialised_items_data | 1746 lambda serialised_items_data: ("",) + serialised_items_data |
1762 items_d.addCallback(lambda items_completed: (items_completed, metadata)) | 1769 items_d.addCallback(lambda items_completed: (items_completed, metadata)) |
1763 return items_d | 1770 return items_d |
1764 | 1771 |
1765 deferreds = {} | 1772 deferreds = {} |
1766 for service, node in node_data: | 1773 for service, node in node_data: |
1767 d = deferreds[(service, node)] = defer.ensureDeferred(self._p.get_items( | 1774 d = deferreds[(service, node)] = defer.ensureDeferred( |
1768 client, service, node, max_items, rsm_request=rsm_request, extra=extra | 1775 self._p.get_items( |
1769 )) | 1776 client, service, node, max_items, rsm_request=rsm_request, extra=extra |
1777 ) | |
1778 ) | |
1770 d.addCallback( | 1779 d.addCallback( |
1771 lambda items_data: self._p.trans_items_data_d( | 1780 lambda items_data: self._p.trans_items_data_d( |
1772 items_data, | 1781 items_data, |
1773 partial(self.item_2_mb_data, client, service=service, node=node), | 1782 partial(self.item_2_mb_data, client, service=service, node=node), |
1774 ) | 1783 ) |