# HG changeset patch # User Goffi # Date 1657707304 -7200 # Node ID 5b192a5eb72d892dd4397c13569cbab6267cb36f # Parent 395b514526014d5050da01bd39f3ad05c931d595 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 diff -r 395b51452601 -r 5b192a5eb72d sat/plugins/plugin_xep_0277.py --- 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" 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