comparison sat/plugins/plugin_comp_ap_gateway/__init__.py @ 3833:381340b9a9ee

component AP gateway: convert XMPP mentions to AP: When a XEP-0372 mention is received, the linked pubsub item is looked after in cache, and if found, it is send to mentioned entity with `mention` tag added. However, this doesn't work in some cases (see incoming doc for details). To work around that, `@user@server.tld` type mention are also scanned in body, and mentions are added when found (this can be disabled with `auto_mentions` setting). Mention are only scanned in "public" messages, i.e. for pubsub items, and not direct messages. rel 369
author Goffi <goffi@goffi.org>
date Sun, 10 Jul 2022 16:15:06 +0200
parents 201a22bfbb74
children 943901372eba
comparison
equal deleted inserted replaced
3832:201a22bfbb74 3833:381340b9a9ee
58 from .constants import ( 58 from .constants import (
59 ACTIVITY_OBJECT_MANDATORY, 59 ACTIVITY_OBJECT_MANDATORY,
60 ACTIVITY_TARGET_MANDATORY, 60 ACTIVITY_TARGET_MANDATORY,
61 ACTIVITY_TYPES, 61 ACTIVITY_TYPES,
62 ACTIVITY_TYPES_LOWER, 62 ACTIVITY_TYPES_LOWER,
63 AP_MB_MAP,
64 COMMENTS_MAX_PARENTS, 63 COMMENTS_MAX_PARENTS,
65 CONF_SECTION, 64 CONF_SECTION,
66 IMPORT_NAME, 65 IMPORT_NAME,
67 LRU_MAX_SIZE, 66 LRU_MAX_SIZE,
68 MEDIA_TYPE_AP, 67 MEDIA_TYPE_AP,
72 TYPE_TOMBSTONE, 71 TYPE_TOMBSTONE,
73 TYPE_MENTION, 72 TYPE_MENTION,
74 NS_AP_PUBLIC, 73 NS_AP_PUBLIC,
75 PUBLIC_TUPLE 74 PUBLIC_TUPLE
76 ) 75 )
76 from .regex import RE_MENTION
77 from .http_server import HTTPServer 77 from .http_server import HTTPServer
78 from .pubsub_service import APPubsubService 78 from .pubsub_service import APPubsubService
79 79
80 80
81 log = getLogger(__name__) 81 log = getLogger(__name__)
129 "", items_cb=self._itemsReceived 129 "", items_cb=self._itemsReceived
130 ) 130 )
131 self.pubsub_service = APPubsubService(self) 131 self.pubsub_service = APPubsubService(self)
132 host.trigger.add("messageReceived", self._messageReceivedTrigger, priority=-1000) 132 host.trigger.add("messageReceived", self._messageReceivedTrigger, priority=-1000)
133 host.trigger.add("XEP-0424_retractReceived", self._onMessageRetract) 133 host.trigger.add("XEP-0424_retractReceived", self._onMessageRetract)
134 host.trigger.add("XEP-0372_ref_received", self._onReferenceReceived)
134 135
135 host.bridge.addMethod( 136 host.bridge.addMethod(
136 "APSend", 137 "APSend",
137 ".plugin", 138 ".plugin",
138 in_sign="sss", 139 in_sign="sss",
220 self.ap_path = self.host.memory.getConfig(CONF_SECTION, 'ap_path', '_ap') 221 self.ap_path = self.host.memory.getConfig(CONF_SECTION, 'ap_path', '_ap')
221 self.base_ap_url = parse.urljoin(f"https://{self.public_url}", f"{self.ap_path}/") 222 self.base_ap_url = parse.urljoin(f"https://{self.public_url}", f"{self.ap_path}/")
222 # True (default) if we provide gateway only to entities/services from our server 223 # True (default) if we provide gateway only to entities/services from our server
223 self.local_only = C.bool( 224 self.local_only = C.bool(
224 self.host.memory.getConfig(CONF_SECTION, 'local_only', C.BOOL_TRUE) 225 self.host.memory.getConfig(CONF_SECTION, 'local_only', C.BOOL_TRUE)
226 )
227 # if True (default), mention will be parsed in non-private content coming from
228 # XMPP. This is necessary as XEP-0372 are coming separately from item where the
229 # mention is done, which is hard to impossible to translate to ActivityPub (where
230 # mention specified inside the item directly). See documentation for details.
231 self.auto_mentions = C.bool(
232 self.host.memory.getConfig(CONF_SECTION, "auto_mentions", C.BOOL_TRUE)
225 ) 233 )
226 234
227 # HTTP server launch 235 # HTTP server launch
228 self.server = HTTPServer(self) 236 self.server = HTTPServer(self)
229 if connection_type == 'http': 237 if connection_type == 'http':
1301 item_id = ap_object.get("id") 1309 item_id = ap_object.get("id")
1302 if not item_id: 1310 if not item_id:
1303 log.warning(f'No "id" found in AP item: {ap_object!r}') 1311 log.warning(f'No "id" found in AP item: {ap_object!r}')
1304 raise exceptions.DataError 1312 raise exceptions.DataError
1305 mb_data = {"id": item_id} 1313 mb_data = {"id": item_id}
1306 for ap_key, mb_key in AP_MB_MAP.items():
1307 data = ap_object.get(ap_key)
1308 if data is None:
1309 continue
1310 mb_data[mb_key] = data
1311 1314
1312 # content 1315 # content
1313 try: 1316 try:
1314 language, content_xhtml = ap_object["contentMap"].popitem() 1317 language, content_xhtml = ap_object["contentMap"].popitem()
1315 except (KeyError, AttributeError): 1318 except (KeyError, AttributeError):
1316 try: 1319 try:
1317 mb_data["content_xhtml"] = mb_data["content"] 1320 mb_data["content_xhtml"] = ap_object["content"]
1318 except KeyError: 1321 except KeyError:
1319 log.warning(f"no content found:\n{ap_object!r}") 1322 log.warning(f"no content found:\n{ap_object!r}")
1320 raise exceptions.DataError 1323 raise exceptions.DataError
1321 else: 1324 else:
1322 mb_data["language"] = language 1325 mb_data["language"] = language
1323 mb_data["content_xhtml"] = content_xhtml 1326 mb_data["content_xhtml"] = content_xhtml
1324 if not mb_data.get("content"): 1327
1325 mb_data["content"] = await self._t.convert( 1328 mb_data["content"] = await self._t.convert(
1326 content_xhtml, 1329 mb_data["content_xhtml"],
1327 self._t.SYNTAX_XHTML, 1330 self._t.SYNTAX_XHTML,
1328 self._t.SYNTAX_TEXT, 1331 self._t.SYNTAX_TEXT,
1329 False, 1332 False,
1330 ) 1333 )
1331 1334
1332 # author 1335 # author
1333 if is_activity: 1336 if is_activity:
1334 authors = await self.apGetActors(ap_item, "actor") 1337 authors = await self.apGetActors(ap_item, "actor")
1335 else: 1338 else:
1479 if language: 1482 if language:
1480 ap_object["contentMap"] = {language: ap_object["content"]} 1483 ap_object["contentMap"] = {language: ap_object["content"]}
1481 1484
1482 if public: 1485 if public:
1483 ap_object["to"] = [NS_AP_PUBLIC] 1486 ap_object["to"] = [NS_AP_PUBLIC]
1487 if self.auto_mentions:
1488 for m in RE_MENTION.finditer(ap_object["content"]):
1489 mention = m.group()
1490 mentioned = mention[1:]
1491 __, m_host = mentioned.split("@", 1)
1492 if m_host in (self.public_url, self.client.jid.host):
1493 # we ignore mention of local users, they should be sent as XMPP
1494 # references
1495 continue
1496 try:
1497 mentioned_id = await self.getAPActorIdFromAccount(mentioned)
1498 except Exception as e:
1499 log.warning(f"Can't add mention to {mentioned!r}: {e}")
1500 else:
1501 ap_object["to"].append(mentioned_id)
1502 ap_object.setdefault("tag", []).append({
1503 "type": TYPE_MENTION,
1504 "href": mentioned_id,
1505 "name": mention,
1506 })
1484 try: 1507 try:
1485 node = mb_data["node"] 1508 node = mb_data["node"]
1486 service = jid.JID(mb_data["service"]) 1509 service = jid.JID(mb_data["service"])
1487 except KeyError: 1510 except KeyError:
1488 # node and service must always be specified when this method is used 1511 # node and service must always be specified when this method is used
1691 f"unexpected return code while sending AP item: {resp.code}\n{text}\n" 1714 f"unexpected return code while sending AP item: {resp.code}\n{text}\n"
1692 f"{pformat(ap_item)}" 1715 f"{pformat(ap_item)}"
1693 ) 1716 )
1694 return False 1717 return False
1695 1718
1719 async def _onReferenceReceived(
1720 self,
1721 client: SatXMPPEntity,
1722 message_elt: domish.Element,
1723 reference_data: Dict[str, Union[str, int]]
1724 ) -> bool:
1725 parsed_uri: dict = reference_data.get("parsed_uri")
1726 if not parsed_uri:
1727 log.warning(f"no parsed URI available in reference {reference_data}")
1728 return False
1729
1730 try:
1731 mentioned = jid.JID(parsed_uri["path"])
1732 except RuntimeError:
1733 log.warning(f"invalid target: {reference_data['uri']}")
1734 return False
1735
1736 if mentioned.host != self.client.jid.full() or not mentioned.user:
1737 log.warning(
1738 f"ignoring mentioned user {mentioned}, it's not a JID mapping an AP "
1739 "account"
1740 )
1741 return False
1742
1743 ap_account = self._e.unescape(mentioned.user)
1744 actor_id = await self.getAPActorIdFromAccount(ap_account)
1745
1746 parsed_anchor: dict = reference_data.get("parsed_anchor")
1747 if not parsed_anchor:
1748 log.warning(f"no XMPP anchor, ignoring reference {reference_data!r}")
1749 return False
1750
1751 if parsed_anchor["type"] != "pubsub":
1752 log.warning(
1753 f"ignoring reference with non pubsub anchor, this is not supported: "
1754 "{reference_data!r}"
1755 )
1756 return False
1757
1758 try:
1759 pubsub_service = jid.JID(parsed_anchor["path"])
1760 except RuntimeError:
1761 log.warning(f"invalid anchor: {reference_data['anchor']}")
1762 return False
1763 pubsub_node = parsed_anchor.get("node")
1764 if not pubsub_node:
1765 log.warning(f"missing pubsub node in anchor: {reference_data['anchor']}")
1766 return False
1767 pubsub_item = parsed_anchor.get("item")
1768 if not pubsub_item:
1769 log.warning(f"missing pubsub item in anchor: {reference_data['anchor']}")
1770 return False
1771
1772 cached_node = await self.host.memory.storage.getPubsubNode(
1773 client, pubsub_service, pubsub_node
1774 )
1775 if not cached_node:
1776 log.warning(f"Anchored node not found in cache: {reference_data['anchor']}")
1777 return False
1778
1779 cached_items, __ = await self.host.memory.storage.getItems(
1780 cached_node, item_ids=[pubsub_item]
1781 )
1782 if not cached_items:
1783 log.warning(
1784 f"Anchored pubsub item not found in cache: {reference_data['anchor']}"
1785 )
1786 return False
1787
1788 cached_item = cached_items[0]
1789
1790 mb_data = await self._m.item2mbdata(
1791 client, cached_item.data, pubsub_service, pubsub_node
1792 )
1793 ap_item = await self.mbdata2APitem(client, mb_data)
1794 ap_object = ap_item["object"]
1795 ap_object["to"] = [actor_id]
1796 ap_object.setdefault("tag", []).append({
1797 "type": TYPE_MENTION,
1798 "href": actor_id,
1799 "name": ap_account,
1800 })
1801
1802 inbox = await self.getAPInboxFromId(actor_id)
1803
1804 resp = await self.signAndPost(inbox, ap_item["actor"], ap_item)
1805 if resp.code >= 300:
1806 text = await resp.text()
1807 log.warning(
1808 f"unexpected return code while sending AP item: {resp.code}\n{text}\n"
1809 f"{pformat(ap_item)}"
1810 )
1811
1812 return False
1813
1696 async def newReplyToXMPPItem( 1814 async def newReplyToXMPPItem(
1697 self, 1815 self,
1698 client: SatXMPPEntity, 1816 client: SatXMPPEntity,
1699 ap_item: dict, 1817 ap_item: dict,
1700 targets: Dict[str, Set[str]], 1818 targets: Dict[str, Set[str]],