feat: add avatars
This commit is contained in:
@@ -36,4 +36,8 @@ class IUserRepository(ABC):
|
||||
|
||||
@abstractmethod
|
||||
async def email_exists(self, email: str) -> bool:
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
async def set_avatar_link(self, user_id: str, avatar_link: str) -> UserEntity:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from src.application.commands.get_me import GetMeCommand
|
||||
from src.application.commands.set_phone import SetPhoneCommand
|
||||
from src.application.commands.set_avatar import SetAvatarCommand
|
||||
from src.application.commands.set_encrypted_mnemonic_start import SetEncryptedMnemonicStartCommand
|
||||
from src.application.commands.set_encrypted_mnemonic_complete import SetEncryptedMnemonicCompleteCommand
|
||||
from src.application.commands.update_bank_details_start import UpdateBankDetailsStartCommand
|
||||
|
||||
54
src/application/commands/set_avatar.py
Normal file
54
src/application/commands/set_avatar.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from PIL import UnidentifiedImageError
|
||||
from ulid import ULID
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from src.application.abstractions import IUnitOfWork
|
||||
from src.application.contracts import ICache, ILogger, IS3
|
||||
from src.application.domain.entities import UserEntity
|
||||
from src.application.domain.exceptions import BadRequestException, ServiceUnavailableException
|
||||
from src.infrastructure.config import settings
|
||||
from src.infrastructure.database.decorators import transactional
|
||||
from src.infrastructure.media.webp import image_bytes_to_webp
|
||||
|
||||
|
||||
class SetAvatarCommand:
|
||||
def __init__(self, unit_of_work: IUnitOfWork, logger: ILogger, cache: ICache, s3: IS3):
|
||||
self._unit_of_work = unit_of_work
|
||||
self._logger = logger
|
||||
self._cache = cache
|
||||
self._s3 = s3
|
||||
|
||||
async def __call__(self, user_id: str, image_bytes: bytes) -> tuple[UserEntity, int]:
|
||||
try:
|
||||
webp_bytes = image_bytes_to_webp(image_bytes)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise BadRequestException(message='Unsupported or corrupt image') from exc
|
||||
except Exception as exc:
|
||||
self._logger.exception(str(exc))
|
||||
raise BadRequestException(message='Could not process image') from exc
|
||||
|
||||
pid = user_id.replace('/', '').replace('.', '_')
|
||||
name_id = str(ULID())
|
||||
ts = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
prefix = settings.S3_AVATAR_KEY_PREFIX.strip().strip('/')
|
||||
fname = f'{name_id}_{pid}_{ts}.webp'
|
||||
object_key = f'{prefix}/{fname}' if prefix else fname
|
||||
|
||||
try:
|
||||
url = await self._s3.upload_bytes(key=object_key, body=webp_bytes, content_type='image/webp')
|
||||
except ClientError as exc:
|
||||
self._logger.exception(str(exc))
|
||||
raise ServiceUnavailableException(message='S3 upload failed') from exc
|
||||
|
||||
user = await self._save_avatar_link(user_id, url)
|
||||
await self._cache.set_user(user_id, user)
|
||||
self._logger.info(f'Avatar set for user_id={user_id} key={object_key}')
|
||||
return user, len(webp_bytes)
|
||||
|
||||
@transactional
|
||||
async def _save_avatar_link(self, user_id: str, avatar_link: str) -> UserEntity:
|
||||
return await self._unit_of_work.user_repository.set_avatar_link(user_id, avatar_link)
|
||||
@@ -3,4 +3,5 @@ from src.application.contracts.i_jwt_service import IJwtService
|
||||
from src.application.contracts.i_csrf_service import ICsrfService
|
||||
from src.application.contracts.i_cache import ICache
|
||||
from src.application.contracts.i_hash_service import IHashService
|
||||
from src.application.contracts.i_queue_messanger import IQueueMessanger
|
||||
from src.application.contracts.i_queue_messanger import IQueueMessanger
|
||||
from src.application.contracts.i_s3 import IS3
|
||||
|
||||
9
src/application/contracts/i_s3.py
Normal file
9
src/application/contracts/i_s3.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IS3(Protocol):
|
||||
async def upload_bytes(self, *, key: str, body: bytes, content_type: str) -> str:
|
||||
...
|
||||
@@ -20,6 +20,7 @@ class UserEntity:
|
||||
passport_data: str | None = None
|
||||
inn: str | None = None
|
||||
erc20: str | None = None
|
||||
avatar_link: str | None = None
|
||||
|
||||
kyc_verified: bool | None = None
|
||||
is_deleted: bool | None = None
|
||||
|
||||
Reference in New Issue
Block a user