changeset 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 395b51452601
children b337d7da72e5
files sat/plugins/plugin_xep_0277.py
diffstat 1 files changed, 168 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_xep_0277.py	Sun Jul 10 16:40:16 2022 +0200
+++ b/sat/plugins/plugin_xep_0277.py	Wed Jul 13 12:15:04 2022 +0200
@@ -20,7 +20,7 @@
 import dateutil
 import calendar
 from secrets import token_urlsafe
-from typing import Optional
+from typing import Optional, Dict, Union, Any
 from functools import partial
 
 import shortuuid
@@ -114,6 +114,14 @@
             async_=True,
         )
         host.bridge.addMethod(
+            "mbRepeat",
+            ".plugin",
+            in_sign="sssss",
+            out_sign="s",
+            method=self._mbRepeat,
+            async_=True,
+        )
+        host.bridge.addMethod(
             "mbPreview",
             ".plugin",
             in_sign="ssss",
@@ -272,7 +280,11 @@
         if service is None:
             service = client.jid.userhostJID()
 
-        microblog_data = {"service": service.full()}
+        extra: Dict[str, Any] = {}
+        microblog_data: Dict[str, Any] = {
+            "service": service.full(),
+            "extra": extra
+        }
 
         def check_conflict(key, increment=False):
             """Check if key is already in microblog data
@@ -440,10 +452,8 @@
         # links
         comments = microblog_data['comments'] = []
         for link_elt in entry_elt.elements(NS_ATOM, "link"):
-            if (
-                link_elt.getAttribute("rel") == "replies"
-                and link_elt.getAttribute("title") == "comments"
-            ):
+            rel = link_elt.getAttribute("rel")
+            if (rel == "replies" and link_elt.getAttribute("title") == "comments"):
                 uri = link_elt["href"]
                 comments_data = {
                     "uri": uri,
@@ -457,8 +467,36 @@
                     comments_data["service"] = comment_service.full()
                     comments_data["node"] = comment_node
                 comments.append(comments_data)
+            elif rel == "via":
+                href = link_elt.getAttribute("href")
+                if not href:
+                    log.warning(
+                        f'missing href in "via" <link> element: {link_elt.toXml()}'
+                    )
+                    continue
+                try:
+                    repeater_jid = jid.JID(item_elt["publisher"])
+                except (KeyError, RuntimeError):
+                    try:
+                        # we look for stanza element which is at the root, meaning that it
+                        # has not parent
+                        top_elt = item_elt.parent
+                        while top_elt.parent is not None:
+                            top_elt = top_elt.parent
+                        repeater_jid = jid.JID(top_elt["from"])
+                    except (AttributeError, RuntimeError):
+                        # we should always have either the "publisher" attribute or the
+                        # stanza available
+                        log.error(
+                            f"Can't find repeater of the post: {item_elt.toXml()}"
+                        )
+                        continue
+
+                extra["repeated"] = {
+                    "by": repeater_jid.full(),
+                    "uri": href
+                }
             else:
-                rel = link_elt.getAttribute("rel", "")
                 title = link_elt.getAttribute("title", "")
                 href = link_elt.getAttribute("href", "")
                 log.warning(
@@ -516,12 +554,13 @@
                 elif jid.JID(publisher).userhostJID() == jid.JID(uri).userhostJID():
                     microblog_data["author_jid_verified"] = True
                 else:
-                    log.warning(
-                        "item atom:uri differ from publisher attribute, spoofing "
-                        "attempt ? atom:uri = {} publisher = {}".format(
-                            uri, item_elt.getAttribute("publisher")
+                    if "repeated" not in extra:
+                        log.warning(
+                            "item atom:uri differ from publisher attribute, spoofing "
+                            "attempt ? atom:uri = {} publisher = {}".format(
+                                uri, item_elt.getAttribute("publisher")
+                            )
                         )
-                    )
                     microblog_data["author_jid_verified"] = False
             # email
             try:
@@ -555,10 +594,10 @@
 
         defer.returnValue(microblog_data)
 
-    async def data2entry(self, client, data, item_id, service, node):
+    async def data2entry(self, client, mb_data, item_id, service, node):
         """Convert a data dict to en entry usable to create an item
 
-        @param data: data dict as given by bridge method.
+        @param mb_data: data dict as given by bridge method.
         @param item_id(unicode): id of the item to use
         @param service(jid.JID, None): pubsub service where the item is sent
             Needed to construct Atom id
@@ -569,8 +608,8 @@
         entry_elt = domish.Element((NS_ATOM, "entry"))
 
         ## language ##
-        if "language" in data:
-            entry_elt[(C.NS_XML, "lang")] = data["language"].strip()
+        if "language" in mb_data:
+            entry_elt[(C.NS_XML, "lang")] = mb_data["language"].strip()
 
         ## content and title ##
         synt = self.host.plugins["TEXT_SYNTAXES"]
@@ -578,14 +617,14 @@
         for elem_name in ("title", "content"):
             for type_ in ["", "_rich", "_xhtml"]:
                 attr = "{}{}".format(elem_name, type_)
-                if attr in data:
+                if attr in mb_data:
                     elem = entry_elt.addElement(elem_name)
                     if type_:
                         if type_ == "_rich":  # convert input from current syntax to XHTML
                             xml_content = await synt.convert(
-                                data[attr], synt.getCurrentSyntax(client.profile), "XHTML"
+                                mb_data[attr], synt.getCurrentSyntax(client.profile), "XHTML"
                             )
-                            if "{}_xhtml".format(elem_name) in data:
+                            if "{}_xhtml".format(elem_name) in mb_data:
                                 raise failure.Failure(
                                     exceptions.DataError(
                                         _(
@@ -594,7 +633,7 @@
                                     )
                                 )
                         else:
-                            xml_content = data[attr]
+                            xml_content = mb_data[attr]
 
                         div_elt = xml_tools.ElementParser()(
                             xml_content, namespace=C.NS_XHTML
@@ -610,7 +649,7 @@
                             div_elt = wrap_div_elt
                         elem.addChild(div_elt)
                         elem["type"] = "xhtml"
-                        if elem_name not in data:
+                        if elem_name not in mb_data:
                             # there is raw text content, which is mandatory
                             # so we create one from xhtml content
                             elem_txt = entry_elt.addElement(elem_name)
@@ -626,7 +665,7 @@
                             elem_txt["type"] = "text"
 
                     else:  # raw text only needs to be escaped to get HTML-safe sequence
-                        elem.addContent(data[attr])
+                        elem.addContent(mb_data[attr])
                         elem["type"] = "text"
 
         try:
@@ -645,40 +684,40 @@
         ## author ##
         author_elt = entry_elt.addElement("author")
         try:
-            author_name = data["author"]
+            author_name = mb_data["author"]
         except KeyError:
             # FIXME: must use better name
             author_name = client.jid.user
         author_elt.addElement("name", content=author_name)
 
         try:
-            author_jid_s = data["author_jid"]
+            author_jid_s = mb_data["author_jid"]
         except KeyError:
             author_jid_s = client.jid.userhost()
         author_elt.addElement("uri", content="xmpp:{}".format(author_jid_s))
 
         try:
-            author_jid_s = data["author_email"]
+            author_jid_s = mb_data["author_email"]
         except KeyError:
             pass
 
         ## published/updated time ##
         current_time = time.time()
         entry_elt.addElement(
-            "updated", content=utils.xmpp_date(float(data.get("updated", current_time)))
+            "updated", content=utils.xmpp_date(float(mb_data.get("updated", current_time)))
         )
         entry_elt.addElement(
             "published",
-            content=utils.xmpp_date(float(data.get("published", current_time))),
+            content=utils.xmpp_date(float(mb_data.get("published", current_time))),
         )
 
         ## categories ##
-        for tag in data.get('tags', []):
+        for tag in mb_data.get('tags', []):
             category_elt = entry_elt.addElement("category")
             category_elt["term"] = tag
 
         ## id ##
-        entry_id = data.get(
+        entry_id = mb_data.get(
             "id",
             xmpp_uri.buildXMPPUri(
                 "pubsub",
@@ -690,20 +729,32 @@
         entry_elt.addElement("id", content=entry_id)  #
 
         ## comments ##
-        for comments_data in data.get('comments', []):
+        for comments_data in mb_data.get('comments', []):
             link_elt = entry_elt.addElement("link")
             # XXX: "uri" is set in self._manageComments if not already existing
             link_elt["href"] = comments_data["uri"]
             link_elt["rel"] = "replies"
             link_elt["title"] = "comments"
 
+        extra = mb_data.get("extra", {})
+        if "repeated" in extra:
+            try:
+                repeated = extra["repeated"]
+                link_elt = entry_elt.addElement("link")
+                link_elt["rel"] = "via"
+                link_elt["href"] = repeated["uri"]
+            except KeyError as e:
+                log.warning(
+                    f"invalid repeated element({e}): {extra['repeated']}"
+                )
+
         ## final item building ##
         item_elt = pubsub.Item(id=item_id, payload=entry_elt)
 
         ## the trigger ##
         # if other plugins have things to add or change
         self.host.trigger.point(
-            "XEP-0277_data2entry", client, data, entry_elt, item_elt
+            "XEP-0277_data2entry", client, mb_data, entry_elt, item_elt
         )
 
         return item_elt
@@ -926,6 +977,92 @@
         item = await self.data2entry(client, data, item_id, service, node)
         return await self._p.publish(client, service, node, [item])
 
+    def _mbRepeat(
+            self,
+            service_s: str,
+            node: str,
+            item: str,
+            extra_s: str,
+            profile_key: str
+    ) -> defer.Deferred:
+        service = jid.JID(service_s) if service_s else None
+        node = node if node else NS_MICROBLOG
+        client = self.host.getClient(profile_key)
+        extra = data_format.deserialise(extra_s)
+        d = defer.ensureDeferred(
+            self.repeat(client, item, service, node, extra)
+        )
+        # [repeat] can return None, and we always need a str
+        d.addCallback(lambda ret: ret or "")
+        return d
+
+    async def repeat(
+        self,
+        client: SatXMPPEntity,
+        item: str,
+        service: Optional[jid.JID] = None,
+        node: str = NS_MICROBLOG,
+        extra: Optional[dict] = None,
+    ) -> Optional[str]:
+        """Re-publish a post from somewhere else
+
+        This is a feature often name "share" or "boost", it is generally used to make a
+        publication more visible by sharing it with our own audience
+        """
+        if service is None:
+            service = client.jid.userhostJID()
+
+        # we first get the post to repeat
+        items, __ = await self._p.getItems(
+            client,
+            service,
+            node,
+            item_ids = [item]
+        )
+        if not items:
+            raise exceptions.NotFound(
+                f"no item found at node {node!r} on {service} with ID {item!r}"
+            )
+        item_elt = items[0]
+        try:
+            entry_elt = next(item_elt.elements(NS_ATOM, "entry"))
+        except StopIteration:
+            raise exceptions.DataError(
+                "post to repeat is not a XEP-0277 blog item"
+            )
+
+        # we want to be sure that we have an author element
+        try:
+            author_elt = next(entry_elt.elements(NS_ATOM, "author"))
+        except StopIteration:
+            author_elt = entry_elt.addElement("author")
+
+        try:
+            next(author_elt.elements(NS_ATOM, "name"))
+        except StopIteration:
+            author_elt.addElement("name", content=service.user)
+
+        try:
+            next(author_elt.elements(NS_ATOM, "uri"))
+        except StopIteration:
+            entry_elt.addElement(
+                "uri", content=xmpp_uri.buildXMPPUri(None, path=service.full())
+            )
+
+        # we add the link indicating that it's a repeated post
+        link_elt = entry_elt.addElement("link")
+        link_elt["rel"] = "via"
+        link_elt["href"] = xmpp_uri.buildXMPPUri(
+            "pubsub", path=service.full(), node=node, item=item
+        )
+
+        return await self._p.sendItem(
+            client,
+            client.jid.userhostJID(),
+            NS_MICROBLOG,
+            entry_elt
+        )
+
     def _mbPreview(self, service, node, data, profile_key):
         service = jid.JID(service) if service else None
         node = node if node else NS_MICROBLOG