Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0277.py @ 3840:5b192a5eb72d
plugin XEP-0277: post repeating implementation:
Repeating a post as specified at https://xmpp.org/extensions/xep-0277.html#repeat is now
implemented. On sending, one can either use the new `repeat` method (or the bridge
`mbRepeat` one from frontends) to repeat any XEP-0277 item, or use the field `repeated` in
microblog data's `extra`. This field must mainly contains the `by` field with JID of
repeater (the "author*" field being filled with original author metadata), and the `uri`
field with link (usually `xmpp:` one) to repeated item.
When receiving a repeated item, the same `repeated` field is filled.
rel 370
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 13 Jul 2022 12:15:04 +0200 |
parents | b2ade5ecdbab |
children | e9c380ef41c8 |
comparison
equal
deleted
inserted
replaced
3839:395b51452601 | 3840:5b192a5eb72d |
---|---|
18 | 18 |
19 import time | 19 import time |
20 import dateutil | 20 import dateutil |
21 import calendar | 21 import calendar |
22 from secrets import token_urlsafe | 22 from secrets import token_urlsafe |
23 from typing import Optional | 23 from typing import Optional, Dict, Union, Any |
24 from functools import partial | 24 from functools import partial |
25 | 25 |
26 import shortuuid | 26 import shortuuid |
27 | 27 |
28 from twisted.words.protocols.jabber import jid, error | 28 from twisted.words.protocols.jabber import jid, error |
112 out_sign="", | 112 out_sign="", |
113 method=self._mbSend, | 113 method=self._mbSend, |
114 async_=True, | 114 async_=True, |
115 ) | 115 ) |
116 host.bridge.addMethod( | 116 host.bridge.addMethod( |
117 "mbRepeat", | |
118 ".plugin", | |
119 in_sign="sssss", | |
120 out_sign="s", | |
121 method=self._mbRepeat, | |
122 async_=True, | |
123 ) | |
124 host.bridge.addMethod( | |
117 "mbPreview", | 125 "mbPreview", |
118 ".plugin", | 126 ".plugin", |
119 in_sign="ssss", | 127 in_sign="ssss", |
120 out_sign="s", | 128 out_sign="s", |
121 method=self._mbPreview, | 129 method=self._mbPreview, |
270 @return: microblog data | 278 @return: microblog data |
271 """ | 279 """ |
272 if service is None: | 280 if service is None: |
273 service = client.jid.userhostJID() | 281 service = client.jid.userhostJID() |
274 | 282 |
275 microblog_data = {"service": service.full()} | 283 extra: Dict[str, Any] = {} |
284 microblog_data: Dict[str, Any] = { | |
285 "service": service.full(), | |
286 "extra": extra | |
287 } | |
276 | 288 |
277 def check_conflict(key, increment=False): | 289 def check_conflict(key, increment=False): |
278 """Check if key is already in microblog data | 290 """Check if key is already in microblog data |
279 | 291 |
280 @param key(unicode): key to check | 292 @param key(unicode): key to check |
438 ) | 450 ) |
439 | 451 |
440 # links | 452 # links |
441 comments = microblog_data['comments'] = [] | 453 comments = microblog_data['comments'] = [] |
442 for link_elt in entry_elt.elements(NS_ATOM, "link"): | 454 for link_elt in entry_elt.elements(NS_ATOM, "link"): |
443 if ( | 455 rel = link_elt.getAttribute("rel") |
444 link_elt.getAttribute("rel") == "replies" | 456 if (rel == "replies" and link_elt.getAttribute("title") == "comments"): |
445 and link_elt.getAttribute("title") == "comments" | |
446 ): | |
447 uri = link_elt["href"] | 457 uri = link_elt["href"] |
448 comments_data = { | 458 comments_data = { |
449 "uri": uri, | 459 "uri": uri, |
450 } | 460 } |
451 try: | 461 try: |
455 continue | 465 continue |
456 else: | 466 else: |
457 comments_data["service"] = comment_service.full() | 467 comments_data["service"] = comment_service.full() |
458 comments_data["node"] = comment_node | 468 comments_data["node"] = comment_node |
459 comments.append(comments_data) | 469 comments.append(comments_data) |
470 elif rel == "via": | |
471 href = link_elt.getAttribute("href") | |
472 if not href: | |
473 log.warning( | |
474 f'missing href in "via" <link> element: {link_elt.toXml()}' | |
475 ) | |
476 continue | |
477 try: | |
478 repeater_jid = jid.JID(item_elt["publisher"]) | |
479 except (KeyError, RuntimeError): | |
480 try: | |
481 # we look for stanza element which is at the root, meaning that it | |
482 # has not parent | |
483 top_elt = item_elt.parent | |
484 while top_elt.parent is not None: | |
485 top_elt = top_elt.parent | |
486 repeater_jid = jid.JID(top_elt["from"]) | |
487 except (AttributeError, RuntimeError): | |
488 # we should always have either the "publisher" attribute or the | |
489 # stanza available | |
490 log.error( | |
491 f"Can't find repeater of the post: {item_elt.toXml()}" | |
492 ) | |
493 continue | |
494 | |
495 extra["repeated"] = { | |
496 "by": repeater_jid.full(), | |
497 "uri": href | |
498 } | |
460 else: | 499 else: |
461 rel = link_elt.getAttribute("rel", "") | |
462 title = link_elt.getAttribute("title", "") | 500 title = link_elt.getAttribute("title", "") |
463 href = link_elt.getAttribute("href", "") | 501 href = link_elt.getAttribute("href", "") |
464 log.warning( | 502 log.warning( |
465 "Unmanaged link element: rel={rel} title={title} href={href}".format( | 503 "Unmanaged link element: rel={rel} title={title} href={href}".format( |
466 rel=rel, title=title, href=href | 504 rel=rel, title=title, href=href |
514 log.debug("No publisher attribute, we can't verify author jid") | 552 log.debug("No publisher attribute, we can't verify author jid") |
515 microblog_data["author_jid_verified"] = False | 553 microblog_data["author_jid_verified"] = False |
516 elif jid.JID(publisher).userhostJID() == jid.JID(uri).userhostJID(): | 554 elif jid.JID(publisher).userhostJID() == jid.JID(uri).userhostJID(): |
517 microblog_data["author_jid_verified"] = True | 555 microblog_data["author_jid_verified"] = True |
518 else: | 556 else: |
519 log.warning( | 557 if "repeated" not in extra: |
520 "item atom:uri differ from publisher attribute, spoofing " | 558 log.warning( |
521 "attempt ? atom:uri = {} publisher = {}".format( | 559 "item atom:uri differ from publisher attribute, spoofing " |
522 uri, item_elt.getAttribute("publisher") | 560 "attempt ? atom:uri = {} publisher = {}".format( |
561 uri, item_elt.getAttribute("publisher") | |
562 ) | |
523 ) | 563 ) |
524 ) | |
525 microblog_data["author_jid_verified"] = False | 564 microblog_data["author_jid_verified"] = False |
526 # email | 565 # email |
527 try: | 566 try: |
528 email_elt = next(author_elt.elements(NS_ATOM, "email")) | 567 email_elt = next(author_elt.elements(NS_ATOM, "email")) |
529 except StopIteration: | 568 except StopIteration: |
553 "XEP-0277_item2data", item_elt, entry_elt, microblog_data | 592 "XEP-0277_item2data", item_elt, entry_elt, microblog_data |
554 ) | 593 ) |
555 | 594 |
556 defer.returnValue(microblog_data) | 595 defer.returnValue(microblog_data) |
557 | 596 |
558 async def data2entry(self, client, data, item_id, service, node): | 597 async def data2entry(self, client, mb_data, item_id, service, node): |
559 """Convert a data dict to en entry usable to create an item | 598 """Convert a data dict to en entry usable to create an item |
560 | 599 |
561 @param data: data dict as given by bridge method. | 600 @param mb_data: data dict as given by bridge method. |
562 @param item_id(unicode): id of the item to use | 601 @param item_id(unicode): id of the item to use |
563 @param service(jid.JID, None): pubsub service where the item is sent | 602 @param service(jid.JID, None): pubsub service where the item is sent |
564 Needed to construct Atom id | 603 Needed to construct Atom id |
565 @param node(unicode): pubsub node where the item is sent | 604 @param node(unicode): pubsub node where the item is sent |
566 Needed to construct Atom id | 605 Needed to construct Atom id |
567 @return: deferred which fire domish.Element | 606 @return: deferred which fire domish.Element |
568 """ | 607 """ |
569 entry_elt = domish.Element((NS_ATOM, "entry")) | 608 entry_elt = domish.Element((NS_ATOM, "entry")) |
570 | 609 |
571 ## language ## | 610 ## language ## |
572 if "language" in data: | 611 if "language" in mb_data: |
573 entry_elt[(C.NS_XML, "lang")] = data["language"].strip() | 612 entry_elt[(C.NS_XML, "lang")] = mb_data["language"].strip() |
574 | 613 |
575 ## content and title ## | 614 ## content and title ## |
576 synt = self.host.plugins["TEXT_SYNTAXES"] | 615 synt = self.host.plugins["TEXT_SYNTAXES"] |
577 | 616 |
578 for elem_name in ("title", "content"): | 617 for elem_name in ("title", "content"): |
579 for type_ in ["", "_rich", "_xhtml"]: | 618 for type_ in ["", "_rich", "_xhtml"]: |
580 attr = "{}{}".format(elem_name, type_) | 619 attr = "{}{}".format(elem_name, type_) |
581 if attr in data: | 620 if attr in mb_data: |
582 elem = entry_elt.addElement(elem_name) | 621 elem = entry_elt.addElement(elem_name) |
583 if type_: | 622 if type_: |
584 if type_ == "_rich": # convert input from current syntax to XHTML | 623 if type_ == "_rich": # convert input from current syntax to XHTML |
585 xml_content = await synt.convert( | 624 xml_content = await synt.convert( |
586 data[attr], synt.getCurrentSyntax(client.profile), "XHTML" | 625 mb_data[attr], synt.getCurrentSyntax(client.profile), "XHTML" |
587 ) | 626 ) |
588 if "{}_xhtml".format(elem_name) in data: | 627 if "{}_xhtml".format(elem_name) in mb_data: |
589 raise failure.Failure( | 628 raise failure.Failure( |
590 exceptions.DataError( | 629 exceptions.DataError( |
591 _( | 630 _( |
592 "Can't have xhtml and rich content at the same time" | 631 "Can't have xhtml and rich content at the same time" |
593 ) | 632 ) |
594 ) | 633 ) |
595 ) | 634 ) |
596 else: | 635 else: |
597 xml_content = data[attr] | 636 xml_content = mb_data[attr] |
598 | 637 |
599 div_elt = xml_tools.ElementParser()( | 638 div_elt = xml_tools.ElementParser()( |
600 xml_content, namespace=C.NS_XHTML | 639 xml_content, namespace=C.NS_XHTML |
601 ) | 640 ) |
602 if ( | 641 if ( |
608 wrap_div_elt = domish.Element((C.NS_XHTML, "div")) | 647 wrap_div_elt = domish.Element((C.NS_XHTML, "div")) |
609 wrap_div_elt.addChild(div_elt) | 648 wrap_div_elt.addChild(div_elt) |
610 div_elt = wrap_div_elt | 649 div_elt = wrap_div_elt |
611 elem.addChild(div_elt) | 650 elem.addChild(div_elt) |
612 elem["type"] = "xhtml" | 651 elem["type"] = "xhtml" |
613 if elem_name not in data: | 652 if elem_name not in mb_data: |
614 # there is raw text content, which is mandatory | 653 # there is raw text content, which is mandatory |
615 # so we create one from xhtml content | 654 # so we create one from xhtml content |
616 elem_txt = entry_elt.addElement(elem_name) | 655 elem_txt = entry_elt.addElement(elem_name) |
617 text_content = await self.host.plugins[ | 656 text_content = await self.host.plugins[ |
618 "TEXT_SYNTAXES" | 657 "TEXT_SYNTAXES" |
624 ) | 663 ) |
625 elem_txt.addContent(text_content) | 664 elem_txt.addContent(text_content) |
626 elem_txt["type"] = "text" | 665 elem_txt["type"] = "text" |
627 | 666 |
628 else: # raw text only needs to be escaped to get HTML-safe sequence | 667 else: # raw text only needs to be escaped to get HTML-safe sequence |
629 elem.addContent(data[attr]) | 668 elem.addContent(mb_data[attr]) |
630 elem["type"] = "text" | 669 elem["type"] = "text" |
631 | 670 |
632 try: | 671 try: |
633 next(entry_elt.elements(NS_ATOM, "title")) | 672 next(entry_elt.elements(NS_ATOM, "title")) |
634 except StopIteration: | 673 except StopIteration: |
643 elem.name = "title" | 682 elem.name = "title" |
644 | 683 |
645 ## author ## | 684 ## author ## |
646 author_elt = entry_elt.addElement("author") | 685 author_elt = entry_elt.addElement("author") |
647 try: | 686 try: |
648 author_name = data["author"] | 687 author_name = mb_data["author"] |
649 except KeyError: | 688 except KeyError: |
650 # FIXME: must use better name | 689 # FIXME: must use better name |
651 author_name = client.jid.user | 690 author_name = client.jid.user |
652 author_elt.addElement("name", content=author_name) | 691 author_elt.addElement("name", content=author_name) |
653 | 692 |
654 try: | 693 try: |
655 author_jid_s = data["author_jid"] | 694 author_jid_s = mb_data["author_jid"] |
656 except KeyError: | 695 except KeyError: |
657 author_jid_s = client.jid.userhost() | 696 author_jid_s = client.jid.userhost() |
658 author_elt.addElement("uri", content="xmpp:{}".format(author_jid_s)) | 697 author_elt.addElement("uri", content="xmpp:{}".format(author_jid_s)) |
659 | 698 |
660 try: | 699 try: |
661 author_jid_s = data["author_email"] | 700 author_jid_s = mb_data["author_email"] |
662 except KeyError: | 701 except KeyError: |
663 pass | 702 pass |
664 | 703 |
665 ## published/updated time ## | 704 ## published/updated time ## |
666 current_time = time.time() | 705 current_time = time.time() |
667 entry_elt.addElement( | 706 entry_elt.addElement( |
668 "updated", content=utils.xmpp_date(float(data.get("updated", current_time))) | 707 "updated", content=utils.xmpp_date(float(mb_data.get("updated", current_time))) |
669 ) | 708 ) |
670 entry_elt.addElement( | 709 entry_elt.addElement( |
671 "published", | 710 "published", |
672 content=utils.xmpp_date(float(data.get("published", current_time))), | 711 content=utils.xmpp_date(float(mb_data.get("published", current_time))), |
673 ) | 712 ) |
674 | 713 |
675 ## categories ## | 714 ## categories ## |
676 for tag in data.get('tags', []): | 715 for tag in mb_data.get('tags', []): |
677 category_elt = entry_elt.addElement("category") | 716 category_elt = entry_elt.addElement("category") |
678 category_elt["term"] = tag | 717 category_elt["term"] = tag |
679 | 718 |
680 ## id ## | 719 ## id ## |
681 entry_id = data.get( | 720 entry_id = mb_data.get( |
682 "id", | 721 "id", |
683 xmpp_uri.buildXMPPUri( | 722 xmpp_uri.buildXMPPUri( |
684 "pubsub", | 723 "pubsub", |
685 path=service.full() if service is not None else client.jid.userhost(), | 724 path=service.full() if service is not None else client.jid.userhost(), |
686 node=node, | 725 node=node, |
688 ), | 727 ), |
689 ) | 728 ) |
690 entry_elt.addElement("id", content=entry_id) # | 729 entry_elt.addElement("id", content=entry_id) # |
691 | 730 |
692 ## comments ## | 731 ## comments ## |
693 for comments_data in data.get('comments', []): | 732 for comments_data in mb_data.get('comments', []): |
694 link_elt = entry_elt.addElement("link") | 733 link_elt = entry_elt.addElement("link") |
695 # XXX: "uri" is set in self._manageComments if not already existing | 734 # XXX: "uri" is set in self._manageComments if not already existing |
696 link_elt["href"] = comments_data["uri"] | 735 link_elt["href"] = comments_data["uri"] |
697 link_elt["rel"] = "replies" | 736 link_elt["rel"] = "replies" |
698 link_elt["title"] = "comments" | 737 link_elt["title"] = "comments" |
699 | 738 |
739 extra = mb_data.get("extra", {}) | |
740 if "repeated" in extra: | |
741 try: | |
742 repeated = extra["repeated"] | |
743 link_elt = entry_elt.addElement("link") | |
744 link_elt["rel"] = "via" | |
745 link_elt["href"] = repeated["uri"] | |
746 except KeyError as e: | |
747 log.warning( | |
748 f"invalid repeated element({e}): {extra['repeated']}" | |
749 ) | |
750 | |
700 ## final item building ## | 751 ## final item building ## |
701 item_elt = pubsub.Item(id=item_id, payload=entry_elt) | 752 item_elt = pubsub.Item(id=item_id, payload=entry_elt) |
702 | 753 |
703 ## the trigger ## | 754 ## the trigger ## |
704 # if other plugins have things to add or change | 755 # if other plugins have things to add or change |
705 self.host.trigger.point( | 756 self.host.trigger.point( |
706 "XEP-0277_data2entry", client, data, entry_elt, item_elt | 757 "XEP-0277_data2entry", client, mb_data, entry_elt, item_elt |
707 ) | 758 ) |
708 | 759 |
709 return item_elt | 760 return item_elt |
710 | 761 |
711 ## publish/preview ## | 762 ## publish/preview ## |
923 await self._manageComments(client, data, service, node, item_id, access=None) | 974 await self._manageComments(client, data, service, node, item_id, access=None) |
924 except error.StanzaError: | 975 except error.StanzaError: |
925 log.warning("Can't create comments node for item {}".format(item_id)) | 976 log.warning("Can't create comments node for item {}".format(item_id)) |
926 item = await self.data2entry(client, data, item_id, service, node) | 977 item = await self.data2entry(client, data, item_id, service, node) |
927 return await self._p.publish(client, service, node, [item]) | 978 return await self._p.publish(client, service, node, [item]) |
979 | |
980 def _mbRepeat( | |
981 self, | |
982 service_s: str, | |
983 node: str, | |
984 item: str, | |
985 extra_s: str, | |
986 profile_key: str | |
987 ) -> defer.Deferred: | |
988 service = jid.JID(service_s) if service_s else None | |
989 node = node if node else NS_MICROBLOG | |
990 client = self.host.getClient(profile_key) | |
991 extra = data_format.deserialise(extra_s) | |
992 d = defer.ensureDeferred( | |
993 self.repeat(client, item, service, node, extra) | |
994 ) | |
995 # [repeat] can return None, and we always need a str | |
996 d.addCallback(lambda ret: ret or "") | |
997 return d | |
998 | |
999 async def repeat( | |
1000 self, | |
1001 client: SatXMPPEntity, | |
1002 item: str, | |
1003 service: Optional[jid.JID] = None, | |
1004 node: str = NS_MICROBLOG, | |
1005 extra: Optional[dict] = None, | |
1006 ) -> Optional[str]: | |
1007 """Re-publish a post from somewhere else | |
1008 | |
1009 This is a feature often name "share" or "boost", it is generally used to make a | |
1010 publication more visible by sharing it with our own audience | |
1011 """ | |
1012 if service is None: | |
1013 service = client.jid.userhostJID() | |
1014 | |
1015 # we first get the post to repeat | |
1016 items, __ = await self._p.getItems( | |
1017 client, | |
1018 service, | |
1019 node, | |
1020 item_ids = [item] | |
1021 ) | |
1022 if not items: | |
1023 raise exceptions.NotFound( | |
1024 f"no item found at node {node!r} on {service} with ID {item!r}" | |
1025 ) | |
1026 item_elt = items[0] | |
1027 try: | |
1028 entry_elt = next(item_elt.elements(NS_ATOM, "entry")) | |
1029 except StopIteration: | |
1030 raise exceptions.DataError( | |
1031 "post to repeat is not a XEP-0277 blog item" | |
1032 ) | |
1033 | |
1034 # we want to be sure that we have an author element | |
1035 try: | |
1036 author_elt = next(entry_elt.elements(NS_ATOM, "author")) | |
1037 except StopIteration: | |
1038 author_elt = entry_elt.addElement("author") | |
1039 | |
1040 try: | |
1041 next(author_elt.elements(NS_ATOM, "name")) | |
1042 except StopIteration: | |
1043 author_elt.addElement("name", content=service.user) | |
1044 | |
1045 try: | |
1046 next(author_elt.elements(NS_ATOM, "uri")) | |
1047 except StopIteration: | |
1048 entry_elt.addElement( | |
1049 "uri", content=xmpp_uri.buildXMPPUri(None, path=service.full()) | |
1050 ) | |
1051 | |
1052 # we add the link indicating that it's a repeated post | |
1053 link_elt = entry_elt.addElement("link") | |
1054 link_elt["rel"] = "via" | |
1055 link_elt["href"] = xmpp_uri.buildXMPPUri( | |
1056 "pubsub", path=service.full(), node=node, item=item | |
1057 ) | |
1058 | |
1059 return await self._p.sendItem( | |
1060 client, | |
1061 client.jid.userhostJID(), | |
1062 NS_MICROBLOG, | |
1063 entry_elt | |
1064 ) | |
928 | 1065 |
929 def _mbPreview(self, service, node, data, profile_key): | 1066 def _mbPreview(self, service, node, data, profile_key): |
930 service = jid.JID(service) if service else None | 1067 service = jid.JID(service) if service else None |
931 node = node if node else NS_MICROBLOG | 1068 node = node if node else NS_MICROBLOG |
932 client = self.host.getClient(profile_key) | 1069 client = self.host.getClient(profile_key) |