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)