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:
parent
89c23c8a4c
commit
709366fe13
1 changed files with 103 additions and 50 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue