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
This commit is contained in:
Goffi 2022-07-13 12:15:04 +02:00
commit 2550264eda

View file

@ -20,7 +20,7 @@ import time
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
@ -113,6 +113,14 @@ class XEP_0277(object):
method=self._mbSend,
async_=True,
)
host.bridge.addMethod(
"mbRepeat",
".plugin",
in_sign="sssss",
out_sign="s",
method=self._mbRepeat,
async_=True,
)
host.bridge.addMethod(
"mbPreview",
".plugin",
@ -272,7 +280,11 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
# 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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
)
)
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
## 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 @@ class XEP_0277(object):
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 @@ class XEP_0277(object):
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