comparison sat/plugins/plugin_comp_ap_gateway/__init__.py @ 3832:201a22bfbb74

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
author Goffi <goffi@goffi.org>
date Sun, 10 Jul 2022 15:16:15 +0200
parents 6329ee6b6df4
children 381340b9a9ee
comparison
equal deleted inserted replaced
3831:604b6acaee22 3832:201a22bfbb74
68 MEDIA_TYPE_AP, 68 MEDIA_TYPE_AP,
69 TYPE_ACTOR, 69 TYPE_ACTOR,
70 TYPE_ITEM, 70 TYPE_ITEM,
71 TYPE_FOLLOWERS, 71 TYPE_FOLLOWERS,
72 TYPE_TOMBSTONE, 72 TYPE_TOMBSTONE,
73 TYPE_MENTION,
73 NS_AP_PUBLIC, 74 NS_AP_PUBLIC,
74 PUBLIC_TUPLE 75 PUBLIC_TUPLE
75 ) 76 )
76 from .http_server import HTTPServer 77 from .http_server import HTTPServer
77 from .pubsub_service import APPubsubService 78 from .pubsub_service import APPubsubService
87 C.PI_MODES: [C.PLUG_MODE_COMPONENT], 88 C.PI_MODES: [C.PLUG_MODE_COMPONENT],
88 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT, 89 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
89 C.PI_PROTOCOLS: [], 90 C.PI_PROTOCOLS: [],
90 C.PI_DEPENDENCIES: [ 91 C.PI_DEPENDENCIES: [
91 "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277", "XEP-0292", "XEP-0329", 92 "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277", "XEP-0292", "XEP-0329",
92 "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "XEP-0054" 93 "XEP-0372", "XEP-0424", "XEP-0465", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY",
94 "XEP-0054"
93 ], 95 ],
94 C.PI_RECOMMENDATIONS: [], 96 C.PI_RECOMMENDATIONS: [],
95 C.PI_MAIN: "APGateway", 97 C.PI_MAIN: "APGateway",
96 C.PI_HANDLER: C.BOOL_TRUE, 98 C.PI_HANDLER: C.BOOL_TRUE,
97 C.PI_DESCRIPTION: _( 99 C.PI_DESCRIPTION: _(
115 self._p = host.plugins["XEP-0060"] 117 self._p = host.plugins["XEP-0060"]
116 self._a = host.plugins["XEP-0084"] 118 self._a = host.plugins["XEP-0084"]
117 self._e = host.plugins["XEP-0106"] 119 self._e = host.plugins["XEP-0106"]
118 self._m = host.plugins["XEP-0277"] 120 self._m = host.plugins["XEP-0277"]
119 self._v = host.plugins["XEP-0292"] 121 self._v = host.plugins["XEP-0292"]
122 self._refs = host.plugins["XEP-0372"]
120 self._r = host.plugins["XEP-0424"] 123 self._r = host.plugins["XEP-0424"]
121 self._pps = host.plugins["XEP-0465"] 124 self._pps = host.plugins["XEP-0465"]
122 self._c = host.plugins["PUBSUB_CACHE"] 125 self._c = host.plugins["PUBSUB_CACHE"]
123 self._t = host.plugins["TEXT_SYNTAXES"] 126 self._t = host.plugins["TEXT_SYNTAXES"]
124 self._i = host.plugins["IDENTITY"] 127 self._i = host.plugins["IDENTITY"]
1692 1695
1693 async def newReplyToXMPPItem( 1696 async def newReplyToXMPPItem(
1694 self, 1697 self,
1695 client: SatXMPPEntity, 1698 client: SatXMPPEntity,
1696 ap_item: dict, 1699 ap_item: dict,
1700 targets: Dict[str, Set[str]],
1701 mentions: List[Dict[str, str]],
1697 ) -> None: 1702 ) -> None:
1698 """We got an AP item which is a reply to an XMPP item""" 1703 """We got an AP item which is a reply to an XMPP item"""
1699 in_reply_to = ap_item["inReplyTo"] 1704 in_reply_to = ap_item["inReplyTo"]
1700 url_type, url_args = self.parseAPURL(in_reply_to) 1705 url_type, url_args = self.parseAPURL(in_reply_to)
1701 if url_type != "item": 1706 if url_type != "item":
1739 log.info(f"{ppElt(parent_item_elt.toXml())}") 1744 log.info(f"{ppElt(parent_item_elt.toXml())}")
1740 raise NotImplemented() 1745 raise NotImplemented()
1741 else: 1746 else:
1742 __, item_elt = await self.apItem2MbDataAndElt(ap_item) 1747 __, item_elt = await self.apItem2MbDataAndElt(ap_item)
1743 await self._p.publish(client, comment_service, comment_node, [item_elt]) 1748 await self._p.publish(client, comment_service, comment_node, [item_elt])
1744 1749 await self.notifyMentions(
1745 def getAPItemTargets(self, item: Dict[str, Any]) -> Tuple[bool, Set[str], Set[str]]: 1750 targets, mentions, comment_service, comment_node, item_elt["id"]
1751 )
1752
1753 def getAPItemTargets(
1754 self,
1755 item: Dict[str, Any]
1756 ) -> Tuple[bool, Dict[str, Set[str]], List[Dict[str, str]]]:
1746 """Retrieve targets of an AP item, and indicate if it's a public one 1757 """Retrieve targets of an AP item, and indicate if it's a public one
1747 1758
1748 @param item: AP object payload 1759 @param item: AP object payload
1749 @return: Are returned: 1760 @return: Are returned:
1750 - is_public flag, indicating if the item is world-readable 1761 - is_public flag, indicating if the item is world-readable
1751 - targets of the item 1762 - a dict mapping target type to targets
1752 - targets of the items 1763 """
1753 """ 1764 targets: Dict[str, Set[str]] = {}
1754 targets: Set[str] = set()
1755 is_public = False 1765 is_public = False
1756 # TODO: handle "audience" 1766 # TODO: handle "audience"
1757 for key in ("to", "bto", "cc", "bcc"): 1767 for key in ("to", "bto", "cc", "bcc"):
1758 values = item.get(key) 1768 values = item.get(key)
1759 if not values: 1769 if not values:
1766 continue 1776 continue
1767 if not value: 1777 if not value:
1768 continue 1778 continue
1769 if not self.isLocalURL(value): 1779 if not self.isLocalURL(value):
1770 continue 1780 continue
1771 targets.add(value) 1781 target_type = self.parseAPURL(value)[0]
1772 1782 if target_type != TYPE_ACTOR:
1773 targets_types = {self.parseAPURL(t)[0] for t in targets} 1783 log.debug(f"ignoring non actor type as a target: {href}")
1774 return is_public, targets, targets_types 1784 else:
1785 targets.setdefault(target_type, set()).add(value)
1786
1787 mentions = []
1788 tags = item.get("tag")
1789 if tags:
1790 for tag in tags:
1791 if tag.get("type") != TYPE_MENTION:
1792 continue
1793 href = tag.get("href")
1794 if not href:
1795 log.warning('Missing "href" field from mention object: {tag!r}')
1796 continue
1797 if not self.isLocalURL(href):
1798 continue
1799 uri_type = self.parseAPURL(href)[0]
1800 if uri_type != TYPE_ACTOR:
1801 log.debug(f"ignoring non actor URI as a target: {href}")
1802 continue
1803 mention = {"uri": href}
1804 mentions.append(mention)
1805 name = tag.get("name")
1806 if name:
1807 mention["content"] = name
1808
1809 return is_public, targets, mentions
1775 1810
1776 async def newAPItem( 1811 async def newAPItem(
1777 self, 1812 self,
1778 client: SatXMPPEntity, 1813 client: SatXMPPEntity,
1779 destinee: Optional[jid.JID], 1814 destinee: Optional[jid.JID],
1784 1819
1785 @param destinee: jid of the destinee, 1820 @param destinee: jid of the destinee,
1786 @param node: XMPP pubsub node 1821 @param node: XMPP pubsub node
1787 @param item: AP object payload 1822 @param item: AP object payload
1788 """ 1823 """
1789 is_public, targets, targets_types = self.getAPItemTargets(item) 1824 is_public, targets, mentions = self.getAPItemTargets(item)
1790 if not is_public and targets_types == {TYPE_ACTOR}: 1825 if not is_public and targets.keys() == {TYPE_ACTOR}:
1791 # this is a direct message 1826 # this is a direct message
1792 await self.handleMessageAPItem( 1827 await self.handleMessageAPItem(
1793 client, targets, destinee, item 1828 client, targets, mentions, destinee, item
1794 ) 1829 )
1795 else: 1830 else:
1796 await self.handlePubsubAPItem( 1831 await self.handlePubsubAPItem(
1797 client, targets, destinee, node, item, is_public 1832 client, targets, mentions, destinee, node, item, is_public
1798 ) 1833 )
1799 1834
1800 async def handleMessageAPItem( 1835 async def handleMessageAPItem(
1801 self, 1836 self,
1802 client: SatXMPPEntity, 1837 client: SatXMPPEntity,
1803 targets: Set[str], 1838 targets: Dict[str, Set[str]],
1839 mentions: List[Dict[str, str]],
1804 destinee: Optional[jid.JID], 1840 destinee: Optional[jid.JID],
1805 item: dict, 1841 item: dict,
1806 ) -> None: 1842 ) -> None:
1807 """Parse and deliver direct AP items translating to XMPP messages 1843 """Parse and deliver direct AP items translating to XMPP messages
1808 1844
1809 @param targets: actors where the item must be delivered 1845 @param targets: actors where the item must be delivered
1810 @param destinee: jid of the destinee, 1846 @param destinee: jid of the destinee,
1811 @param item: AP object payload 1847 @param item: AP object payload
1812 """ 1848 """
1813 targets_jids = {await self.getJIDFromId(t) for t in targets} 1849 targets_jids = {
1850 await self.getJIDFromId(t)
1851 for t_set in targets.values()
1852 for t in t_set
1853 }
1814 if destinee is not None: 1854 if destinee is not None:
1815 targets_jids.add(destinee) 1855 targets_jids.add(destinee)
1816 mb_data = await self.apItem2MBdata(item) 1856 mb_data = await self.apItem2MBdata(item)
1817 defer_l = [] 1857 defer_l = []
1818 for target_jid in targets_jids: 1858 for target_jid in targets_jids:
1824 extra={"origin_id": mb_data["id"]} 1864 extra={"origin_id": mb_data["id"]}
1825 ) 1865 )
1826 ) 1866 )
1827 await defer.DeferredList(defer_l) 1867 await defer.DeferredList(defer_l)
1828 1868
1869 async def notifyMentions(
1870 self,
1871 targets: Dict[str, Set[str]],
1872 mentions: List[Dict[str, str]],
1873 service: jid.JID,
1874 node: str,
1875 item_id: str,
1876 ) -> None:
1877 """Send mention notifications to recipients and mentioned entities
1878
1879 XEP-0372 (References) is used.
1880
1881 Mentions are also sent to recipients as they are primary audience (see
1882 https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes).
1883
1884 """
1885 anchor = uri.buildXMPPUri("pubsub", path=service.full(), node=node, item=item_id)
1886 seen = set()
1887 # we start with explicit mentions because mentions' content will be used in the
1888 # future to fill "begin" and "end" reference attributes (we can't do it at the
1889 # moment as there is no way to specify the XML element to use in the blog item).
1890 for mention in mentions:
1891 mentioned_jid = await self.getJIDFromId(mention["uri"])
1892 self._refs.sendReference(
1893 self.client,
1894 to_jid=mentioned_jid,
1895 anchor=anchor
1896 )
1897 seen.add(mentioned_jid)
1898
1899 remaining = {
1900 await self.getJIDFromId(t)
1901 for t_set in targets.values()
1902 for t in t_set
1903 } - seen
1904 for target in remaining:
1905 self._refs.sendReference(
1906 self.client,
1907 to_jid=target,
1908 anchor=anchor
1909 )
1910
1829 async def handlePubsubAPItem( 1911 async def handlePubsubAPItem(
1830 self, 1912 self,
1831 client: SatXMPPEntity, 1913 client: SatXMPPEntity,
1832 targets: Set[str], 1914 targets: Dict[str, Set[str]],
1915 mentions: List[Dict[str, str]],
1833 destinee: Optional[jid.JID], 1916 destinee: Optional[jid.JID],
1834 node: str, 1917 node: str,
1835 item: dict, 1918 item: dict,
1836 public: bool 1919 public: bool
1837 ) -> None: 1920 ) -> None:
1851 if in_reply_to and isinstance(in_reply_to, list): 1934 if in_reply_to and isinstance(in_reply_to, list):
1852 in_reply_to = in_reply_to[0] 1935 in_reply_to = in_reply_to[0]
1853 if in_reply_to and isinstance(in_reply_to, str): 1936 if in_reply_to and isinstance(in_reply_to, str):
1854 if self.isLocalURL(in_reply_to): 1937 if self.isLocalURL(in_reply_to):
1855 # this is a reply to an XMPP item 1938 # this is a reply to an XMPP item
1856 return await self.newReplyToXMPPItem(client, item) 1939 await self.newReplyToXMPPItem(client, item, targets, mentions)
1940 return
1857 1941
1858 # this item is a reply to an AP item, we use or create a corresponding node 1942 # this item is a reply to an AP item, we use or create a corresponding node
1859 # for comments 1943 # for comments
1860 parent_node, __ = await self.getCommentsNodes(item["id"], in_reply_to) 1944 parent_node, __ = await self.getCommentsNodes(item["id"], in_reply_to)
1861 node = parent_node or node 1945 node = parent_node or node
1908 service, 1992 service,
1909 node, 1993 node,
1910 [(subscription.subscriber, None, [item_elt])] 1994 [(subscription.subscriber, None, [item_elt])]
1911 ) 1995 )
1912 1996
1997 await self.notifyMentions(targets, mentions, service, node, item_elt["id"])
1998
1913 async def newAPDeleteItem( 1999 async def newAPDeleteItem(
1914 self, 2000 self,
1915 client: SatXMPPEntity, 2001 client: SatXMPPEntity,
1916 destinee: Optional[jid.JID], 2002 destinee: Optional[jid.JID],
1917 node: str, 2003 node: str,