plugin XEP-0499: metadata + filtering:

- Added `full_metadata` handling to return only relationship fields when requested.
- Implemented `filter_node_hierarchy_by_access` for access control filtering

rel 476
This commit is contained in:
Goffi 2026-04-11 02:03:22 +02:00
commit 20bf3dffa1

View file

@ -17,26 +17,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Callable, Coroutine, Iterable
from typing import (
TYPE_CHECKING,
ClassVar,
Final,
Literal,
NamedTuple,
Self,
cast,
)
from typing import ClassVar, Final, Literal, NamedTuple, Self, TYPE_CHECKING, cast
from pydantic import BaseModel, Field, RootModel, model_validator
from sqlalchemy.exc import SQLAlchemyError
from twisted.internet import defer
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber.jid import JID
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from tx_xmpp import disco, data_form, pubsub
from tx_xmpp import data_form, disco, pubsub
from zope.interface import implementer
from libervia.backend import G
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPComponent, SatXMPPEntity
@ -45,6 +38,7 @@ from libervia.backend.core.log import getLogger
from libervia.backend.memory.sqla_mapping import PubsubItem, PubsubNode
from libervia.backend.models.types import JIDType
from libervia.backend.plugins.plugin_xep_0060 import NodeMetadata
from libervia.backend.plugins.plugin_xep_0496 import LINK_VAR, PARENT_VAR, XEP_0496
from libervia.backend.tools import utils
if TYPE_CHECKING:
@ -60,7 +54,7 @@ PLUGIN_INFO = {
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0499"],
C.PI_DEPENDENCIES: [
"XEP-0060",
"XEP-0060", "XEP-0496"
],
C.PI_RECOMMENDATIONS: [],
C.PI_MAIN: "XEP_0499",
@ -71,9 +65,6 @@ PLUGIN_INFO = {
}
NS_PUBSUB_EXT_DISCO: Final = "urn:xmpp:pubsub-ext-disco:0"
NS_PUBSUB_RELATIONSHIPS: Final = "urn:xmpp:pubsub-relationships:0"
PARENT_VAR = f"{{{NS_PUBSUB_RELATIONSHIPS}}}parent"
LINK_VAR = f"{{{NS_PUBSUB_RELATIONSHIPS}}}link"
class ExtDiscoMetadata(NodeMetadata):
@ -95,18 +86,35 @@ class ExtDiscoMetadata(NodeMetadata):
metadata.link = form.get(LINK_VAR)
return metadata
def to_data_form(self) -> data_form.Form:
def to_data_form(self, full_metadata: bool = False) -> data_form.Form:
"""Convert this instance to a data form.
@param full_metadata: If True, include all metadata fields. If False, only include
relationship fields (parent and link).
@return: Data form representation of this instance.
"""
form = super().to_data_form()
if full_metadata:
form = super().to_data_form()
else:
form = data_form.Form(
formType="result", formNamespace=pubsub.NS_PUBSUB_META_DATA
)
if self.parent is not None:
form.addField(data_form.Field(var=PARENT_VAR, value=self.parent))
if self.link is not None:
form.addField(data_form.Field(var=LINK_VAR, value=self.link))
return form
def to_element(self, full_metadata: bool = False) -> domish.Element:
"""Generate the <x> element corresponding to this form.
@param full_metadata: If True, include all metadata fields. If False, only include
relationship fields (parent and link).
@return: XML element representation of this instance.
"""
return self.to_data_form(full_metadata).toElement()
class ExtDiscoOptions(BaseModel):
"""Pydantic model for the pubsub extended discovery form fields."""
@ -320,29 +328,27 @@ class DiscoPubsubNode(BaseModel):
return disco_node
def to_element(self) -> domish.Element:
def to_element(self, full_metadata: bool = False) -> domish.Element:
item_elt = domish.Element((disco.NS_DISCO_ITEMS, "item"))
item_elt["jid"] = self.jid.full()
item_elt["node"] = self.name
item_elt.addChild(self.metadata.to_element())
item_elt.addChild(self.metadata.to_element(full_metadata))
return item_elt
def to_elements(
self,
) -> list[domish.Element]:
def to_elements(self, full_metadata: bool = False) -> list[domish.Element]:
"""Return this elements and all its descendants and linking nodes.
@param parent_node: Parent of this node, None if it's a root node.
@param linked_node: Node this node is linking to, if any.
@return: This node, its descendants and linking nodes.
"""
elements = [self.to_element()]
elements = [self.to_element(full_metadata)]
if self.linking_nodes is not None:
for linking_node in self.linking_nodes:
elements.append(linking_node.to_element())
elements.append(linking_node.to_element(full_metadata))
if self.children is not None:
for child in self.children:
elements.extend(child.to_elements())
elements.extend(child.to_elements(full_metadata))
return elements
@ -455,11 +461,11 @@ class ExtDiscoResult(RootModel):
return cls(root=result_items)
def to_elements(self) -> list[domish.Element]:
def to_elements(self, full_metadata: bool = False) -> list[domish.Element]:
elements = []
for item in self.root:
if isinstance(item, DiscoPubsubNode):
elements.extend(item.to_elements())
elements.extend(item.to_elements(full_metadata))
else:
elements.append(item.to_element())
return elements
@ -484,7 +490,6 @@ class XEP_0499:
self.handlers: list[RequestHandler] = []
host.register_namespace("pubsub-ext-disco", NS_PUBSUB_EXT_DISCO)
host.register_namespace("pubsub-relationships", NS_PUBSUB_RELATIONSHIPS)
host.bridge.add_method(
"ps_disco_get",
".plugin",
@ -493,6 +498,95 @@ class XEP_0499:
method=self._disco_get,
async_=True,
)
self._xep_0496 = cast(XEP_0496, host.plugins["XEP-0496"])
async def filter_node_hierarchy_by_access(
self,
client: SatXMPPEntity,
service: JID,
node: DiscoPubsubNode,
entity_jid: JID,
depth: int = 0,
linked_nodes: bool = False,
current_depth: int = 0,
access_cache: dict[str, bool] | None = None,
) -> DiscoPubsubNode | None:
"""Filter a node hierarchy based on access control and depth.
@param client: Client instance.
@param service: The pubsub service JID.
@param node: The DiscoPubsubNode to filter.
@param entity_jid: The entity JID to check access for.
@param depth: Maximum depth to traverse (0 = only this node).
@param linked_nodes: Whether to include linking nodes.
@param current_depth: Current depth in the traversal.
@param access_cache: Cache of access results for nodes already checked.
@return: Filtered DiscoPubsubNode or None if not accessible.
"""
if access_cache is None:
access_cache = {}
if node.name in access_cache:
if not access_cache[node.name]:
return None
return DiscoPubsubNode(jid=node.jid, name=node.name, metadata=node.metadata)
pubsub_node = await G.storage.get_pubsub_node(
client, service, node.name, with_affiliations=True, with_parent=True
)
if pubsub_node is None:
access_cache[node.name] = False
return None
has_access = await self._xep_0496.has_access(
client, service, pubsub_node, entity_jid
)
access_cache[node.name] = has_access
if not has_access:
return None
filtered_node = DiscoPubsubNode(
jid=node.jid, name=node.name, metadata=node.metadata
)
if current_depth < depth:
if node.children is not None:
filtered_children: list[DiscoPubsubNode] = []
for child in node.children:
filtered_child = await self.filter_node_hierarchy_by_access(
client,
service,
child,
entity_jid,
depth,
linked_nodes,
current_depth + 1,
access_cache,
)
if filtered_child is not None:
filtered_children.append(filtered_child)
if filtered_children:
filtered_node.children = filtered_children
if linked_nodes and node.linking_nodes is not None:
filtered_linking: list[DiscoPubsubNode] = []
for linking in node.linking_nodes:
filtered_linking_node = await self.filter_node_hierarchy_by_access(
client,
service,
linking,
entity_jid,
depth,
linked_nodes,
current_depth + 1,
access_cache,
)
if filtered_linking_node is not None:
filtered_linking.append(filtered_linking_node)
if filtered_linking:
filtered_node.linking_nodes = filtered_linking
return filtered_node
def get_handler(self, client: SatXMPPEntity) -> "PubsubExtDiscoHandler":
return PubsubExtDiscoHandler(self)
@ -564,7 +658,7 @@ class XEP_0499:
result_query_elt = iq_result_elt.addElement((disco.NS_DISCO_ITEMS, "query"))
if query_elt.hasAttribute("node"):
result_query_elt["node"] = query_elt["node"]
for elt in disco_result.to_elements():
for elt in disco_result.to_elements(ext_disco_options.full_metadata):
result_query_elt.addChild(elt)
client.send(iq_result_elt)