# HG changeset patch # User Goffi # Date 1657458975 -7200 # Node ID 201a22bfbb748125361878845d508eebe17aed99 # Parent 604b6acaee2222825eba7020c93159b452540807 component AP gateway: convert AP mention to XEP-0372 mentions: when a mentions are found in AP items (either with people specified directly as target, of with `mention` tags as in https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes), they are converted to XEP-0372 `mention` references. This is only done for Pubsub items (i.e. not for private messages). rel 369 diff -r 604b6acaee22 -r 201a22bfbb74 sat/plugins/plugin_comp_ap_gateway/__init__.py --- a/sat/plugins/plugin_comp_ap_gateway/__init__.py Sun Jul 10 15:16:15 2022 +0200 +++ b/sat/plugins/plugin_comp_ap_gateway/__init__.py Sun Jul 10 15:16:15 2022 +0200 @@ -70,6 +70,7 @@ TYPE_ITEM, TYPE_FOLLOWERS, TYPE_TOMBSTONE, + TYPE_MENTION, NS_AP_PUBLIC, PUBLIC_TUPLE ) @@ -89,7 +90,8 @@ C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [ "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277", "XEP-0292", "XEP-0329", - "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "XEP-0054" + "XEP-0372", "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", + "XEP-0054" ], C.PI_RECOMMENDATIONS: [], C.PI_MAIN: "APGateway", @@ -117,6 +119,7 @@ self._e = host.plugins["XEP-0106"] self._m = host.plugins["XEP-0277"] self._v = host.plugins["XEP-0292"] + self._refs = host.plugins["XEP-0372"] self._r = host.plugins["XEP-0424"] self._pps = host.plugins["XEP-0465"] self._c = host.plugins["PUBSUB_CACHE"] @@ -1694,6 +1697,8 @@ self, client: SatXMPPEntity, ap_item: dict, + targets: Dict[str, Set[str]], + mentions: List[Dict[str, str]], ) -> None: """We got an AP item which is a reply to an XMPP item""" in_reply_to = ap_item["inReplyTo"] @@ -1741,17 +1746,22 @@ else: __, item_elt = await self.apItem2MbDataAndElt(ap_item) await self._p.publish(client, comment_service, comment_node, [item_elt]) + await self.notifyMentions( + targets, mentions, comment_service, comment_node, item_elt["id"] + ) - def getAPItemTargets(self, item: Dict[str, Any]) -> Tuple[bool, Set[str], Set[str]]: + def getAPItemTargets( + self, + item: Dict[str, Any] + ) -> Tuple[bool, Dict[str, Set[str]], List[Dict[str, str]]]: """Retrieve targets of an AP item, and indicate if it's a public one @param item: AP object payload @return: Are returned: - is_public flag, indicating if the item is world-readable - - targets of the item - - targets of the items + - a dict mapping target type to targets """ - targets: Set[str] = set() + targets: Dict[str, Set[str]] = {} is_public = False # TODO: handle "audience" for key in ("to", "bto", "cc", "bcc"): @@ -1768,10 +1778,35 @@ continue if not self.isLocalURL(value): continue - targets.add(value) + target_type = self.parseAPURL(value)[0] + if target_type != TYPE_ACTOR: + log.debug(f"ignoring non actor type as a target: {href}") + else: + targets.setdefault(target_type, set()).add(value) - targets_types = {self.parseAPURL(t)[0] for t in targets} - return is_public, targets, targets_types + mentions = [] + tags = item.get("tag") + if tags: + for tag in tags: + if tag.get("type") != TYPE_MENTION: + continue + href = tag.get("href") + if not href: + log.warning('Missing "href" field from mention object: {tag!r}') + continue + if not self.isLocalURL(href): + continue + uri_type = self.parseAPURL(href)[0] + if uri_type != TYPE_ACTOR: + log.debug(f"ignoring non actor URI as a target: {href}") + continue + mention = {"uri": href} + mentions.append(mention) + name = tag.get("name") + if name: + mention["content"] = name + + return is_public, targets, mentions async def newAPItem( self, @@ -1786,21 +1821,22 @@ @param node: XMPP pubsub node @param item: AP object payload """ - is_public, targets, targets_types = self.getAPItemTargets(item) - if not is_public and targets_types == {TYPE_ACTOR}: + is_public, targets, mentions = self.getAPItemTargets(item) + if not is_public and targets.keys() == {TYPE_ACTOR}: # this is a direct message await self.handleMessageAPItem( - client, targets, destinee, item + client, targets, mentions, destinee, item ) else: await self.handlePubsubAPItem( - client, targets, destinee, node, item, is_public + client, targets, mentions, destinee, node, item, is_public ) async def handleMessageAPItem( self, client: SatXMPPEntity, - targets: Set[str], + targets: Dict[str, Set[str]], + mentions: List[Dict[str, str]], destinee: Optional[jid.JID], item: dict, ) -> None: @@ -1810,7 +1846,11 @@ @param destinee: jid of the destinee, @param item: AP object payload """ - targets_jids = {await self.getJIDFromId(t) for t in targets} + targets_jids = { + await self.getJIDFromId(t) + for t_set in targets.values() + for t in t_set + } if destinee is not None: targets_jids.add(destinee) mb_data = await self.apItem2MBdata(item) @@ -1826,10 +1866,53 @@ ) await defer.DeferredList(defer_l) + async def notifyMentions( + self, + targets: Dict[str, Set[str]], + mentions: List[Dict[str, str]], + service: jid.JID, + node: str, + item_id: str, + ) -> None: + """Send mention notifications to recipients and mentioned entities + + XEP-0372 (References) is used. + + Mentions are also sent to recipients as they are primary audience (see + https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes). + + """ + anchor = uri.buildXMPPUri("pubsub", path=service.full(), node=node, item=item_id) + seen = set() + # we start with explicit mentions because mentions' content will be used in the + # future to fill "begin" and "end" reference attributes (we can't do it at the + # moment as there is no way to specify the XML element to use in the blog item). + for mention in mentions: + mentioned_jid = await self.getJIDFromId(mention["uri"]) + self._refs.sendReference( + self.client, + to_jid=mentioned_jid, + anchor=anchor + ) + seen.add(mentioned_jid) + + remaining = { + await self.getJIDFromId(t) + for t_set in targets.values() + for t in t_set + } - seen + for target in remaining: + self._refs.sendReference( + self.client, + to_jid=target, + anchor=anchor + ) + async def handlePubsubAPItem( self, client: SatXMPPEntity, - targets: Set[str], + targets: Dict[str, Set[str]], + mentions: List[Dict[str, str]], destinee: Optional[jid.JID], node: str, item: dict, @@ -1853,7 +1936,8 @@ if in_reply_to and isinstance(in_reply_to, str): if self.isLocalURL(in_reply_to): # this is a reply to an XMPP item - return await self.newReplyToXMPPItem(client, item) + await self.newReplyToXMPPItem(client, item, targets, mentions) + return # this item is a reply to an AP item, we use or create a corresponding node # for comments @@ -1910,6 +1994,8 @@ [(subscription.subscriber, None, [item_elt])] ) + await self.notifyMentions(targets, mentions, service, node, item_elt["id"]) + async def newAPDeleteItem( self, client: SatXMPPEntity, diff -r 604b6acaee22 -r 201a22bfbb74 sat/plugins/plugin_comp_ap_gateway/constants.py --- a/sat/plugins/plugin_comp_ap_gateway/constants.py Sun Jul 10 15:16:15 2022 +0200 +++ b/sat/plugins/plugin_comp_ap_gateway/constants.py Sun Jul 10 15:16:15 2022 +0200 @@ -28,6 +28,7 @@ TYPE_FOLLOWING = "following" TYPE_ITEM = "item" TYPE_TOMBSTONE = "Tombstone" +TYPE_MENTION = "Mention" MEDIA_TYPE_AP = "application/activity+json" NS_AP_PUBLIC = "https://www.w3.org/ns/activitystreams#Public" # 3 values can be used, see https://www.w3.org/TR/activitypub/#public-addressing