бэк несколько медиа
This commit is contained in:
@@ -348,7 +348,7 @@ class Postgres(DatabaseBase):
|
||||
@staticmethod
|
||||
async def get_creative(workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
||||
return await domain.Creative.get_or_none(id=creative_id, project__workspace_id=workspace_id).prefetch_related(
|
||||
'project', 'project__channel'
|
||||
'project', 'project__channel', 'media_items'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -374,7 +374,7 @@ class Postgres(DatabaseBase):
|
||||
if not include_archived:
|
||||
query = query.filter(status=domain.CreativeStatus.ACTIVE)
|
||||
|
||||
return await query.prefetch_related('project', 'project__channel').order_by('-created_at').all()
|
||||
return await query.prefetch_related('project', 'project__channel', 'media_items').order_by('-created_at').all()
|
||||
|
||||
@staticmethod
|
||||
async def delete_creative(creative_id: uuid.UUID) -> None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaVideo
|
||||
|
||||
from shared.telegram_base import TelegramBase
|
||||
from src.usecase import TelegramBotWriter
|
||||
@@ -81,6 +82,32 @@ class TelegramBot(TelegramBase, TelegramBotWriter):
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
async def send_media_group(
|
||||
self,
|
||||
chat_id: int,
|
||||
media_items: list[Any],
|
||||
caption: str | None = None,
|
||||
parse_mode: str | None = None,
|
||||
) -> None:
|
||||
if not media_items:
|
||||
return
|
||||
|
||||
group = []
|
||||
for index, item in enumerate(media_items):
|
||||
item_caption = caption if index == 0 else None
|
||||
if item.media_type == 'photo':
|
||||
group.append(
|
||||
InputMediaPhoto(media=item.media_file_id, caption=item_caption, parse_mode=parse_mode)
|
||||
)
|
||||
elif item.media_type == 'video':
|
||||
group.append(
|
||||
InputMediaVideo(media=item.media_file_id, caption=item_caption, parse_mode=parse_mode)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f'Unsupported media type for group: {item.media_type}')
|
||||
|
||||
await self.bot.send_media_group(chat_id=chat_id, media=group)
|
||||
|
||||
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None:
|
||||
await self.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text, reply_markup=None)
|
||||
|
||||
|
||||
@@ -21,9 +21,12 @@ __all__ = (
|
||||
'PlacementType',
|
||||
'PlacementPost',
|
||||
'Creative',
|
||||
'CreativeMedia',
|
||||
'replace_invite_link_with_tag',
|
||||
'validate_media_size',
|
||||
'validate_media_items',
|
||||
'MAX_CREATIVE_MEDIA_BYTES',
|
||||
'MAX_CREATIVE_MEDIA_ITEMS',
|
||||
'validate_workspace_avatar_size',
|
||||
'MAX_WORKSPACE_AVATAR_BYTES',
|
||||
'Post',
|
||||
@@ -61,15 +64,20 @@ __all__ = (
|
||||
'CreativeInviteLinkNotFound',
|
||||
'CreativeMultipleInviteLinks',
|
||||
'CreativeMediaTooLarge',
|
||||
'CreativeMediaTooMany',
|
||||
'CreativeMediaGroupUnsupported',
|
||||
'UserByUsernameNotFound',
|
||||
)
|
||||
|
||||
from .channel import Channel
|
||||
from .creative import (
|
||||
MAX_CREATIVE_MEDIA_BYTES,
|
||||
MAX_CREATIVE_MEDIA_ITEMS,
|
||||
Creative,
|
||||
CreativeMedia,
|
||||
CreativeStatus,
|
||||
replace_invite_link_with_tag,
|
||||
validate_media_items,
|
||||
validate_media_size,
|
||||
)
|
||||
from .error import (
|
||||
@@ -78,7 +86,9 @@ from .error import (
|
||||
ChannelNotFound,
|
||||
CreativeInUse,
|
||||
CreativeInviteLinkNotFound,
|
||||
CreativeMediaGroupUnsupported,
|
||||
CreativeMediaTooLarge,
|
||||
CreativeMediaTooMany,
|
||||
CreativeMultipleInviteLinks,
|
||||
CreativeNotFound,
|
||||
LoginTokenAlreadyUsed,
|
||||
|
||||
@@ -6,7 +6,13 @@ from uuid import UUID
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
from .error import CreativeInviteLinkNotFound, CreativeMediaTooLarge, CreativeMultipleInviteLinks
|
||||
from .error import (
|
||||
CreativeInviteLinkNotFound,
|
||||
CreativeMediaGroupUnsupported,
|
||||
CreativeMediaTooLarge,
|
||||
CreativeMediaTooMany,
|
||||
CreativeMultipleInviteLinks,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .project import Project
|
||||
@@ -20,9 +26,6 @@ class CreativeStatus(str, enum.Enum):
|
||||
class Creative(TimestampedModel):
|
||||
name = fields.CharField(max_length=255)
|
||||
text = fields.TextField()
|
||||
media_type = fields.CharField(max_length=32, null=True)
|
||||
media_file_id = fields.CharField(max_length=512, null=True)
|
||||
media_s3_key = fields.CharField(max_length=512, null=True)
|
||||
buttons = fields.JSONField(default=list)
|
||||
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
||||
|
||||
@@ -37,7 +40,26 @@ class Creative(TimestampedModel):
|
||||
table = 'creative'
|
||||
|
||||
|
||||
class CreativeMedia(TimestampedModel):
|
||||
media_type = fields.CharField(max_length=32)
|
||||
media_file_id = fields.CharField(max_length=512)
|
||||
media_s3_key = fields.CharField(max_length=512, null=True)
|
||||
position = fields.IntField()
|
||||
|
||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||
'models.Creative', related_name='media_items', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
creative_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'creative_media'
|
||||
unique_together = (('creative_id', 'position'),)
|
||||
|
||||
|
||||
MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
|
||||
MAX_CREATIVE_MEDIA_ITEMS = 10
|
||||
|
||||
|
||||
_INVITE_LINK_HTML = re.compile(
|
||||
@@ -99,3 +121,10 @@ def validate_media_size(media_data: bytes | None) -> None:
|
||||
return
|
||||
if len(media_data) > MAX_CREATIVE_MEDIA_BYTES:
|
||||
raise CreativeMediaTooLarge(MAX_CREATIVE_MEDIA_BYTES)
|
||||
|
||||
|
||||
def validate_media_items(media_types: list[str]) -> None:
|
||||
if len(media_types) > MAX_CREATIVE_MEDIA_ITEMS:
|
||||
raise CreativeMediaTooMany(MAX_CREATIVE_MEDIA_ITEMS)
|
||||
if len(media_types) > 1 and "animation" in media_types:
|
||||
raise CreativeMediaGroupUnsupported()
|
||||
|
||||
@@ -94,6 +94,17 @@ def CreativeMediaTooLarge(max_bytes: int) -> HTTPException:
|
||||
return HTTPException(status.HTTP_400_BAD_REQUEST, f'Creative media is too large (max {max_bytes} bytes)')
|
||||
|
||||
|
||||
def CreativeMediaTooMany(max_items: int) -> HTTPException:
|
||||
return HTTPException(status.HTTP_400_BAD_REQUEST, f'Creative media exceeds max items ({max_items})')
|
||||
|
||||
|
||||
def CreativeMediaGroupUnsupported() -> HTTPException:
|
||||
return HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
'Creative media group supports only photo/video; animation allowed only as a single item',
|
||||
)
|
||||
|
||||
|
||||
def CreativeInUse(creative_id: uuid.UUID) -> HTTPException:
|
||||
return HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -35,6 +35,8 @@ __all__ = (
|
||||
'PostOutput',
|
||||
'PlacementWithPostsOutput',
|
||||
'CreativeButton',
|
||||
'CreativeMediaInput',
|
||||
'CreativeMediaItem',
|
||||
'CreativeOutput',
|
||||
'CreativePreviewOutput',
|
||||
'GetCreativesInput',
|
||||
@@ -134,6 +136,8 @@ from .channel import (
|
||||
from .creative import (
|
||||
CreateCreativeInput,
|
||||
CreativeButton,
|
||||
CreativeMediaInput,
|
||||
CreativeMediaItem,
|
||||
CreativeOutput,
|
||||
CreativePreviewOutput,
|
||||
DeleteCreativeInput,
|
||||
|
||||
@@ -11,12 +11,23 @@ class CreativeButton(pydantic.BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
class CreativeMediaItem(pydantic.BaseModel):
|
||||
media_type: str
|
||||
media_file_id: str
|
||||
position: int
|
||||
|
||||
|
||||
class CreativeMediaInput(pydantic.BaseModel):
|
||||
media_type: str
|
||||
media_file_id: str
|
||||
media_data: bytes | None = None
|
||||
|
||||
|
||||
class CreativeOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
text: str
|
||||
media_type: str | None
|
||||
media_file_id: str | None
|
||||
media_items: list[CreativeMediaItem]
|
||||
buttons: list[CreativeButton]
|
||||
project_id: uuid.UUID
|
||||
project_channel_title: str
|
||||
@@ -29,8 +40,7 @@ class CreativePreviewOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
text: str
|
||||
media_type: str | None
|
||||
media_file_id: str | None
|
||||
media_items: list[CreativeMediaItem]
|
||||
buttons: list[CreativeButton]
|
||||
|
||||
|
||||
@@ -54,18 +64,14 @@ class GetCreativeInput(pydantic.BaseModel):
|
||||
class CreateCreativeInput(pydantic.BaseModel):
|
||||
name: str
|
||||
text: str
|
||||
media_type: str | None = None
|
||||
media_file_id: str | None = None
|
||||
media_data: bytes | None = None
|
||||
media_items: list[CreativeMediaInput] = pydantic.Field(default_factory=list)
|
||||
buttons: list[CreativeButton] = pydantic.Field(default_factory=list)
|
||||
|
||||
|
||||
class UpdateCreativeInput(pydantic.BaseModel):
|
||||
name: str | None = None
|
||||
text: str | None = None
|
||||
media_type: str | None = None
|
||||
media_file_id: str | None = None
|
||||
media_data: bytes | None = None
|
||||
media_items: list[CreativeMediaInput] | None = None
|
||||
buttons: list[CreativeButton] | None = None
|
||||
status: domain.CreativeStatus | None = None
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ from .workspace.update_workspace_avatar import update_workspace_avatar
|
||||
from .workspace.update_workspace_member_permissions import update_workspace_member_permissions
|
||||
|
||||
|
||||
class MediaItem(typing.Protocol):
|
||||
media_type: str
|
||||
media_file_id: str
|
||||
|
||||
|
||||
class TelegramBotWriter(typing.Protocol):
|
||||
async def send_message(
|
||||
self, text: str, chat_id: int, parse_mode: str | None = None, disable_preview: bool = False
|
||||
@@ -86,6 +91,14 @@ class TelegramBotWriter(typing.Protocol):
|
||||
parse_mode: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
async def send_media_group(
|
||||
self,
|
||||
chat_id: int,
|
||||
media_items: list[MediaItem],
|
||||
caption: str | None = None,
|
||||
parse_mode: str | None = None,
|
||||
) -> None: ...
|
||||
|
||||
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None: ...
|
||||
|
||||
async def edit_message_reply_markup(self, chat_id: int, message_id: int) -> None: ...
|
||||
|
||||
@@ -22,36 +22,28 @@ async def create_creative(
|
||||
log.warning('User %s attempted to create creative for unavailable project %s', user_id, project_id)
|
||||
raise domain.ProjectNotFound(project_id)
|
||||
|
||||
domain.validate_media_size(input.media_data)
|
||||
|
||||
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)
|
||||
media_items = input.media_items or []
|
||||
domain.validate_media_items([item.media_type for item in media_items])
|
||||
for item in media_items:
|
||||
domain.validate_media_size(item.media_data)
|
||||
|
||||
creative = domain.Creative(
|
||||
name=input.name,
|
||||
text=creative_text,
|
||||
media_type=input.media_type,
|
||||
media_file_id=input.media_file_id,
|
||||
media_s3_key=media_s3_key,
|
||||
buttons=[button.model_dump() for button in input.buttons],
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
project_id=project.id,
|
||||
)
|
||||
await self.database.create_creative(creative)
|
||||
|
||||
created_media = await _replace_media_items(self, creative.id, workspace_id, media_items)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=creative.text,
|
||||
media_type=creative.media_type,
|
||||
media_file_id=creative.media_file_id,
|
||||
media_items=created_media,
|
||||
buttons=creative.buttons,
|
||||
project_id=project.id,
|
||||
project_channel_title=project.channel.title,
|
||||
@@ -72,3 +64,35 @@ def _get_content_type(media_type: str | None) -> str:
|
||||
'animation': 'image/gif',
|
||||
}
|
||||
return mapping.get(media_type, 'application/octet-stream')
|
||||
|
||||
|
||||
async def _replace_media_items(
|
||||
self: 'Usecase',
|
||||
creative_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
media_items: list[dto.CreativeMediaInput],
|
||||
) -> list[dto.CreativeMediaItem]:
|
||||
created: list[dto.CreativeMediaItem] = []
|
||||
for position, item in enumerate(media_items):
|
||||
media_s3_key: str | None = None
|
||||
if item.media_data:
|
||||
file_id = uuid.uuid4()
|
||||
media_s3_key = f'creatives/{workspace_id}/{file_id}'
|
||||
content_type = _get_content_type(item.media_type)
|
||||
await self.s3.upload(media_s3_key, item.media_data, content_type)
|
||||
log.info('Uploaded creative media to S3: %s', media_s3_key)
|
||||
media = await domain.CreativeMedia.create(
|
||||
creative_id=creative_id,
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
media_s3_key=media_s3_key,
|
||||
position=position,
|
||||
)
|
||||
created.append(
|
||||
dto.CreativeMediaItem(
|
||||
media_type=media.media_type,
|
||||
media_file_id=media.media_file_id,
|
||||
position=media.position,
|
||||
)
|
||||
)
|
||||
return created
|
||||
|
||||
@@ -27,15 +27,17 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No
|
||||
log.warning('Creative %s is used in placement_posts and cannot be deleted', input.creative_id)
|
||||
raise domain.CreativeInUse(input.creative_id)
|
||||
|
||||
media_key = creative.media_s3_key
|
||||
if media_key:
|
||||
media_items = await creative.media_items.all()
|
||||
media_keys = [item.media_s3_key for item in media_items if item.media_s3_key]
|
||||
if media_keys:
|
||||
|
||||
async def delete_old_media() -> None:
|
||||
try:
|
||||
await self.s3.delete(media_key)
|
||||
log.info('Deleted old creative media from S3: %s', media_key)
|
||||
except Exception as e:
|
||||
log.warning('Failed to delete old creative media from S3: %s', e)
|
||||
for key in media_keys:
|
||||
try:
|
||||
await self.s3.delete(key)
|
||||
log.info('Deleted old creative media from S3: %s', key)
|
||||
except Exception as e:
|
||||
log.warning('Failed to delete old creative media from S3: %s', e)
|
||||
|
||||
asyncio.create_task(delete_old_media())
|
||||
|
||||
|
||||
@@ -21,13 +21,24 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea
|
||||
context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id)
|
||||
|
||||
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||
media_rel = creative.media_items
|
||||
if hasattr(media_rel, 'all'):
|
||||
media_items = await media_rel.all().order_by('position')
|
||||
else:
|
||||
media_items = sorted(media_rel, key=lambda item: item.position)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=creative.text,
|
||||
media_type=creative.media_type,
|
||||
media_file_id=creative.media_file_id,
|
||||
media_items=[
|
||||
dto.CreativeMediaItem(
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
position=item.position,
|
||||
)
|
||||
for item in media_items
|
||||
],
|
||||
buttons=creative.buttons,
|
||||
project_id=creative.project_id,
|
||||
project_channel_title=creative.project.channel.title,
|
||||
|
||||
@@ -27,19 +27,32 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[d
|
||||
creative_ids = [c.id for c in creatives]
|
||||
placements_counts = await self.database.count_placement_posts_by_creative_batch(creative_ids)
|
||||
|
||||
return [
|
||||
dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=creative.text,
|
||||
media_type=creative.media_type,
|
||||
media_file_id=creative.media_file_id,
|
||||
buttons=creative.buttons,
|
||||
project_id=creative.project_id,
|
||||
project_channel_title=creative.project.channel.title,
|
||||
created_at=creative.created_at,
|
||||
status=creative.status,
|
||||
placements_count=placements_counts.get(creative.id, 0),
|
||||
results: list[dto.CreativeOutput] = []
|
||||
for creative in creatives:
|
||||
media_rel = creative.media_items
|
||||
if hasattr(media_rel, 'all'):
|
||||
media_items = await media_rel.all().order_by('position')
|
||||
else:
|
||||
media_items = sorted(media_rel, key=lambda item: item.position)
|
||||
results.append(
|
||||
dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=creative.text,
|
||||
media_items=[
|
||||
dto.CreativeMediaItem(
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
position=item.position,
|
||||
)
|
||||
for item in media_items
|
||||
],
|
||||
buttons=creative.buttons,
|
||||
project_id=creative.project_id,
|
||||
project_channel_title=creative.project.channel.title,
|
||||
created_at=creative.created_at,
|
||||
status=creative.status,
|
||||
placements_count=placements_counts.get(creative.id, 0),
|
||||
)
|
||||
)
|
||||
for creative in creatives
|
||||
]
|
||||
return results
|
||||
|
||||
@@ -31,37 +31,12 @@ async def update_creative(
|
||||
creative.name = input.name
|
||||
if input.text:
|
||||
creative.text = domain.replace_invite_link_with_tag(input.text)
|
||||
if input.media_data is not None:
|
||||
domain.validate_media_size(input.media_data)
|
||||
|
||||
# Удаляем старое медиа из S3 в фоне
|
||||
old_media_key = creative.media_s3_key
|
||||
if old_media_key:
|
||||
|
||||
async def delete_old_media() -> None:
|
||||
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)
|
||||
|
||||
asyncio.create_task(delete_old_media())
|
||||
|
||||
# Загружаем новое медиа в 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:
|
||||
creative.media_file_id = input.media_file_id
|
||||
media_items: list[dto.CreativeMediaInput] | None = None
|
||||
if input.media_items is not None:
|
||||
media_items = input.media_items
|
||||
domain.validate_media_items([item.media_type for item in media_items])
|
||||
for item in media_items:
|
||||
domain.validate_media_size(item.media_data)
|
||||
if input.buttons is not None:
|
||||
creative.buttons = [button.model_dump() for button in input.buttons]
|
||||
if input.status:
|
||||
@@ -69,14 +44,17 @@ async def update_creative(
|
||||
|
||||
await self.database.update_creative(creative)
|
||||
|
||||
if media_items is not None:
|
||||
await _replace_media_items(self, creative.id, workspace_id, media_items)
|
||||
|
||||
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||
creative_media = await _get_media_items(creative)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=creative.text,
|
||||
media_type=creative.media_type,
|
||||
media_file_id=creative.media_file_id,
|
||||
media_items=creative_media,
|
||||
buttons=creative.buttons,
|
||||
project_id=creative.project_id,
|
||||
project_channel_title=creative.project.channel.title,
|
||||
@@ -96,3 +74,59 @@ def _get_content_type(media_type: str | None) -> str:
|
||||
'animation': 'image/gif',
|
||||
}
|
||||
return mapping.get(media_type, 'application/octet-stream')
|
||||
|
||||
|
||||
async def _replace_media_items(
|
||||
self: 'Usecase',
|
||||
creative_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
media_items: list[dto.CreativeMediaInput],
|
||||
) -> None:
|
||||
existing_items = await domain.CreativeMedia.filter(creative_id=creative_id).all()
|
||||
if existing_items:
|
||||
old_media_keys = [item.media_s3_key for item in existing_items if item.media_s3_key]
|
||||
await domain.CreativeMedia.filter(creative_id=creative_id).delete()
|
||||
|
||||
if old_media_keys:
|
||||
|
||||
async def delete_old_media() -> None:
|
||||
for key in old_media_keys:
|
||||
try:
|
||||
await self.s3.delete(key)
|
||||
log.info('Deleted old creative media from S3: %s', key)
|
||||
except Exception as e:
|
||||
log.warning('Failed to delete old creative media from S3: %s', e)
|
||||
|
||||
asyncio.create_task(delete_old_media())
|
||||
|
||||
for position, item in enumerate(media_items):
|
||||
media_s3_key: str | None = None
|
||||
if item.media_data:
|
||||
file_id = uuid.uuid4()
|
||||
media_s3_key = f'creatives/{workspace_id}/{file_id}'
|
||||
content_type = _get_content_type(item.media_type)
|
||||
await self.s3.upload(media_s3_key, item.media_data, content_type)
|
||||
log.info('Uploaded new creative media to S3: %s', media_s3_key)
|
||||
await domain.CreativeMedia.create(
|
||||
creative_id=creative_id,
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
media_s3_key=media_s3_key,
|
||||
position=position,
|
||||
)
|
||||
|
||||
|
||||
async def _get_media_items(creative: domain.Creative) -> list[dto.CreativeMediaItem]:
|
||||
media_rel = creative.media_items
|
||||
if hasattr(media_rel, 'all'):
|
||||
items = await media_rel.all().order_by('position')
|
||||
else:
|
||||
items = sorted(media_rel, key=lambda item: item.position)
|
||||
return [
|
||||
dto.CreativeMediaItem(
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
position=item.position,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
|
||||
@@ -93,12 +93,23 @@ async def build_placement_creative(
|
||||
if not invite_link:
|
||||
raise domain.PlacementNotFound(placement.id)
|
||||
|
||||
media_rel = creative.media_items
|
||||
if hasattr(media_rel, 'all'):
|
||||
media_items = await media_rel.all().order_by('position')
|
||||
else:
|
||||
media_items = sorted(media_rel, key=lambda item: item.position)
|
||||
preview = dto.CreativePreviewOutput(
|
||||
id=creative.id,
|
||||
name=creative.name,
|
||||
text=_inject_invite_link(creative.text, invite_link),
|
||||
media_type=creative.media_type,
|
||||
media_file_id=creative.media_file_id,
|
||||
media_items=[
|
||||
dto.CreativeMediaItem(
|
||||
media_type=item.media_type,
|
||||
media_file_id=item.media_file_id,
|
||||
position=item.position,
|
||||
)
|
||||
for item in media_items
|
||||
],
|
||||
buttons=_build_buttons(creative.buttons, invite_link),
|
||||
)
|
||||
|
||||
@@ -112,15 +123,24 @@ async def build_placement_creative(
|
||||
await self.telegram_bot.send_message(header, chat_id, parse_mode='HTML')
|
||||
|
||||
keyboard_buttons = _build_keyboard_buttons(preview.buttons)
|
||||
if preview.media_type and preview.media_file_id:
|
||||
await self.telegram_bot.send_media_with_inline_keyboard(
|
||||
text=preview.text,
|
||||
chat_id=chat_id,
|
||||
media_type=preview.media_type,
|
||||
media_file_id=preview.media_file_id,
|
||||
buttons=keyboard_buttons,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
if preview.media_items:
|
||||
if len(preview.media_items) == 1:
|
||||
media_item = preview.media_items[0]
|
||||
await self.telegram_bot.send_media_with_inline_keyboard(
|
||||
text=preview.text,
|
||||
chat_id=chat_id,
|
||||
media_type=media_item.media_type,
|
||||
media_file_id=media_item.media_file_id,
|
||||
buttons=keyboard_buttons,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
else:
|
||||
await self.telegram_bot.send_media_group(
|
||||
chat_id=chat_id,
|
||||
media_items=preview.media_items,
|
||||
caption=preview.text,
|
||||
parse_mode='HTML',
|
||||
)
|
||||
elif keyboard_buttons:
|
||||
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||
preview.text,
|
||||
|
||||
Reference in New Issue
Block a user