This commit is contained in:
Artem Tsyrulnikov
2026-01-06 18:50:22 +03:00
parent 61f7ed093c
commit c73798da87
11 changed files with 959 additions and 521 deletions

View File

@@ -290,12 +290,25 @@ class Parser(typing.Protocol):
async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: ...
class S3Storage(typing.Protocol):
async def connect(self) -> None: ...
async def close(self) -> None: ...
async def upload(self, key: str, data: bytes, content_type: str) -> None: ...
async def get(self, key: str) -> bytes: ...
async def delete(self, key: str) -> None: ...
@dataclass
class Usecase:
database: Database
telegram_bot: TelegramBotWriter
jwt_encoder: JWTEncoder
parser: Parser
s3: S3Storage
async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace:
workspace = await self.database.get_workspace_for_user(workspace_id, user_id)

View File

@@ -26,12 +26,20 @@ async def create_creative(
creative_text = domain.replace_invite_link_with_tag(input.text)
media_s3_key: str | None = None
if input.media_data:
file_id = uuid.uuid4()
media_s3_key = f'creatives/{workspace_id}/{file_id}'
content_type = _get_content_type(input.media_type)
await self.s3.upload(media_s3_key, input.media_data, content_type)
log.info('Uploaded creative media to S3: %s', media_s3_key)
creative = domain.Creative(
name=input.name,
text=creative_text,
media_type=input.media_type,
media_file_id=input.media_file_id,
media_data=input.media_data,
media_s3_key=media_s3_key,
buttons=[button.model_dump() for button in input.buttons],
status=domain.CreativeStatus.ACTIVE,
project_id=project.id,
@@ -52,3 +60,16 @@ async def create_creative(
status=creative.status,
placements_count=0,
)
def _get_content_type(media_type: str | None) -> str:
"""Map Telegram media type to MIME content type."""
if not media_type:
return 'application/octet-stream'
mapping = {
'photo': 'image/jpeg',
'video': 'video/mp4',
'animation': 'image/gif',
}
return mapping.get(media_type, 'application/octet-stream')

View File

@@ -26,4 +26,12 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No
log.warning('Creative %s is used in placements and cannot be deleted', input.creative_id)
raise domain.CreativeInUse(input.creative_id)
media_key = creative.media_s3_key
if media_key:
try:
await self.s3.delete(media_key)
log.info('Deleted creative media from S3: %s', media_key)
except Exception as e:
log.warning('Failed to delete creative media from S3: %s', e)
await self.database.delete_creative(input.creative_id)

View File

@@ -32,7 +32,27 @@ async def update_creative(
creative.text = domain.replace_invite_link_with_tag(input.text)
if input.media_data is not None:
domain.validate_media_size(input.media_data)
creative.media_data = input.media_data
# Удаляем старое медиа из S3
old_media_key = creative.media_s3_key
if old_media_key:
try:
await self.s3.delete(old_media_key)
log.info('Deleted old creative media from S3: %s', old_media_key)
except Exception as e:
log.warning('Failed to delete old creative media from S3: %s', e)
# Загружаем новое медиа в S3
new_media_data = input.media_data
if new_media_data:
file_id = uuid.uuid4()
new_media_key = f'creatives/{workspace_id}/{file_id}'
content_type = _get_content_type(input.media_type)
await self.s3.upload(new_media_key, new_media_data, content_type)
log.info('Uploaded new creative media to S3: %s', new_media_key)
creative.media_s3_key = new_media_key
else:
creative.media_s3_key = None
if input.media_type is not None:
creative.media_type = input.media_type
if input.media_file_id is not None:
@@ -59,3 +79,16 @@ async def update_creative(
status=creative.status,
placements_count=placements_count,
)
def _get_content_type(media_type: str | None) -> str:
"""Map Telegram media type to MIME content type."""
if not media_type:
return 'application/octet-stream'
mapping = {
'photo': 'image/jpeg',
'video': 'video/mp4',
'animation': 'image/gif',
}
return mapping.get(media_type, 'application/octet-stream')