workspace avatar and send builded creative
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
# ruff: noqa
|
||||||
from tortoise import BaseDBAsyncClient
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
RUN_IN_TRANSACTION = True
|
||||||
@@ -32,75 +33,75 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
MODELS_STATE = (
|
||||||
"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'
|
||||||
"6PeujRRh85nntPEZ25IDp6BtDjfPnrwBM9vCMADM7OAX8MDMDtSxwMyAmQEzA2aWT2bGfu"
|
'6PeujRRh85nntPEZ25IDp6BtDjfPnrwBM9vCMADM7OAX8MDMDtSxwMyAmQEzA2aWT2bGfu'
|
||||||
"I77nKMtdpgWbZ+AMoNFWNQc5toUthYs+PDzftYUtovmx0tcs6omhQ3xur4UAPdCXSnfOpO"
|
'I77nKMtdpgWbZ+AMoNFWNQc5toUthYs+PDzftYUtovmx0tcs6omhQ3xur4UAPdCXSnfOpO'
|
||||||
"IX07AyjjX5Kyrw69/nt6zJC1Hjw6p2aAXtNXVVHh45YYMRofnVqzaH7+uoqKIL/WiNEEnd"
|
'IX07AyjjX5Kyrw69/nt6zJC1Hjw6p2aAXtNXVVHh45YYMRofnVqzaH7+uoqKIL/WiNEEnd'
|
||||||
"VwFi3Qq6mo8LHUIE+iex0ZivxYDpHX3ZyTVUK65JXJjV4e+V2ruJ+zch24mU6+oc7nfM2q"
|
'VwFi3Qq6mo8LHUIE+iex0ZivxYDpHX3ZyTVUK65JXJjV4e+V2ruJ+zch24mU6+oc7nfM2q'
|
||||||
"cla7rF1VL2rLj1gtU1ZFsdPvVEXL48/IiFZowtHzmRz5fW/+UY50jQQgusWLCeDZ6WkMAH"
|
'cla7rF1VL2rLj1gtU1ZFsdPvVEXL48/IiFZowtHzmRz5fW/+UY50jQQgusWLCeDZ6WkMAH'
|
||||||
"Gp6K+1kzxONNA1K3Se/TLodiIEA8+EA3Kk4Rf8OVFk66SkKqb1K5+wrkCRvDWj9FLwPtzV"
|
'Gp6K+1kzxONNA1K3Se/TLodiIEA8+EA3Kk4Rf8OVFk66SkKqb1K5+wrkCRvDWj9FLwPtzV'
|
||||||
"f/C4NtvdBk9VSAWNZJcSZj+9vP8f6Gvh+w=="
|
'f/C4NtvdBk9VSAWNZJcSZj+9vP8f6Gvh+w=='
|
||||||
)
|
)
|
||||||
|
|||||||
92
migrations/models/6_20260120222211_add_workspace_avatar.py
Normal file
92
migrations/models/6_20260120222211_add_workspace_avatar.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "workspace" ADD "avatar_s3_key" VARCHAR(512);"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "workspace" DROP COLUMN "avatar_s3_key";"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
'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'
|
||||||
|
'slSFZHCb7lLC/IeXX/8CYMWYuQ=='
|
||||||
|
)
|
||||||
@@ -16,6 +16,7 @@ class S3Config(BaseModel):
|
|||||||
SECRET_ACCESS_KEY: str
|
SECRET_ACCESS_KEY: str
|
||||||
BUCKET_NAME: str
|
BUCKET_NAME: str
|
||||||
REGION: str = 'us-east-1'
|
REGION: str = 'us-east-1'
|
||||||
|
PUBLIC_BASE_URL: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class S3(S3Storage):
|
class S3(S3Storage):
|
||||||
@@ -103,3 +104,9 @@ class S3(S3Storage):
|
|||||||
) as client:
|
) as client:
|
||||||
await client.delete_object(Bucket=self.config.BUCKET_NAME, Key=key)
|
await client.delete_object(Bucket=self.config.BUCKET_NAME, Key=key)
|
||||||
log.info(f'Deleted file from S3: {key}')
|
log.info(f'Deleted file from S3: {key}')
|
||||||
|
|
||||||
|
def public_url(self, key: str) -> str:
|
||||||
|
base_url = self.config.PUBLIC_BASE_URL
|
||||||
|
if not base_url:
|
||||||
|
base_url = f'{self.config.ENDPOINT_URL.rstrip("/")}/{self.config.BUCKET_NAME}'
|
||||||
|
return f'{base_url.rstrip("/")}/{key.lstrip("/")}'
|
||||||
|
|||||||
@@ -9,18 +9,77 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class TelegramBot(TelegramBase, TelegramBotWriter):
|
class TelegramBot(TelegramBase, TelegramBotWriter):
|
||||||
async def send_message(self, text: str, chat_id: int) -> None:
|
async def send_message(
|
||||||
await self.bot.send_message(chat_id=chat_id, text=text)
|
self, text: str, chat_id: int, parse_mode: str | None = None, disable_preview: bool = False
|
||||||
|
) -> None:
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=chat_id, text=text, parse_mode=parse_mode, disable_web_page_preview=disable_preview
|
||||||
|
)
|
||||||
|
|
||||||
async def create_chat_invite_link(self, chat_id: int, requires_approval: bool = False) -> str:
|
async def create_chat_invite_link(self, chat_id: int, requires_approval: bool = False) -> str:
|
||||||
invite_link = await self.bot.create_chat_invite_link(chat_id=chat_id, creates_join_request=requires_approval)
|
invite_link = await self.bot.create_chat_invite_link(chat_id=chat_id, creates_join_request=requires_approval)
|
||||||
return invite_link.invite_link
|
return invite_link.invite_link
|
||||||
|
|
||||||
async def send_message_with_inline_keyboard(
|
async def send_message_with_inline_keyboard(
|
||||||
self, text: str, chat_id: int, buttons: list[list[InlineKeyboardButton]]
|
self,
|
||||||
|
text: str,
|
||||||
|
chat_id: int,
|
||||||
|
buttons: list[list[InlineKeyboardButton]],
|
||||||
|
parse_mode: str | None = None,
|
||||||
|
disable_preview: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard)
|
await self.bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
disable_web_page_preview=disable_preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_media_with_inline_keyboard(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
chat_id: int,
|
||||||
|
media_type: str,
|
||||||
|
media_file_id: str,
|
||||||
|
buttons: list[list[InlineKeyboardButton]],
|
||||||
|
parse_mode: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) if buttons else None
|
||||||
|
if media_type == 'photo':
|
||||||
|
await self.bot.send_photo(
|
||||||
|
chat_id=chat_id,
|
||||||
|
photo=media_file_id,
|
||||||
|
caption=text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if media_type == 'video':
|
||||||
|
await self.bot.send_video(
|
||||||
|
chat_id=chat_id,
|
||||||
|
video=media_file_id,
|
||||||
|
caption=text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if media_type == 'animation':
|
||||||
|
await self.bot.send_animation(
|
||||||
|
chat_id=chat_id,
|
||||||
|
animation=media_file_id,
|
||||||
|
caption=text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await self.bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
)
|
||||||
|
|
||||||
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None:
|
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)
|
await self.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text, reply_markup=None)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Annotated, Literal, Union
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -86,13 +86,11 @@ class BotPermissionsEventRequest(dto.UpdateProjectPermissionsInput):
|
|||||||
|
|
||||||
|
|
||||||
EventRequest = Annotated[
|
EventRequest = Annotated[
|
||||||
Union[
|
SubscriptionEventRequest
|
||||||
SubscriptionEventRequest,
|
| UnsubscriptionEventRequest
|
||||||
UnsubscriptionEventRequest,
|
| BotAddedEventRequest
|
||||||
BotAddedEventRequest,
|
| BotRemovedEventRequest
|
||||||
BotRemovedEventRequest,
|
| BotPermissionsEventRequest,
|
||||||
BotPermissionsEventRequest,
|
|
||||||
],
|
|
||||||
Field(discriminator='type'),
|
Field(discriminator='type'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -117,15 +115,15 @@ async def handle_event(input: EventRequest) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(input, BotAddedEventRequest):
|
if isinstance(input, BotAddedEventRequest):
|
||||||
data = dto.ConnectProjectInput(**input.dict(exclude={'type'}))
|
add_data = dto.ConnectProjectInput(**input.dict(exclude={'type'}))
|
||||||
await deps.get_usecase().tg_add_project(input=data)
|
await deps.get_usecase().tg_add_project(input=add_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(input, BotRemovedEventRequest):
|
if isinstance(input, BotRemovedEventRequest):
|
||||||
data = dto.DisconnectProjectByTgIdInput(**input.dict(exclude={'type'}))
|
remove_data = dto.DisconnectProjectByTgIdInput(**input.dict(exclude={'type'}))
|
||||||
await deps.get_usecase().disconnect_project_by_tg_id(input=data)
|
await deps.get_usecase().disconnect_project_by_tg_id(input=remove_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(input, BotPermissionsEventRequest):
|
if isinstance(input, BotPermissionsEventRequest):
|
||||||
data = dto.UpdateProjectPermissionsInput(**input.dict(exclude={'type'}))
|
permissions_data = dto.UpdateProjectPermissionsInput(**input.dict(exclude={'type'}))
|
||||||
await deps.get_usecase().update_project_permissions(input=data)
|
await deps.get_usecase().update_project_permissions(input=permissions_data)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends, File, UploadFile
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from fastapi_pagination import Page, paginate
|
from fastapi_pagination import Page, paginate
|
||||||
|
|
||||||
@@ -42,3 +42,26 @@ async def delete_workspace(
|
|||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> None:
|
) -> None:
|
||||||
await deps.get_usecase().delete_workspace(workspace_id, current_user.user_id)
|
await deps.get_usecase().delete_workspace(workspace_id, current_user.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.post('/{workspace_id}/avatar')
|
||||||
|
async def upload_workspace_avatar(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
avatar_data = await file.read()
|
||||||
|
return await deps.get_usecase().update_workspace_avatar(
|
||||||
|
workspace_id,
|
||||||
|
current_user.user_id,
|
||||||
|
avatar_data,
|
||||||
|
file.content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.delete('/{workspace_id}/avatar')
|
||||||
|
async def delete_workspace_avatar(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
return await deps.get_usecase().delete_workspace_avatar(workspace_id, current_user.user_id)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ __all__ = (
|
|||||||
'replace_invite_link_with_tag',
|
'replace_invite_link_with_tag',
|
||||||
'validate_media_size',
|
'validate_media_size',
|
||||||
'MAX_CREATIVE_MEDIA_BYTES',
|
'MAX_CREATIVE_MEDIA_BYTES',
|
||||||
|
'validate_workspace_avatar_size',
|
||||||
|
'MAX_WORKSPACE_AVATAR_BYTES',
|
||||||
'Post',
|
'Post',
|
||||||
'Subscription',
|
'Subscription',
|
||||||
'PostViewsHistory',
|
'PostViewsHistory',
|
||||||
@@ -41,6 +43,7 @@ __all__ = (
|
|||||||
'WorkspaceInviteAlreadyExists',
|
'WorkspaceInviteAlreadyExists',
|
||||||
'WorkspaceInviteAlreadyProcessed',
|
'WorkspaceInviteAlreadyProcessed',
|
||||||
'WorkspaceMemberAlreadyExists',
|
'WorkspaceMemberAlreadyExists',
|
||||||
|
'WorkspaceAvatarTooLarge',
|
||||||
'LoginTokenNotFound',
|
'LoginTokenNotFound',
|
||||||
'LoginTokenExpired',
|
'LoginTokenExpired',
|
||||||
'LoginTokenAlreadyUsed',
|
'LoginTokenAlreadyUsed',
|
||||||
@@ -48,6 +51,7 @@ __all__ = (
|
|||||||
'ProjectChannelConflict',
|
'ProjectChannelConflict',
|
||||||
'PlacementNotFound',
|
'PlacementNotFound',
|
||||||
'PlacementPostNotFound',
|
'PlacementPostNotFound',
|
||||||
|
'PlacementHasPosts',
|
||||||
'ChannelNotFound',
|
'ChannelNotFound',
|
||||||
'ChannelAlreadyExists',
|
'ChannelAlreadyExists',
|
||||||
'ChannelNoAdminRights',
|
'ChannelNoAdminRights',
|
||||||
@@ -80,6 +84,7 @@ from .error import (
|
|||||||
LoginTokenAlreadyUsed,
|
LoginTokenAlreadyUsed,
|
||||||
LoginTokenExpired,
|
LoginTokenExpired,
|
||||||
LoginTokenNotFound,
|
LoginTokenNotFound,
|
||||||
|
PlacementHasPosts,
|
||||||
PlacementNotFound,
|
PlacementNotFound,
|
||||||
PlacementPostNotFound,
|
PlacementPostNotFound,
|
||||||
ProjectChannelConflict,
|
ProjectChannelConflict,
|
||||||
@@ -88,6 +93,7 @@ from .error import (
|
|||||||
UserByUsernameNotFound,
|
UserByUsernameNotFound,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
WorkspaceAccessDenied,
|
WorkspaceAccessDenied,
|
||||||
|
WorkspaceAvatarTooLarge,
|
||||||
WorkspaceInviteAlreadyExists,
|
WorkspaceInviteAlreadyExists,
|
||||||
WorkspaceInviteAlreadyProcessed,
|
WorkspaceInviteAlreadyProcessed,
|
||||||
WorkspaceInviteNotFound,
|
WorkspaceInviteNotFound,
|
||||||
@@ -110,6 +116,7 @@ from .subscription import Subscription, SubscriptionStatus
|
|||||||
from .telegram_user import TelegramUser
|
from .telegram_user import TelegramUser
|
||||||
from .user import User
|
from .user import User
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
|
MAX_WORKSPACE_AVATAR_BYTES,
|
||||||
PermissionKey,
|
PermissionKey,
|
||||||
PermissionScopeType,
|
PermissionScopeType,
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -119,6 +126,7 @@ from .workspace import (
|
|||||||
WorkspaceUserPermission,
|
WorkspaceUserPermission,
|
||||||
WorkspaceUserPermissionScope,
|
WorkspaceUserPermissionScope,
|
||||||
WorkspaceUserStatus,
|
WorkspaceUserStatus,
|
||||||
|
validate_workspace_avatar_size,
|
||||||
)
|
)
|
||||||
from .workspace_permissions import (
|
from .workspace_permissions import (
|
||||||
WorkspacePermissionContext,
|
WorkspacePermissionContext,
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ class Creative(TimestampedModel):
|
|||||||
MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
|
MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
|
_INVITE_LINK_HTML = re.compile(
|
||||||
|
r'<a\s+href=["\']https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+["\']>([^<]*)</a>',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
|
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
|
||||||
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>([^<]*)</a>', re.IGNORECASE)
|
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>([^<]*)</a>', re.IGNORECASE)
|
||||||
_INVITE_LINK_REPLACED_URL = re.compile(
|
_INVITE_LINK_REPLACED_URL = re.compile(
|
||||||
@@ -72,7 +75,8 @@ def replace_invite_link_with_tag(text: str) -> str:
|
|||||||
return _INVITE_LINK_REPLACED_URL.sub('<a class="tg-link"></a>', text)
|
return _INVITE_LINK_REPLACED_URL.sub('<a class="tg-link"></a>', text)
|
||||||
|
|
||||||
if html_count == 1:
|
if html_count == 1:
|
||||||
def _replace_html(match: re.Match) -> str:
|
|
||||||
|
def _replace_html(match: re.Match[str]) -> str:
|
||||||
inner = match.group(1).strip()
|
inner = match.group(1).strip()
|
||||||
if _INVITE_LINK_PLAIN.fullmatch(inner):
|
if _INVITE_LINK_PLAIN.fullmatch(inner):
|
||||||
return '<a class="tg-link"></a>'
|
return '<a class="tg-link"></a>'
|
||||||
|
|||||||
@@ -129,3 +129,7 @@ def WorkspaceInviteAlreadyProcessed() -> HTTPException:
|
|||||||
|
|
||||||
def WorkspaceMemberAlreadyExists() -> HTTPException:
|
def WorkspaceMemberAlreadyExists() -> HTTPException:
|
||||||
return HTTPException(status.HTTP_409_CONFLICT, 'User is already a workspace member')
|
return HTTPException(status.HTTP_409_CONFLICT, 'User is already a workspace member')
|
||||||
|
|
||||||
|
|
||||||
|
def WorkspaceAvatarTooLarge(max_bytes: int) -> HTTPException:
|
||||||
|
return HTTPException(status.HTTP_400_BAD_REQUEST, f'Workspace avatar is too large (max {max_bytes} bytes)')
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class Placement(TimestampedModel):
|
|||||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
creative: fields.ForeignKeyRelation['Creative'] | None = fields.ForeignKeyField(
|
||||||
'models.Creative', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
|
'models.Creative', related_name='placements', on_delete=fields.CASCADE, null=True, index=True
|
||||||
)
|
)
|
||||||
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|||||||
from tortoise import fields
|
from tortoise import fields
|
||||||
|
|
||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
from .error import WorkspaceAvatarTooLarge
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .channel import Channel
|
from .channel import Channel
|
||||||
@@ -19,6 +20,7 @@ if TYPE_CHECKING:
|
|||||||
class Workspace(TimestampedModel):
|
class Workspace(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
id = fields.UUIDField(pk=True)
|
||||||
name = fields.CharField(max_length=255)
|
name = fields.CharField(max_length=255)
|
||||||
|
avatar_s3_key = fields.CharField(max_length=512, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'workspace'
|
table = 'workspace'
|
||||||
@@ -149,3 +151,13 @@ class WorkspaceUserPermissionScope(TimestampedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'workspace_user_permission_scope'
|
table = 'workspace_user_permission_scope'
|
||||||
|
|
||||||
|
|
||||||
|
MAX_WORKSPACE_AVATAR_BYTES = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def validate_workspace_avatar_size(avatar_data: bytes | None) -> None:
|
||||||
|
if avatar_data is None:
|
||||||
|
return
|
||||||
|
if len(avatar_data) > MAX_WORKSPACE_AVATAR_BYTES:
|
||||||
|
raise WorkspaceAvatarTooLarge(MAX_WORKSPACE_AVATAR_BYTES)
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ from .purchase import (
|
|||||||
PlacementDetails,
|
PlacementDetails,
|
||||||
PlacementOutput,
|
PlacementOutput,
|
||||||
PlacementPostOutput,
|
PlacementPostOutput,
|
||||||
PostOutput,
|
|
||||||
PlacementWithPostsOutput,
|
PlacementWithPostsOutput,
|
||||||
|
PostOutput,
|
||||||
UpdatePlacementInput,
|
UpdatePlacementInput,
|
||||||
)
|
)
|
||||||
from .user import UserOutput
|
from .user import UserOutput
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import pydantic
|
|||||||
from src.domain.placement import CostType, InviteLinkType, PlacementStatus, PlacementType
|
from src.domain.placement import CostType, InviteLinkType, PlacementStatus, PlacementType
|
||||||
|
|
||||||
from .channel import ChannelOutput
|
from .channel import ChannelOutput
|
||||||
from .creative import CreativePreviewOutput
|
|
||||||
|
|
||||||
|
|
||||||
class CostInfo(pydantic.BaseModel):
|
class CostInfo(pydantic.BaseModel):
|
||||||
@@ -57,8 +56,6 @@ class PlacementWithPostsOutput(PlacementOutput):
|
|||||||
placement_post: PlacementPostOutput | None = None
|
placement_post: PlacementPostOutput | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CreatePlacementChannelInput(pydantic.BaseModel):
|
class CreatePlacementChannelInput(pydantic.BaseModel):
|
||||||
"""Input для создания одного placement (channel + детали)"""
|
"""Input для создания одного placement (channel + детали)"""
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from src import domain
|
|||||||
class WorkspaceMembershipOutput(pydantic.BaseModel):
|
class WorkspaceMembershipOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
name: str
|
name: str
|
||||||
|
avatar_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class CreateWorkspaceOutput(WorkspaceMembershipOutput): ...
|
class CreateWorkspaceOutput(WorkspaceMembershipOutput): ...
|
||||||
|
|||||||
@@ -51,19 +51,38 @@ from .workspace.accept_workspace_invite import accept_workspace_invite
|
|||||||
from .workspace.create_workspace import create_workspace
|
from .workspace.create_workspace import create_workspace
|
||||||
from .workspace.create_workspace_invite import create_workspace_invite
|
from .workspace.create_workspace_invite import create_workspace_invite
|
||||||
from .workspace.delete_workspace import delete_workspace
|
from .workspace.delete_workspace import delete_workspace
|
||||||
|
from .workspace.delete_workspace_avatar import delete_workspace_avatar
|
||||||
from .workspace.get_workspace_invites import get_workspace_invites
|
from .workspace.get_workspace_invites import get_workspace_invites
|
||||||
from .workspace.get_workspace_members import get_workspace_members
|
from .workspace.get_workspace_members import get_workspace_members
|
||||||
from .workspace.get_workspaces import get_workspaces
|
from .workspace.get_workspaces import get_workspaces
|
||||||
from .workspace.tg_accept_workspace_invite import tg_accept_workspace_invite
|
from .workspace.tg_accept_workspace_invite import tg_accept_workspace_invite
|
||||||
from .workspace.update_workspace import update_workspace
|
from .workspace.update_workspace import update_workspace
|
||||||
|
from .workspace.update_workspace_avatar import update_workspace_avatar
|
||||||
from .workspace.update_workspace_member_permissions import update_workspace_member_permissions
|
from .workspace.update_workspace_member_permissions import update_workspace_member_permissions
|
||||||
|
|
||||||
|
|
||||||
class TelegramBotWriter(typing.Protocol):
|
class TelegramBotWriter(typing.Protocol):
|
||||||
async def send_message(self, text: str, chat_id: int) -> None: ...
|
async def send_message(
|
||||||
|
self, text: str, chat_id: int, parse_mode: str | None = None, disable_preview: bool = False
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
async def send_message_with_inline_keyboard(
|
async def send_message_with_inline_keyboard(
|
||||||
self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']]
|
self,
|
||||||
|
text: str,
|
||||||
|
chat_id: int,
|
||||||
|
buttons: list[list['InlineKeyboardButton']],
|
||||||
|
parse_mode: str | None = None,
|
||||||
|
disable_preview: bool = False,
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
async def send_media_with_inline_keyboard(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
chat_id: int,
|
||||||
|
media_type: str,
|
||||||
|
media_file_id: str,
|
||||||
|
buttons: list[list['InlineKeyboardButton']],
|
||||||
|
parse_mode: str | None = None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|
||||||
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None: ...
|
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None: ...
|
||||||
@@ -94,6 +113,8 @@ class S3Storage(typing.Protocol):
|
|||||||
|
|
||||||
async def delete(self, key: str) -> None: ...
|
async def delete(self, key: str) -> None: ...
|
||||||
|
|
||||||
|
def public_url(self, key: str) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Usecase:
|
class Usecase:
|
||||||
@@ -194,6 +215,8 @@ class Usecase:
|
|||||||
create_workspace = create_workspace
|
create_workspace = create_workspace
|
||||||
update_workspace = update_workspace
|
update_workspace = update_workspace
|
||||||
delete_workspace = delete_workspace
|
delete_workspace = delete_workspace
|
||||||
|
update_workspace_avatar = update_workspace_avatar
|
||||||
|
delete_workspace_avatar = delete_workspace_avatar
|
||||||
get_workspace_members = get_workspace_members
|
get_workspace_members = get_workspace_members
|
||||||
update_workspace_member_permissions = update_workspace_member_permissions
|
update_workspace_member_permissions = update_workspace_member_permissions
|
||||||
create_workspace_invite = create_workspace_invite
|
create_workspace_invite = create_workspace_invite
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async def validate_login_token(self: 'Usecase', input: dto.ValidateLoginTokenInp
|
|||||||
message_id=login_token.message_id,
|
message_id=login_token.message_id,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.getLogger(__name__).exception("Failed to update login message")
|
logging.getLogger(__name__).exception('Failed to update login message')
|
||||||
|
|
||||||
access_token = self.jwt_encoder.encode_access_token(
|
access_token = self.jwt_encoder.encode_access_token(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import re
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from aiogram.types import InlineKeyboardButton
|
||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -18,7 +20,7 @@ def _inject_invite_link(text: str, invite_link: str) -> str:
|
|||||||
if not text:
|
if not text:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _replace(match: re.Match) -> str:
|
def _replace(match: re.Match[str]) -> str:
|
||||||
inner = match.group(1).strip()
|
inner = match.group(1).strip()
|
||||||
if inner:
|
if inner:
|
||||||
return f'<a href="{invite_link}">{inner}</a>'
|
return f'<a href="{invite_link}">{inner}</a>'
|
||||||
@@ -27,7 +29,7 @@ def _inject_invite_link(text: str, invite_link: str) -> str:
|
|||||||
return _INVITE_LINK_TAG.sub(_replace, text)
|
return _INVITE_LINK_TAG.sub(_replace, text)
|
||||||
|
|
||||||
|
|
||||||
def _build_buttons(buttons: list[dict], invite_link: str) -> list[dto.CreativeButton]:
|
def _build_buttons(buttons: list[dict[str, str]], invite_link: str) -> list[dto.CreativeButton]:
|
||||||
result: list[dto.CreativeButton] = []
|
result: list[dto.CreativeButton] = []
|
||||||
for raw in buttons:
|
for raw in buttons:
|
||||||
text = raw.get('text')
|
text = raw.get('text')
|
||||||
@@ -40,6 +42,18 @@ def _build_buttons(buttons: list[dict], invite_link: str) -> list[dto.CreativeBu
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _format_channel_name(channel: domain.Channel) -> str:
|
||||||
|
if channel.title:
|
||||||
|
return channel.title
|
||||||
|
if channel.username:
|
||||||
|
return f'@{channel.username}'
|
||||||
|
return 'Без названия'
|
||||||
|
|
||||||
|
|
||||||
|
def _build_keyboard_buttons(buttons: list[dto.CreativeButton]) -> list[list[InlineKeyboardButton]]:
|
||||||
|
return [[InlineKeyboardButton(text=btn.text, url=btn.url)] for btn in buttons]
|
||||||
|
|
||||||
|
|
||||||
async def build_placement_creative(
|
async def build_placement_creative(
|
||||||
self: 'Usecase',
|
self: 'Usecase',
|
||||||
placement_id: uuid.UUID,
|
placement_id: uuid.UUID,
|
||||||
@@ -47,9 +61,7 @@ async def build_placement_creative(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
) -> dto.CreativePreviewOutput:
|
) -> dto.CreativePreviewOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@@ -89,4 +101,35 @@ async def build_placement_creative(
|
|||||||
media_file_id=creative.media_file_id,
|
media_file_id=creative.media_file_id,
|
||||||
buttons=_build_buttons(creative.buttons, invite_link),
|
buttons=_build_buttons(creative.buttons, invite_link),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user = await self.database.get_user(user_id=user_id)
|
||||||
|
if not user or not user.telegram_user:
|
||||||
|
raise domain.UserNotFound(user_id)
|
||||||
|
|
||||||
|
chat_id = user.telegram_user.telegram_id
|
||||||
|
channel_name = _format_channel_name(placement.channel)
|
||||||
|
header = f'<b>Канал:</b> {channel_name}'
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
elif keyboard_buttons:
|
||||||
|
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||||
|
preview.text,
|
||||||
|
chat_id,
|
||||||
|
keyboard_buttons,
|
||||||
|
parse_mode='HTML',
|
||||||
|
disable_preview=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await self.telegram_bot.send_message(preview.text, chat_id, parse_mode='HTML', disable_preview=True)
|
||||||
|
|
||||||
return preview
|
return preview
|
||||||
|
|||||||
@@ -97,8 +97,10 @@ async def create_placements(
|
|||||||
# Объединяем общие детали с деталями конкретного канала
|
# Объединяем общие детали с деталями конкретного канала
|
||||||
channel_details = channel_input.details
|
channel_details = channel_input.details
|
||||||
creative_id = (
|
creative_id = (
|
||||||
channel_details.creative_id if channel_details and channel_details.creative_id else None
|
(channel_details.creative_id if channel_details and channel_details.creative_id else None)
|
||||||
) or (details.creative_id if details and details.creative_id else None) or input.creative_id
|
or (details.creative_id if details and details.creative_id else None)
|
||||||
|
or input.creative_id
|
||||||
|
)
|
||||||
if not creative_id:
|
if not creative_id:
|
||||||
raise domain.CreativeNotFound()
|
raise domain.CreativeNotFound()
|
||||||
creative = creatives_by_id.get(creative_id)
|
creative = creatives_by_id.get(creative_id)
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
if placement_posts_for_placement:
|
if placement_posts_for_placement:
|
||||||
if len(placement_posts_for_placement) > 1:
|
if len(placement_posts_for_placement) > 1:
|
||||||
log.warning(
|
log.warning(
|
||||||
'Placement %s has %s placement_posts, returning latest', placement.id, len(placement_posts_for_placement)
|
'Placement %s has %s placement_posts, returning latest',
|
||||||
|
placement.id,
|
||||||
|
len(placement_posts_for_placement),
|
||||||
)
|
)
|
||||||
placement_post_output = placement_posts_for_placement[0]
|
placement_post_output = placement_posts_for_placement[0]
|
||||||
placement_outputs.append(
|
placement_outputs.append(
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ async def update_placement(
|
|||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
) -> dto.PlacementWithPostsOutput:
|
) -> dto.PlacementWithPostsOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not project:
|
if not project:
|
||||||
|
|||||||
@@ -30,4 +30,5 @@ async def create_workspace(
|
|||||||
return dto.CreateWorkspaceOutput(
|
return dto.CreateWorkspaceOutput(
|
||||||
id=workspace.id,
|
id=workspace.id,
|
||||||
name=workspace.name,
|
name=workspace.name,
|
||||||
|
avatar_url=self.s3.public_url(workspace.avatar_s3_key) if workspace.avatar_s3_key else None,
|
||||||
)
|
)
|
||||||
|
|||||||
42
src/usecase/workspace/delete_workspace_avatar.py
Normal file
42
src/usecase/workspace/delete_workspace_avatar.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_workspace_avatar(
|
||||||
|
self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL)
|
||||||
|
|
||||||
|
membership = await self.database.get_workspace_membership(workspace_id, user_id)
|
||||||
|
if not membership or not membership.workspace:
|
||||||
|
raise domain.WorkspaceNotFound(workspace_id)
|
||||||
|
|
||||||
|
workspace = membership.workspace
|
||||||
|
old_key = workspace.avatar_s3_key
|
||||||
|
if old_key:
|
||||||
|
workspace.avatar_s3_key = None
|
||||||
|
await self.database.update_workspace(workspace)
|
||||||
|
|
||||||
|
async def delete_old_avatar() -> None:
|
||||||
|
try:
|
||||||
|
await self.s3.delete(old_key)
|
||||||
|
log.info('Deleted workspace avatar from S3: %s', old_key)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning('Failed to delete workspace avatar from S3: %s', exc)
|
||||||
|
|
||||||
|
asyncio.create_task(delete_old_avatar())
|
||||||
|
|
||||||
|
return dto.WorkspaceMembershipOutput(
|
||||||
|
id=workspace.id,
|
||||||
|
name=workspace.name,
|
||||||
|
avatar_url=None,
|
||||||
|
)
|
||||||
@@ -14,6 +14,13 @@ async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> list[dto.Worksp
|
|||||||
dto.WorkspaceMembershipOutput(
|
dto.WorkspaceMembershipOutput(
|
||||||
id=membership.workspace_id,
|
id=membership.workspace_id,
|
||||||
name=membership.workspace.name,
|
name=membership.workspace.name,
|
||||||
|
avatar_url=_build_avatar_url(self, membership.workspace.avatar_s3_key),
|
||||||
)
|
)
|
||||||
for membership in memberships
|
for membership in memberships
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_avatar_url(self: 'Usecase', avatar_key: str | None) -> str | None:
|
||||||
|
if not avatar_key:
|
||||||
|
return None
|
||||||
|
return self.s3.public_url(avatar_key)
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ async def update_workspace(
|
|||||||
return dto.WorkspaceMembershipOutput(
|
return dto.WorkspaceMembershipOutput(
|
||||||
id=workspace.id,
|
id=workspace.id,
|
||||||
name=workspace.name,
|
name=workspace.name,
|
||||||
|
avatar_url=self.s3.public_url(workspace.avatar_s3_key) if workspace.avatar_s3_key else None,
|
||||||
)
|
)
|
||||||
|
|||||||
53
src/usecase/workspace/update_workspace_avatar.py
Normal file
53
src/usecase/workspace/update_workspace_avatar.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_workspace_avatar(
|
||||||
|
self: 'Usecase',
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
avatar_data: bytes,
|
||||||
|
content_type: str | None,
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.ADMIN_FULL)
|
||||||
|
|
||||||
|
membership = await self.database.get_workspace_membership(workspace_id, user_id)
|
||||||
|
if not membership or not membership.workspace:
|
||||||
|
raise domain.WorkspaceNotFound(workspace_id)
|
||||||
|
|
||||||
|
domain.validate_workspace_avatar_size(avatar_data)
|
||||||
|
|
||||||
|
file_id = uuid.uuid4()
|
||||||
|
avatar_key = f'workspaces/{workspace_id}/avatars/{file_id}'
|
||||||
|
await self.s3.upload(avatar_key, avatar_data, content_type or 'application/octet-stream')
|
||||||
|
|
||||||
|
workspace = membership.workspace
|
||||||
|
old_key = workspace.avatar_s3_key
|
||||||
|
workspace.avatar_s3_key = avatar_key
|
||||||
|
await self.database.update_workspace(workspace)
|
||||||
|
|
||||||
|
if old_key:
|
||||||
|
|
||||||
|
async def delete_old_avatar() -> None:
|
||||||
|
try:
|
||||||
|
await self.s3.delete(old_key)
|
||||||
|
log.info('Deleted old workspace avatar from S3: %s', old_key)
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning('Failed to delete old workspace avatar from S3: %s', exc)
|
||||||
|
|
||||||
|
asyncio.create_task(delete_old_avatar())
|
||||||
|
|
||||||
|
return dto.WorkspaceMembershipOutput(
|
||||||
|
id=workspace.id,
|
||||||
|
name=workspace.name,
|
||||||
|
avatar_url=self.s3.public_url(avatar_key),
|
||||||
|
)
|
||||||
@@ -1439,7 +1439,7 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, placement := range placements.Placements {
|
for _, placement := range placements.Placements {
|
||||||
creativePreview, err := b.Backend.BuildPlacementCreative(
|
_, err := b.Backend.BuildPlacementCreative(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
jwt,
|
jwt,
|
||||||
b.Session.WorkspaceID,
|
b.Session.WorkspaceID,
|
||||||
@@ -1450,12 +1450,6 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) {
|
|||||||
log.Error().Err(err).Msg("Failed to build placement creative")
|
log.Error().Err(err).Msg("Failed to build placement creative")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
channelName := s.formatChannelName(placement)
|
|
||||||
header := fmt.Sprintf("<b>Канал:</b> %s", channelName)
|
|
||||||
b.SendMessage(header, b.ChatID, &echotron.MessageOptions{ParseMode: echotron.HTML})
|
|
||||||
|
|
||||||
s.sendCreativePreview(b, creativePreview)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.SetState(&Placements{
|
b.SetState(&Placements{
|
||||||
@@ -1580,36 +1574,3 @@ func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) formatChannelName(placement backend.PlacementOutput) string {
|
|
||||||
if placement.Channel.Title != nil && *placement.Channel.Title != "" {
|
|
||||||
return *placement.Channel.Title
|
|
||||||
}
|
|
||||||
if placement.Channel.Username != nil && *placement.Channel.Username != "" {
|
|
||||||
return "@" + *placement.Channel.Username
|
|
||||||
}
|
|
||||||
return "Без названия"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) sendCreativePreview(
|
|
||||||
b *bot.Bot,
|
|
||||||
creative *backend.CreativePreviewOutput,
|
|
||||||
) {
|
|
||||||
editor := CreativeEditorFields{
|
|
||||||
Text: &creative.Text,
|
|
||||||
MediaType: creative.MediaType,
|
|
||||||
MediaFileID: creative.MediaFileID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(creative.Buttons) > 0 {
|
|
||||||
editor.Buttons = make([]InlineButton, 0, len(creative.Buttons))
|
|
||||||
for _, btn := range creative.Buttons {
|
|
||||||
editor.Buttons = append(editor.Buttons, InlineButton{
|
|
||||||
Text: btn.Text,
|
|
||||||
URL: btn.URL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.SendCreativePreview(b)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user