plugin XEP-0084: fix avatar retrieval and disabling:

- Avatar disabling was not correctly handled for all cases, this should now be fixed.
- Avatar retrieval is now down only on PEP events, no directly anymore. `get_avatar` only
  returns cached version.
This commit is contained in:
Goffi 2026-05-04 16:49:07 +02:00
commit 709366fe13

View file

@ -16,9 +16,10 @@
# 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 Optional, Dict, Any
from typing import Any, Dict
from base64 import b64decode
from pathlib import Path
from base64 import b64decode, b64encode
from twisted.internet import defer
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
@ -82,36 +83,28 @@ class XEP_0084:
async def on_metadata_update(
self, client: SatXMPPEntity, itemsEvent: pubsub.ItemsEvent
) -> None:
entity = itemsEvent.sender
avatar_metadata = await self.get_avatar(client, entity)
await self._i.update(client, IMPORT_NAME, "avatar", avatar_metadata, entity)
"""Handle PEP avatar metadata update event.
async def get_avatar(
self, client: SatXMPPEntity, entity_jid: jid.JID
) -> Optional[dict]:
"""Get avatar data
@param entity: entity to get avatar from
@return: avatar metadata, or None if no avatar has been found
Parse metadata, fetch data if not cached, and update identity cache.
"""
service = entity_jid.userhostJID()
# metadata
try:
items, __ = await self._p.get_items(
client, service, NS_AVATAR_METADATA, max_items=1
)
except exceptions.NotFound:
return None
entity = itemsEvent.sender
service = entity.userhostJID()
items = itemsEvent.items
if not items:
return None
return
if 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 None
return
for info_elt in metadata_elt.elements(NS_AVATAR_METADATA, "info"):
try:
@ -132,40 +125,100 @@ class XEP_0084:
else:
# mandatory image/png is missing, or avatar is disabled
# (https://xmpp.org/extensions/xep-0084.html#pub-disable)
return None
log.debug(f"Avatar disabled for {entity.full()}")
await self._i.update(client, IMPORT_NAME, "avatar", None, entity)
return
# Check if avatar is already cached
try:
cached = self.host.memory.get_entity_datum(client, entity, "avatar")
except (KeyError, exceptions.UnknownEntityError):
cached = None
if cached is not None:
cached_uid = cached.get("cache_uid") if isinstance(cached, dict) else None
if cached_uid == avatar_id:
log.debug(f"Avatar already cached for {entity.full()} (id={avatar_id})")
return
log.debug(
f"Avatar changed for {entity.full()} (old={cached_uid}, new={avatar_id})"
)
# Fetch avatar data if not already cached
cache_data = self.host.common_cache.get_metadata(avatar_id)
if not cache_data:
try:
data_items, __ = await self._p.get_items(
client, service, NS_AVATAR_DATA, item_ids=[avatar_id]
)
data_item_elt = data_items[0]
except (error.StanzaError, IndexError) as e:
log.warning(
f"Can't retrieve avatar of {service.full()} with ID {avatar_id!r}: "
f"{e}"
)
return None
try:
avatar_buf = b64decode(
str(next(data_item_elt.elements(NS_AVATAR_DATA, "data")))
)
except Exception as e:
log.warning(
f"invalid data element for {service.full()} with avatar ID "
f"{avatar_id!r}: {e}\n{data_item_elt.toXml()}"
)
return None
with self.host.common_cache.cache_data(
IMPORT_NAME, avatar_id, metadata["media_type"]
) as f:
f.write(avatar_buf)
cache_data = {"path": Path(f.name), "mime_type": metadata["media_type"]}
cache_data = await self._fetch_avatar_data(
client, service, avatar_id, metadata["media_type"]
)
if cache_data is None:
log.warning(f"Failed to fetch avatar data for {entity.full()}")
return
return self._i.avatar_build_metadata(
log.debug(f"Avatar updated for {entity.full()} (id={avatar_id})")
avatar_metadata = self._i.avatar_build_metadata(
cache_data["path"], cache_data["mime_type"], avatar_id
)
await self._i.update(client, IMPORT_NAME, "avatar", avatar_metadata, entity)
async def _fetch_avatar_data(
self,
client: SatXMPPEntity,
service: jid.JID,
avatar_id: str,
media_type: str,
) -> dict | None:
"""Fetch avatar data from PEP data node and cache it.
@return: Cache data dict with path and mime_type, or None on failure.
"""
try:
data_items, __ = await self._p.get_items(
client, service, NS_AVATAR_DATA, item_ids=[avatar_id]
)
data_item_elt = data_items[0]
except (error.StanzaError, IndexError) as e:
log.warning(
f"Can't retrieve avatar of {service.full()} with ID {avatar_id!r}: "
f"{e}"
)
return None
try:
avatar_buf = b64decode(
str(next(data_item_elt.elements(NS_AVATAR_DATA, "data")))
)
except Exception as e:
log.warning(
f"invalid data element for {service.full()} with avatar ID "
f"{avatar_id!r}: {e}\n{data_item_elt.toXml()}"
)
return None
with self.host.common_cache.cache_data(IMPORT_NAME, avatar_id, media_type) as f:
f.write(avatar_buf)
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."""
try:
cached = self.host.memory.get_entity_datum(client, entity_jid, "avatar")
except (KeyError, exceptions.UnknownEntityError):
return None
if not cached:
return None
cache_uid = cached.get("cache_uid")
filename = cached.get("filename")
if not cache_uid or not filename:
return None
try:
self.host.common_cache.get_metadata(cache_uid)
except Exception:
return None
return cached
def build_item_data_elt(self, avatar_data: Dict[str, Any]) -> domish.Element:
"""Generate the item for the data node