Mercurial > libervia-backend
view libervia/backend/plugins/plugin_xep_0446.py @ 4334:111dce64dcb5
plugins XEP-0300, XEP-0446, XEP-0447, XEP0448 and others: Refactoring to use Pydantic:
Pydantic models are used more and more in Libervia, for the bridge API, and also to
convert `domish.Element` to internal representation.
Type hints have also been added in many places.
rel 453
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 03 Dec 2024 00:12:38 +0100 |
parents | 0d7bb4df2343 |
children |
line wrap: on
line source
#!/usr/bin/env python3 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from pathlib import Path from typing import Self, cast from pydantic import BaseModel, Field from twisted.words.xish import domish from libervia.backend.core import exceptions from libervia.backend.core.constants import Const as C from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger from libervia.backend.plugins.plugin_xep_0300 import XEP_0300 from libervia.backend.tools import utils log = getLogger(__name__) PLUGIN_INFO = { C.PI_NAME: "File Metadata Element", C.PI_IMPORT_NAME: "XEP-0446", C.PI_TYPE: "XEP", C.PI_MODES: C.PLUG_MODE_BOTH, C.PI_PROTOCOLS: ["XEP-0446"], C.PI_DEPENDENCIES: ["XEP-0300"], C.PI_MAIN: "XEP_0446", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Implementation of XEP-0446 (File Metadata Element)"""), } NS_FILE_METADATA = "urn:xmpp:file:metadata:0" class FileMetadata(BaseModel): """ Model for file metadata. """ name: str | None = Field(None, description="Name of the file.") media_type: str | None = Field(None, description="Media type of the file.") desc: str | None = Field(None, description="Description of the file.") size: int | None = Field(None, description="Size of the file in bytes.") file_hash: tuple[str, str] | None = Field( None, description="File hash as a tuple of (algo, hash)." ) date: float | int | None = Field( None, description="Timestamp of the last modification datetime." ) width: int | None = Field(None, description="Image or video width in pixels.") height: int | None = Field(None, description="Image or video height in pixels.") length: int | None = Field(None, description="Video or audio length in milliseconds.") thumbnail: str | None = Field(None, description="URL to a thumbnail.") _hash: XEP_0300 | None = None @classmethod def from_element(cls, file_metadata_elt: domish.Element) -> Self: """Create a FileMetadata instance from a <file> element or its parent. @param file_metadata_elt: The <file> element or a parent element. @return: FileMetadata instance. @raise exceptions.NotFound: If the <file> element is not found. """ assert cls._hash is not None, "_hash attribute is not set" if file_metadata_elt.uri != NS_FILE_METADATA or file_metadata_elt.name != "file": child_file_metadata_elt = next( file_metadata_elt.elements(NS_FILE_METADATA, "file"), None ) if child_file_metadata_elt is None: raise exceptions.NotFound("<file> element not found") else: file_metadata_elt = child_file_metadata_elt kwargs = {} for key in ( "name", "media_type", "desc", "size", "date", "width", "height", "length", ): elt = next(file_metadata_elt.elements(NS_FILE_METADATA, key), None) if elt is not None: if key in ("name", "media_type", "desc"): content = str(elt) if key == "name": content = Path(content).name kwargs[key] = content elif key in ("size", "width", "height", "length"): kwargs[key] = int(str(elt)) elif key == "date": kwargs[key] = utils.parse_xmpp_date(str(elt)) hash_elt = next(file_metadata_elt.elements(NS_FILE_METADATA, "hash"), None) if hash_elt: try: algo, hash_ = cls._hash.parse_hash_elt(hash_elt) except exceptions.NotFound: pass except exceptions.DataError: from libervia.backend.tools.xml_tools import p_fmt_elt log.warning(f"invalid <hash/> element:\n{p_fmt_elt(file_metadata_elt)}") else: kwargs["file_hash"] = (algo, hash_) return cls(**kwargs) def to_element(self) -> domish.Element: """Build the <file> element from this instance's data. @return: <file> element. """ assert self._hash is not None, "_hash attribute is not set" file_elt = domish.Element((NS_FILE_METADATA, "file")) for key, value in ( ("name", self.name), ("media-type", self.media_type), ("desc", self.desc), ("size", self.size), ("width", self.width), ("height", self.height), ("length", self.length), ): if value is not None: file_elt.addElement(key, content=str(value)) if self.file_hash is not None: hash_algo, hash_ = self.file_hash file_elt.addChild(self._hash.build_hash_elt(hash_, hash_algo)) if self.date is not None: file_elt.addElement("date", content=utils.xmpp_date(self.date)) if self.thumbnail is not None: log.warning("thumbnail is not implemented yet") return file_elt @classmethod def from_filedata_dict(cls, file_data: dict) -> Self: """Create an instance of FileMetadata from data dict as returned by ``memory``. @param data: A filedata dict as returned by ``memory.get_files`` @return: An instance of FileMetadata. """ # Extracting relevant fields name = file_data["name"] media_type = f'{file_data["media_type"]}/{file_data["media_subtype"]}' desc = None # TODO size = file_data["size"] file_hash = (file_data["hash_algo"], file_data["file_hash"]) date = file_data.get("modified") or file_data["created"] width = None # TODO height = None # TODO length = None # TODO thumbnail = None # TODO return cls( name=name, media_type=media_type, desc=desc, size=size, file_hash=file_hash, date=date, width=width, height=height, length=length, thumbnail=thumbnail, ) class XEP_0446: def __init__(self, host): log.info(_("XEP-0446 (File Metadata Element) plugin initialization")) host.register_namespace("file-metadata", NS_FILE_METADATA) FileMetadata._hash = cast(XEP_0300, host.plugins["XEP-0300"]) def generate_file_metadata( self, name: str | None = None, media_type: str | None = None, desc: str | None = None, size: int | None = None, file_hash: tuple[str, str] | None = None, date: float | int | None = None, width: int | None = None, height: int | None = None, length: int | None = None, thumbnail: str | None = None, ) -> FileMetadata: """Generate the element describing a file @param name: name of the file @param media_type: media-type @param desc: description @param size: size in bytes @param file_hash: (algo, hash) @param date: timestamp of the last modification datetime @param width: image width in pixels @param height: image height in pixels @param length: video length in seconds @param thumbnail: URL to a thumbnail @return: ``<file/>`` element """ return FileMetadata( name=name, media_type=media_type, desc=desc, size=size, file_hash=file_hash, date=date, width=width, height=height, length=length, thumbnail=thumbnail, ) def parse_file_metadata_elt(self, file_metadata_elt: domish.Element) -> FileMetadata: """Parse <file/> element @param file_metadata_elt: <file/> element a parent element can also be used @return: FileMetadata instance. @raise exceptions.NotFound: no <file/> element has been found """ return FileMetadata.from_element(file_metadata_elt)