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:
parent
31b55fbbe7
commit
20bf3dffa1
1 changed files with 123 additions and 29 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue