plugin XEP-0084: refactor get_avatar:

`get_avatar()` was checking the cache directly, bypassing `plugin_misc_identity`'s
`use_cache` flag. As a result, cache disabling was ineffective. It now fetches from the
PEP node and doesn't handle the cache anymore.

`_parse_metadata_info()` has been added to factorize the metadata parsing.
This commit is contained in:
Goffi 2026-06-09 16:12:39 +02:00
commit 34ecf8b22e

View file

@ -16,23 +16,22 @@
# 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 typing import Any, Dict
from base64 import b64decode
from pathlib import Path
from typing import Any
from twisted.internet import defer
from twisted.words.protocols.jabber import error, jid
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.protocols.jabber import jid, error
from twisted.words.xish import domish
from zope.interface import implementer
from tx_xmpp import disco, itx_xmpp, pubsub
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core import exceptions
from tx_xmpp import disco, itx_xmpp, pubsub
log = getLogger(__name__)
@ -80,55 +79,65 @@ class XEP_0084:
client = self.host.get_client(profile)
defer.ensureDeferred(self.on_metadata_update(client, itemsEvent))
@staticmethod
def _parse_metadata_info(
item_elt: domish.Element,
) -> tuple[str, str, int] | None:
"""Parse avatar metadata item and extract ID, media type, and size.
@param item_elt: pubsub item element containing metadata.
@return: (avatar_id, media_type, size) if a valid info element is found,
None if avatar is disabled or metadata is invalid.
"""
try:
metadata_elt = next(item_elt.elements(NS_AVATAR_METADATA, "metadata"))
except StopIteration:
log.warning(f"missing metadata element: {item_elt.toXml()}")
return None
for info_elt in metadata_elt.elements(NS_AVATAR_METADATA, "info"):
try:
avatar_id = str(info_elt["id"])
media_type = str(info_elt["type"])
size = int(info_elt["bytes"])
except (KeyError, ValueError):
log.warning(f"invalid <info> element: {item_elt.toXml()}")
continue
if not avatar_id:
continue
# FIXME: to simplify, we only handle image/png for now
if media_type == "image/png":
return (avatar_id, media_type, size)
# mandatory image/png is missing, or avatar is disabled
# (https://xmpp.org/extensions/xep-0084.html#pub-disable)
return None
async def on_metadata_update(
self, client: SatXMPPEntity, itemsEvent: pubsub.ItemsEvent
) -> None:
"""Handle PEP avatar metadata update event.
Parse metadata, fetch data if not cached, and update identity cache.
@param client: Client instance.
@param itemsEvent: pubsub items event with avatar metadata.
"""
entity = itemsEvent.sender
service = entity.userhostJID()
items = itemsEvent.items
if not items:
return
if items[0].name == "retract":
if not items or items[0].name == "retract":
log.debug(f"Avatar retracted for {entity.full()}")
await self._i.update(client, IMPORT_NAME, "avatar", None, entity)
return
item_elt = items[0]
try:
metadata_elt = next(item_elt.elements(NS_AVATAR_METADATA, "metadata"))
except StopIteration:
log.warning(f"missing metadata element: {item_elt.toXml()}")
return
for info_elt in metadata_elt.elements(NS_AVATAR_METADATA, "info"):
try:
metadata = {
"id": str(info_elt["id"]),
"size": int(info_elt["bytes"]),
"media_type": str(info_elt["type"]),
}
avatar_id = metadata["id"]
if not avatar_id:
raise ValueError
except (KeyError, ValueError):
log.warning(f"invalid <info> element: {item_elt.toXml()}")
return None
# FIXME: to simplify, we only handle image/png for now
if metadata["media_type"] == "image/png":
break
else:
# mandatory image/png is missing, or avatar is disabled
# (https://xmpp.org/extensions/xep-0084.html#pub-disable)
parsed = self._parse_metadata_info(items[0])
if parsed is None:
log.debug(f"Avatar disabled for {entity.full()}")
await self._i.update(client, IMPORT_NAME, "avatar", None, entity)
return
avatar_id, media_type, _size = parsed
# Check if avatar is already cached
try:
cached = self.host.memory.get_entity_datum(client, entity, "avatar")
@ -148,7 +157,7 @@ class XEP_0084:
cache_data = self.host.common_cache.get_metadata(avatar_id)
if not cache_data:
cache_data = await self._fetch_avatar_data(
client, service, avatar_id, metadata["media_type"]
client, service, avatar_id, media_type
)
if cache_data is None:
log.warning(f"Failed to fetch avatar data for {entity.full()}")
@ -199,28 +208,40 @@ class XEP_0084:
return {"path": Path(f.name), "mime_type": media_type}
async def get_avatar(self, client: SatXMPPEntity, entity_jid: jid.JID) -> dict | None:
"""Get avatar data from cache."""
"""Get avatar data by fetching from PEP metadata node.
@param client: Client instance.
@param entity_jid: entity to get avatar for.
@return: avatar metadata dict if found, None otherwise.
"""
service = entity_jid.userhostJID()
try:
cached = self.host.memory.get_entity_datum(client, entity_jid, "avatar")
except (KeyError, exceptions.UnknownEntityError):
data_items, __ = await self._p.get_items(client, service, NS_AVATAR_METADATA)
except error.StanzaError:
return None
if not cached:
if not data_items:
return None
cache_uid = cached.get("cache_uid")
filename = cached.get("filename")
if not cache_uid or not filename:
parsed = self._parse_metadata_info(data_items[0])
if parsed is None:
return None
try:
self.host.common_cache.get_metadata(cache_uid)
except Exception:
return None
avatar_id, media_type, _size = parsed
return cached
cache_data = self.host.common_cache.get_metadata(avatar_id)
if not cache_data:
cache_data = await self._fetch_avatar_data(
client, service, avatar_id, media_type
)
if cache_data is None:
return None
def build_item_data_elt(self, avatar_data: Dict[str, Any]) -> domish.Element:
return self._i.avatar_build_metadata(
cache_data["path"], cache_data["mime_type"], avatar_id
)
def build_item_data_elt(self, avatar_data: dict[str, Any]) -> domish.Element:
"""Generate the item for the data node
@param avatar_data: data as build by identity plugin (need to be filled with
@ -230,7 +251,7 @@ class XEP_0084:
data_elt.addContent(avatar_data["base64"])
return pubsub.Item(id=avatar_data["cache_uid"], payload=data_elt)
def build_item_metadata_elt(self, avatar_data: Dict[str, Any]) -> domish.Element:
def build_item_metadata_elt(self, avatar_data: dict[str, Any]) -> domish.Element:
"""Generate the item for the metadata node
@param avatar_data: data as build by identity plugin (need to be filled with
@ -246,7 +267,7 @@ class XEP_0084:
return pubsub.Item(id=self._p.ID_SINGLETON, payload=metadata_elt)
async def set_avatar(
self, client: SatXMPPEntity, avatar_data: Dict[str, Any], entity: jid.JID
self, client: SatXMPPEntity, avatar_data: dict[str, Any], entity: jid.JID
) -> None:
"""Set avatar of the profile