Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_identity.py @ 3816:213e83a4ed10
plugin identity, XEP-0054: move avatar resizing and caching method to identity plugin:
resizing and caching is now done in identity plugin, to prepare for the implementation of
other XEP to handle avatars.
rel 368
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 29 Jun 2022 11:47:48 +0200 |
parents | 888109774673 |
children | 998c5318230f |
comparison
equal
deleted
inserted
replaced
3815:853cbaf56e9e | 3816:213e83a4ed10 |
---|---|
13 # GNU Affero General Public License for more details. | 13 # GNU Affero General Public License for more details. |
14 | 14 |
15 # You should have received a copy of the GNU Affero General Public License | 15 # You should have received a copy of the GNU Affero General Public License |
16 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 16 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 |
18 from typing import Dict, List, Union, Coroutine, Any, Optional | |
19 from collections import namedtuple | 18 from collections import namedtuple |
19 import io | |
20 from pathlib import Path | 20 from pathlib import Path |
21 from twisted.internet import defer | 21 from base64 import b64encode |
22 import hashlib | |
23 from typing import Any, Coroutine, Dict, List, Optional, Union | |
24 | |
25 from twisted.internet import defer, threads | |
22 from twisted.words.protocols.jabber import jid | 26 from twisted.words.protocols.jabber import jid |
27 | |
28 from sat.core import exceptions | |
29 from sat.core.constants import Const as C | |
30 from sat.core.i18n import _ | |
31 from sat.core.log import getLogger | |
23 from sat.core.xmpp import SatXMPPEntity | 32 from sat.core.xmpp import SatXMPPEntity |
24 from sat.core.i18n import _ | |
25 from sat.core.constants import Const as C | |
26 from sat.core import exceptions | |
27 from sat.core.log import getLogger | |
28 from sat.memory import persistent | 33 from sat.memory import persistent |
29 from sat.tools import image | 34 from sat.tools import image |
30 from sat.tools import utils | 35 from sat.tools import utils |
31 from sat.tools.common import data_format | 36 from sat.tools.common import data_format |
37 | |
38 try: | |
39 from PIL import Image | |
40 except: | |
41 raise exceptions.MissingModule( | |
42 "Missing module pillow, please download/install it from https://python-pillow.github.io" | |
43 ) | |
44 | |
32 | 45 |
33 | 46 |
34 log = getLogger(__name__) | 47 log = getLogger(__name__) |
35 | 48 |
36 | 49 |
48 C.PI_HANDLER: "no", | 61 C.PI_HANDLER: "no", |
49 C.PI_DESCRIPTION: _("""Identity manager"""), | 62 C.PI_DESCRIPTION: _("""Identity manager"""), |
50 } | 63 } |
51 | 64 |
52 Callback = namedtuple("Callback", ("origin", "get", "set", "priority")) | 65 Callback = namedtuple("Callback", ("origin", "get", "set", "priority")) |
66 AVATAR_DIM = (128, 128) | |
53 | 67 |
54 | 68 |
55 class Identity: | 69 class Identity: |
56 | 70 |
57 def __init__(self, host): | 71 def __init__(self, host): |
472 client = self.host.getClient(profile_key) | 486 client = self.host.getClient(profile_key) |
473 entity = jid.JID(entity) if entity else None | 487 entity = jid.JID(entity) if entity else None |
474 return defer.ensureDeferred( | 488 return defer.ensureDeferred( |
475 self.set(client, "avatar", file_path, entity)) | 489 self.set(client, "avatar", file_path, entity)) |
476 | 490 |
491 def _blockingCacheAvatar( | |
492 self, | |
493 source: str, | |
494 avatar_data: dict[str, Any] | |
495 ): | |
496 """This method is executed in a separated thread""" | |
497 if avatar_data["media_type"] == "image/svg+xml": | |
498 # for vector image, we save directly | |
499 img_buf = open(avatar_data["path"], "rb") | |
500 else: | |
501 # for bitmap image, we check size and resize if necessary | |
502 try: | |
503 img = Image.open(avatar_data["path"]) | |
504 except IOError as e: | |
505 raise exceptions.DataError(f"Can't open image: {e}") | |
506 | |
507 if img.size != AVATAR_DIM: | |
508 img.thumbnail(AVATAR_DIM) | |
509 if img.size[0] != img.size[1]: # we need to crop first | |
510 left, upper = (0, 0) | |
511 right, lower = img.size | |
512 offset = abs(right - lower) / 2 | |
513 if right == min(img.size): | |
514 upper += offset | |
515 lower -= offset | |
516 else: | |
517 left += offset | |
518 right -= offset | |
519 img = img.crop((left, upper, right, lower)) | |
520 img_buf = io.BytesIO() | |
521 # PNG is well supported among clients, so we convert to this format | |
522 img.save(img_buf, "PNG") | |
523 img_buf.seek(0) | |
524 avatar_data["media_type"] = "image/png" | |
525 | |
526 media_type = avatar_data["media_type"] | |
527 avatar_data["base64"] = image_b64 = b64encode(img_buf.read()).decode() | |
528 img_buf.seek(0) | |
529 image_hash = hashlib.sha1(img_buf.read()).hexdigest() | |
530 img_buf.seek(0) | |
531 with self.host.common_cache.cacheData( | |
532 source, image_hash, media_type | |
533 ) as f: | |
534 f.write(img_buf.read()) | |
535 avatar_data['path'] = Path(f.name) | |
536 avatar_data['filename'] = avatar_data['path'].name | |
537 avatar_data['cache_uid'] = image_hash | |
538 | |
539 async def cacheAvatar(self, source: str, avatar_data: Dict[str, Any]) -> None: | |
540 """Resize if necessary and cache avatar | |
541 | |
542 @param source: source importing the avatar (usually it is plugin's import name), | |
543 will be used in cache metadata | |
544 @param avatar_data: avatar metadata as build by [avatarSetDataFilter] | |
545 will be updated with following keys: | |
546 path: updated path using cached file | |
547 filename: updated filename using cached file | |
548 base64: resized and base64 encoded avatar | |
549 cache_uid: SHA1 hash used as cache unique ID | |
550 """ | |
551 await threads.deferToThread(self._blockingCacheAvatar, source, avatar_data) | |
552 | |
477 async def avatarSetDataFilter(self, client, entity, file_path): | 553 async def avatarSetDataFilter(self, client, entity, file_path): |
478 """Convert avatar file path to dict data""" | 554 """Convert avatar file path to dict data""" |
479 file_path = Path(file_path) | 555 file_path = Path(file_path) |
480 if not file_path.is_file(): | 556 if not file_path.is_file(): |
481 raise ValueError(f"There is no file at {file_path} to use as avatar") | 557 raise ValueError(f"There is no file at {file_path} to use as avatar") |
487 media_type = avatar_data['media_type'] | 563 media_type = avatar_data['media_type'] |
488 if media_type is None: | 564 if media_type is None: |
489 raise ValueError(f"Can't identify type of image at {file_path}") | 565 raise ValueError(f"Can't identify type of image at {file_path}") |
490 if not media_type.startswith('image/'): | 566 if not media_type.startswith('image/'): |
491 raise ValueError(f"File at {file_path} doesn't appear to be an image") | 567 raise ValueError(f"File at {file_path} doesn't appear to be an image") |
568 await self.cacheAvatar(IMPORT_NAME, avatar_data) | |
492 return avatar_data | 569 return avatar_data |
493 | 570 |
494 async def avatarSetPostTreatment(self, client, entity, avatar_data): | 571 async def avatarSetPostTreatment(self, client, entity, avatar_data): |
495 """Update our own avatar""" | 572 """Update our own avatar""" |
496 await self.update(client, IMPORT_NAME, "avatar", avatar_data, entity) | 573 await self.update(client, IMPORT_NAME, "avatar", avatar_data, entity) |
497 | 574 |
498 def avatarBuildMetadata(self, path, media_type=None, cache_uid=None): | 575 def avatarBuildMetadata( |
576 self, | |
577 path: Path, | |
578 media_type: Optional[str] = None, | |
579 cache_uid: Optional[str] = None | |
580 ) -> Optional[Dict[str, Union[str, Path, None]]]: | |
499 """Helper method to generate avatar metadata | 581 """Helper method to generate avatar metadata |
500 | 582 |
501 @param path(str, Path, None): path to avatar file | 583 @param path(str, Path, None): path to avatar file |
502 avatar file must be in cache | 584 avatar file must be in cache |
503 None if avatar is explicitely not set | 585 None if avatar is explicitely not set |