Mercurial > libervia-backend
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]], |