From 3cb3da4dbe0fea15a4b37d95e2908be6a7494ac8 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Sat, 13 Dec 2025 22:36:33 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BB=D0=BE=D1=83=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20workspaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/telegram_callback/__init__.py | 1 + .../telegram_callback/creative_commands.py | 25 ++- .../telegram_callback/my_chat_member.py | 2 +- .../target_channel_commands.py | 26 ++++ src/domain/telegram_state.py | 2 + src/dto/__init__.py | 12 -- src/usecase/__init__.py | 36 +++-- .../creative/start_creative_creation.py | 33 ---- src/usecase/creative/tg_create_creative_1.py | 82 ++++++++++ src/usecase/creative/tg_create_creative_2.py | 72 +++++++++ ...ve_callback.py => tg_create_creative_3.py} | 37 ++++- ..._text_input.py => tg_create_creative_4.py} | 29 +++- .../target_channel/connect_target_chan.py | 74 --------- .../target_channel/tg_add_target_chan_1.py | 143 ++++++++++++++++++ .../target_channel/tg_add_target_chan_2.py | 93 ++++++++++++ src/usecase/workspace/delete_workspace.py | 2 +- 16 files changed, 526 insertions(+), 143 deletions(-) create mode 100644 src/controller/telegram_callback/target_channel_commands.py delete mode 100644 src/usecase/creative/start_creative_creation.py create mode 100644 src/usecase/creative/tg_create_creative_1.py create mode 100644 src/usecase/creative/tg_create_creative_2.py rename src/usecase/creative/{handle_creative_callback.py => tg_create_creative_3.py} (56%) rename src/usecase/creative/{handle_creative_text_input.py => tg_create_creative_4.py} (77%) delete mode 100644 src/usecase/target_channel/connect_target_chan.py create mode 100644 src/usecase/target_channel/tg_add_target_chan_1.py create mode 100644 src/usecase/target_channel/tg_add_target_chan_2.py diff --git a/src/controller/telegram_callback/__init__.py b/src/controller/telegram_callback/__init__.py index 70afe7e..2fe7ca8 100644 --- a/src/controller/telegram_callback/__init__.py +++ b/src/controller/telegram_callback/__init__.py @@ -9,4 +9,5 @@ from . import ( # noqa: E402, F401 creative_commands, my_chat_member, start_with_login, + target_channel_commands, ) diff --git a/src/controller/telegram_callback/creative_commands.py b/src/controller/telegram_callback/creative_commands.py index 0c352ec..3f91917 100644 --- a/src/controller/telegram_callback/creative_commands.py +++ b/src/controller/telegram_callback/creative_commands.py @@ -17,17 +17,34 @@ async def cmd_new_creative(message: Message) -> None: return usecase = deps.get_usecase() - await usecase.start_creative_creation(telegram_id=message.from_user.id, chat_id=message.chat.id) + await usecase.tg_create_creative_1(telegram_id=message.from_user.id, chat_id=message.chat.id) -@telegram_callback_router.callback_query(F.data.startswith('cc_ch:')) +@telegram_callback_router.callback_query(F.data.startswith('tg_create_creative_2:')) +async def callback_creative_workspace(callback: CallbackQuery) -> None: + if not callback.from_user or not callback.message or not callback.data: + log.error('Failed to get required data from callback query') + return + + usecase = deps.get_usecase() + await usecase.tg_create_creative_2( + telegram_id=callback.from_user.id, + chat_id=callback.message.chat.id, + callback_data=callback.data, + message_id=callback.message.message_id, + ) + + await callback.answer() + + +@telegram_callback_router.callback_query(F.data.startswith('tg_create_creative_3:')) async def callback_creative_channel(callback: CallbackQuery) -> None: if not callback.from_user or not callback.message or not callback.data: log.error('Failed to get required data from callback query') return usecase = deps.get_usecase() - await usecase.handle_creative_callback( + await usecase.tg_create_creative_3( telegram_id=callback.from_user.id, chat_id=callback.message.chat.id, callback_data=callback.data, @@ -43,7 +60,7 @@ async def handle_text_message(message: Message) -> None: return usecase = deps.get_usecase() - await usecase.handle_creative_text_input( + await usecase.tg_create_creative_4( telegram_id=message.from_user.id, chat_id=message.chat.id, text=message.text, diff --git a/src/controller/telegram_callback/my_chat_member.py b/src/controller/telegram_callback/my_chat_member.py index 96c36c2..a3189c6 100644 --- a/src/controller/telegram_callback/my_chat_member.py +++ b/src/controller/telegram_callback/my_chat_member.py @@ -79,6 +79,6 @@ async def on_bot_added_to_chat(event: ChatMemberUpdated) -> None: bot_permissions=bot_permissions, ) try: - await usecase.connect_target_chan(input=connect_input) + await usecase.tg_add_target_chan_1(input=connect_input) except HTTPException as e: log.error(e) diff --git a/src/controller/telegram_callback/target_channel_commands.py b/src/controller/telegram_callback/target_channel_commands.py new file mode 100644 index 0000000..11a9998 --- /dev/null +++ b/src/controller/telegram_callback/target_channel_commands.py @@ -0,0 +1,26 @@ +import logging + +from aiogram import F +from aiogram.types import CallbackQuery + +from src import deps +from src.controller.telegram_callback import telegram_callback_router + +log = logging.getLogger(__name__) + + +@telegram_callback_router.callback_query(F.data.startswith('tg_add_target_chan_2:')) +async def callback_target_channel_workspace(callback: CallbackQuery) -> None: + if not callback.from_user or not callback.message or not callback.data: + log.error('Failed to get required data from callback query') + return + + usecase = deps.get_usecase() + await usecase.tg_add_target_chan_2( + telegram_id=callback.from_user.id, + chat_id=callback.message.chat.id, + callback_data=callback.data, + message_id=callback.message.message_id, + ) + + await callback.answer() diff --git a/src/domain/telegram_state.py b/src/domain/telegram_state.py index 77f9df0..158bd92 100644 --- a/src/domain/telegram_state.py +++ b/src/domain/telegram_state.py @@ -6,9 +6,11 @@ from .base import TimestampedModel class TelegramStateEnum(str, enum.Enum): + CREATIVE_WAITING_WORKSPACE = 'creative_waiting_workspace' CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel' CREATIVE_WAITING_NAME = 'creative_waiting_name' CREATIVE_WAITING_TEXT = 'creative_waiting_text' + TARGET_CHANNEL_WAITING_WORKSPACE = 'target_channel_waiting_workspace' class TelegramState(TimestampedModel): diff --git a/src/dto/__init__.py b/src/dto/__init__.py index 50f4f8d..6730847 100644 --- a/src/dto/__init__.py +++ b/src/dto/__init__.py @@ -48,11 +48,6 @@ __all__ = ( 'SpendingDataPoint', 'GetSpendingAnalyticsInput', 'GetSpendingAnalyticsOutput', - 'ParserTrackTaskRequest', - 'ParserGetViewsRequest', - 'ParserPostFoundEvent', - 'ParserPostDeletedEvent', - 'ParserViewsUpdatedEvent', 'WorkspaceMembershipOutput', 'GetWorkspacesOutput', 'CreateWorkspaceInput', @@ -93,13 +88,6 @@ from .external_channel import ( UpdateExternalChannelInput, UpdateExternalChannelLinksInput, ) -from .nats_messages import ( - ParserGetViewsRequest, - ParserPostDeletedEvent, - ParserPostFoundEvent, - ParserTrackTaskRequest, - ParserViewsUpdatedEvent, -) from .placement import ( CreatePlacementInput, DeletePlacementInput, diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 884329e..a846acf 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -20,10 +20,10 @@ from .creative.create_creative import create_creative from .creative.delete_creative import delete_creative from .creative.get_creative import get_creative from .creative.get_creatives import get_creatives -from .creative.handle_creative_callback import handle_creative_callback -from .creative.handle_creative_text_input import handle_creative_text_input -from .creative.handle_creative_workspace_callback import handle_creative_workspace_callback -from .creative.start_creative_creation import start_creative_creation +from .creative.tg_create_creative_1 import tg_create_creative_1 +from .creative.tg_create_creative_2 import tg_create_creative_2 +from .creative.tg_create_creative_3 import tg_create_creative_3 +from .creative.tg_create_creative_4 import tg_create_creative_4 from .creative.update_creative import update_creative from .external_channel.create_external_channel import create_external_channel from .external_channel.delete_external_channel import delete_external_channel @@ -37,9 +37,10 @@ from .placement.get_placements import get_placements from .placement.update_placement import update_placement from .subscription.handle_subscription import handle_subscription from .subscription.handle_unsubscription import handle_unsubscription -from .target_channel.connect_target_chan import connect_target_chan from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_chan_by_tg_id from .target_channel.get_user_target_chans import get_user_target_chans +from .target_channel.tg_add_target_chan_1 import tg_add_target_chan_1 +from .target_channel.tg_add_target_chan_2 import tg_add_target_chan_2 from .target_channel.update_target_chan_permissions import update_target_chan_permissions from .views.get_views_history import get_views_history from .workspace.create_workspace import create_workspace @@ -214,11 +215,26 @@ class Usecase: return workspace + async def get_or_create_personal_workspace(self, user: domain.User) -> domain.Workspace: + workspace = await self.database.get_default_workspace_for_user(user.id) + if workspace: + return workspace + + workspace_name = user.username or f'Workspace {user.telegram_id}' + workspace = domain.Workspace(name=workspace_name) + + async with self.database.transaction(): + await self.database.create_workspace(workspace) + await self.database.add_user_to_workspace(workspace.id, user.id) + + return workspace + validate_login_token = validate_login_token telegram_login = telegram_login telegram_start = telegram_start get_me = get_me - connect_target_chan = connect_target_chan + tg_add_target_chan_1 = tg_add_target_chan_1 + tg_add_target_chan_2 = tg_add_target_chan_2 get_user_target_chans = get_user_target_chans disconnect_target_chan_by_tg_id = disconnect_target_chan_by_tg_id update_target_chan_permissions = update_target_chan_permissions @@ -232,10 +248,10 @@ class Usecase: create_creative = create_creative update_creative = update_creative delete_creative = delete_creative - start_creative_creation = start_creative_creation - handle_creative_callback = handle_creative_callback - handle_creative_workspace_callback = handle_creative_workspace_callback - handle_creative_text_input = handle_creative_text_input + tg_create_creative_1 = tg_create_creative_1 + tg_create_creative_2 = tg_create_creative_2 + tg_create_creative_3 = tg_create_creative_3 + tg_create_creative_4 = tg_create_creative_4 get_placements = get_placements get_placement = get_placement create_placement = create_placement diff --git a/src/usecase/creative/start_creative_creation.py b/src/usecase/creative/start_creative_creation.py deleted file mode 100644 index 4fb20e3..0000000 --- a/src/usecase/creative/start_creative_creation.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from aiogram.types import InlineKeyboardButton - -from src import domain - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def start_creative_creation(self: 'Usecase', telegram_id: int, chat_id: int) -> None: - user = await self.database.get_user(telegram_id=telegram_id) - if not user: - await self.telegram_bot.send_message('❌ Вы не авторизованы. Используйте /start для входа.', chat_id) - return - - channels = await self.database.get_user_target_channels(user.id) - if not channels: - await self.telegram_bot.send_message( - '❌ У вас нет подключенных каналов. Добавьте бота в свой канал для начала работы.', chat_id - ) - return - - buttons = [[InlineKeyboardButton(text=channel.title, callback_data=f'cc_ch:{channel.id}')] for channel in channels] - - await self.telegram_bot.send_message_with_inline_keyboard( - '✨ Создание нового креатива\n\nВыберите канал, для которого создается креатив:', chat_id, buttons - ) - - await self.database.set_telegram_state(telegram_id, domain.TelegramStateEnum.CREATIVE_WAITING_CHANNEL, context={}) diff --git a/src/usecase/creative/tg_create_creative_1.py b/src/usecase/creative/tg_create_creative_1.py new file mode 100644 index 0000000..89de253 --- /dev/null +++ b/src/usecase/creative/tg_create_creative_1.py @@ -0,0 +1,82 @@ +import logging +from typing import TYPE_CHECKING + +from aiogram.types import InlineKeyboardButton + +from src import domain + +if TYPE_CHECKING: + from .. import Usecase + +log = logging.getLogger(__name__) + + +async def tg_create_creative_1(self: 'Usecase', telegram_id: int, chat_id: int) -> None: + user = await self.database.get_user(telegram_id=telegram_id) + if not user: + await self.telegram_bot.send_message('❌ Вы не авторизованы. Используйте /start для входа.', chat_id) + return + + memberships = await self.database.get_user_workspaces(user.id) + if not memberships: + await self.telegram_bot.send_message( + '❌ У вас нет рабочих пространств. Создайте workspace в веб-панели, чтобы продолжить.', chat_id + ) + await self.database.clear_telegram_state(telegram_id) + return + + if len(memberships) == 1: + workspace: domain.Workspace | None = memberships[0].workspace + if not workspace: + workspace = await self.database.get_workspace(memberships[0].workspace_id) + + if not workspace: + log.error('Workspace %s not found for user %s', memberships[0].workspace_id, user.id) + await self.telegram_bot.send_message('❌ Рабочее пространство не найдено. Попробуйте позже.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + channels = await self.database.get_workspace_target_channels(workspace.id) + if not channels: + await self.telegram_bot.send_message( + '❌ В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.', + chat_id, + ) + await self.database.clear_telegram_state(telegram_id) + return + + buttons = [ + [InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')] + for channel in channels + ] + + await self.telegram_bot.send_message_with_inline_keyboard( + '✨ Создание нового креатива\n\nВыберите канал, для которого создается креатив:', chat_id, buttons + ) + + await self.database.set_telegram_state( + telegram_id, + domain.TelegramStateEnum.CREATIVE_WAITING_CHANNEL, + context={'workspace_id': str(workspace.id)}, + ) + return + + buttons = [ + [ + InlineKeyboardButton( + text=membership.workspace.name if membership.workspace else 'Workspace', + callback_data=f'tg_create_creative_2:{membership.workspace_id}', + ) + ] + for membership in memberships + ] + + await self.telegram_bot.send_message_with_inline_keyboard( + '✨ Создание нового креатива\n\nВыберите рабочее пространство:', chat_id, buttons + ) + + await self.database.set_telegram_state( + telegram_id, + domain.TelegramStateEnum.CREATIVE_WAITING_WORKSPACE, + context={}, + ) diff --git a/src/usecase/creative/tg_create_creative_2.py b/src/usecase/creative/tg_create_creative_2.py new file mode 100644 index 0000000..ae83b23 --- /dev/null +++ b/src/usecase/creative/tg_create_creative_2.py @@ -0,0 +1,72 @@ +import logging +import uuid +from typing import TYPE_CHECKING + +from aiogram.types import InlineKeyboardButton + +from src import domain + +if TYPE_CHECKING: + from .. import Usecase + +log = logging.getLogger(__name__) + + +async def tg_create_creative_2( + self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int +) -> None: + state = await self.database.get_telegram_state(telegram_id) + if not state or state.state != domain.TelegramStateEnum.CREATIVE_WAITING_WORKSPACE: + log.debug('User %s has no active creative workspace selection flow', telegram_id) + return + + if not callback_data.startswith('tg_create_creative_2:'): + log.warning('Invalid workspace callback data format: %s', callback_data) + return + + workspace_id_str = callback_data.split(':', 1)[1] + try: + workspace_id = uuid.UUID(workspace_id_str) + except ValueError: + log.error('Invalid workspace UUID in callback data: %s', workspace_id_str) + await self.telegram_bot.send_message('❌ Некорректное рабочее пространство.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + user = await self.database.get_user(telegram_id=telegram_id) + if not user: + await self.telegram_bot.send_message('Вы не зарегистрированы на платформе.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + workspace = await self.database.get_workspace_for_user(workspace_id, user.id) + if not workspace: + await self.telegram_bot.send_message('❌ Рабочее пространство недоступно.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + await self.telegram_bot.edit_message_reply_markup(chat_id, message_id) + + channels = await self.database.get_workspace_target_channels(workspace.id) + if not channels: + await self.telegram_bot.send_message( + '❌ В выбранном рабочем пространстве нет подключенных каналов. Добавьте канал и попробуйте снова.', + chat_id, + ) + await self.database.clear_telegram_state(telegram_id) + return + + buttons = [ + [InlineKeyboardButton(text=channel.title, callback_data=f'tg_create_creative_3:{channel.id}')] + for channel in channels + ] + + await self.telegram_bot.send_message_with_inline_keyboard( + '✨ Создание нового креатива\n\nВыберите канал, для которого создается креатив:', chat_id, buttons + ) + + await self.database.set_telegram_state( + telegram_id, + domain.TelegramStateEnum.CREATIVE_WAITING_CHANNEL, + context={'workspace_id': str(workspace.id)}, + ) diff --git a/src/usecase/creative/handle_creative_callback.py b/src/usecase/creative/tg_create_creative_3.py similarity index 56% rename from src/usecase/creative/handle_creative_callback.py rename to src/usecase/creative/tg_create_creative_3.py index 0928f0d..65e6f1c 100644 --- a/src/usecase/creative/handle_creative_callback.py +++ b/src/usecase/creative/tg_create_creative_3.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def handle_creative_callback( +async def tg_create_creative_3( self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int ) -> None: state = await self.database.get_telegram_state(telegram_id) @@ -18,7 +18,7 @@ async def handle_creative_callback( log.debug('User %s has no active creative creation flow or wrong state', telegram_id) return - if not callback_data.startswith('cc_ch:'): + if not callback_data.startswith('tg_create_creative_3:'): log.warning('Invalid callback data format: %s', callback_data) return @@ -31,11 +31,34 @@ async def handle_creative_callback( user = await self.database.get_user(telegram_id=telegram_id) if not user: - await self.telegram_bot.send_message('❌ Ошибка: пользователь не найден.', chat_id) + await self.telegram_bot.send_message('Вы не зарегистрированы на платформе.', chat_id) await self.database.clear_telegram_state(telegram_id) return - channel = await self.database.get_target_channel(user.id, channel_id=channel_id) + workspace = None + workspace_id = None + + workspace_id_str = state.context.get('workspace_id') if state.context else None + if workspace_id_str: + try: + workspace_id = uuid.UUID(workspace_id_str) + except ValueError: + workspace_id = None + + if workspace_id: + workspace = await self.database.get_workspace_for_user(workspace_id, user.id) + + if not workspace: + workspace = await self.get_or_create_personal_workspace(user) + workspace_id = workspace.id + + if not workspace_id: + log.error('Workspace ID missing for user %s when selecting channel %s', telegram_id, channel_id) + await self.telegram_bot.send_message('❌ Ошибка: рабочее пространство не найдено. Начните заново.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + channel = await self.database.get_target_channel(workspace_id, channel_id=channel_id) if not channel: await self.telegram_bot.send_message('❌ Канал не найден или не принадлежит вам.', chat_id) await self.database.clear_telegram_state(telegram_id) @@ -44,7 +67,11 @@ async def handle_creative_callback( await self.database.set_telegram_state( telegram_id, domain.TelegramStateEnum.CREATIVE_WAITING_NAME, - {'target_channel_id': str(channel_id), 'channel_title': channel.title}, + { + 'target_channel_id': str(channel_id), + 'channel_title': channel.title, + 'workspace_id': str(workspace_id), + }, ) await self.telegram_bot.edit_message_reply_markup(chat_id, message_id) diff --git a/src/usecase/creative/handle_creative_text_input.py b/src/usecase/creative/tg_create_creative_4.py similarity index 77% rename from src/usecase/creative/handle_creative_text_input.py rename to src/usecase/creative/tg_create_creative_4.py index e45802e..7b0cb85 100644 --- a/src/usecase/creative/handle_creative_text_input.py +++ b/src/usecase/creative/tg_create_creative_4.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) -async def handle_creative_text_input( +async def tg_create_creative_4( self: 'Usecase', telegram_id: int, chat_id: int, text: str, text_html: str | None = None ) -> None: state = await self.database.get_telegram_state(telegram_id) @@ -40,6 +40,7 @@ async def handle_creative_text_input( elif state.state == domain.TelegramStateEnum.CREATIVE_WAITING_TEXT: target_channel_id_str = state.context.get('target_channel_id') creative_name = state.context.get('creative_name') + workspace_id_str = state.context.get('workspace_id') if not target_channel_id_str or not creative_name: log.error('Missing context data for creative creation: %s', state.context) @@ -55,7 +56,29 @@ async def handle_creative_text_input( await self.database.clear_telegram_state(telegram_id) return - channel = await self.database.get_target_channel(user.id, channel_id=target_channel_id) + workspace = None + workspace_id = None + + if workspace_id_str: + try: + workspace_id = uuid.UUID(workspace_id_str) + except ValueError: + workspace_id = None + + if workspace_id: + workspace = await self.database.get_workspace_for_user(workspace_id, user.id) + + if not workspace: + workspace = await self.get_or_create_personal_workspace(user) + workspace_id = workspace.id + + if not workspace_id: + log.error('Workspace ID missing while finalizing creative for user %s', telegram_id) + await self.telegram_bot.send_message('❌ Ошибка: рабочее пространство не найдено. Начните заново.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + channel = await self.database.get_target_channel(workspace_id, channel_id=target_channel_id) if not channel: await self.telegram_bot.send_message('❌ Канал не найден.', chat_id) await self.database.clear_telegram_state(telegram_id) @@ -68,7 +91,7 @@ async def handle_creative_text_input( name=creative_name, text=creative_text, status=domain.CreativeStatus.ACTIVE, - user_id=user.id, + workspace_id=workspace_id, target_channel_id=target_channel_id, ) diff --git a/src/usecase/target_channel/connect_target_chan.py b/src/usecase/target_channel/connect_target_chan.py deleted file mode 100644 index 11c9a10..0000000 --- a/src/usecase/target_channel/connect_target_chan.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from src import domain, dto - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def connect_target_chan(self: 'Usecase', input: dto.ConnectTargetChanInput) -> dto.ConnectTargetChanOutput: - permissions = input.bot_permissions - - if not permissions.is_admin: - log.warning(f'Bot is not admin in channel {input.telegram_id}. Attempted by user {input.user_telegram_id}') - await self.telegram_bot.send_message( - f'⚠️ Бот был добавлен в канал "{input.title}", но не является админом.\n\n' - 'Пожалуйста, сделайте бота администратором канала.', - input.user_telegram_id, - ) - raise domain.ChannelNoAdminRights() - - missing_permissions = [] - if not permissions.can_invite_users: - missing_permissions.append('Создание инвайт-ссылок') - if not permissions.can_restrict_members: - missing_permissions.append('Управление пользователями (видеть вступления)') - - if missing_permissions: - log.warning( - f'Bot lacks required permissions in channel {input.telegram_id}: {missing_permissions}. ' - f'Attempted by user {input.user_telegram_id}' - ) - permissions_text = '\n'.join(f'• {p}' for p in missing_permissions) - await self.telegram_bot.send_message( - f'⚠️ Бот был добавлен в канал "{input.title}", но не имеет необходимых прав.\n\n' - f'Отсутствующие права:\n{permissions_text}\n\n' - 'Пожалуйста, предоставьте эти права боту в настройках канала.', - input.user_telegram_id, - ) - raise domain.ChannelNoAdminRights() - - user = await self.database.get_user(telegram_id=input.user_telegram_id) - if not user: - log.warning(f'User {input.user_telegram_id} not found when trying to connect channel {input.telegram_id}') - await self.telegram_bot.send_message( - f'⚠️ Канал "{input.title}" не может быть подключен.\n\n' - 'Вы должны сначала авторизоваться в веб-панели перед подключением каналов.', - input.user_telegram_id, - ) - raise domain.UserNotFound() - - channel = domain.TargetChannel( - telegram_id=input.telegram_id, - title=input.title, - username=input.username, - user_id=user.id, - status=domain.TargetChannelStatus.ACTIVE, - ) - - await self.database.upsert_target_channel(channel) - log.info(f'Channel {input.telegram_id} connected/updated successfully by user {input.user_telegram_id}') - - await self.telegram_bot.send_message(f'✅ Канал "{input.title}" успешно подключен!', input.user_telegram_id) - - return dto.ConnectTargetChanOutput( - id=channel.id, - telegram_id=channel.telegram_id, - title=channel.title, - username=channel.username, - status=channel.status, - is_active=channel.status == domain.TargetChannelStatus.ACTIVE, - ) diff --git a/src/usecase/target_channel/tg_add_target_chan_1.py b/src/usecase/target_channel/tg_add_target_chan_1.py new file mode 100644 index 0000000..6d7ad95 --- /dev/null +++ b/src/usecase/target_channel/tg_add_target_chan_1.py @@ -0,0 +1,143 @@ +import logging +from typing import TYPE_CHECKING + +from aiogram.types import InlineKeyboardButton + +from src import domain, dto + +if TYPE_CHECKING: + from .. import Usecase + +log = logging.getLogger(__name__) + + +async def tg_add_target_chan_1( + self: 'Usecase', input: dto.ConnectTargetChanInput +) -> dto.ConnectTargetChanOutput | None: + permissions = input.bot_permissions + + if not permissions.is_admin: + log.warning(f'Bot is not admin in channel {input.telegram_id}. Attempted by user {input.user_telegram_id}') + await self.telegram_bot.send_message( + f'⚠️ Бот был добавлен в канал "{input.title}", но не является админом.\n\n' + 'Пожалуйста, сделайте бота администратором канала.', + input.user_telegram_id, + ) + raise domain.ChannelNoAdminRights() + + missing_permissions = [] + if not permissions.can_invite_users: + missing_permissions.append('Создание инвайт-ссылок') + if not permissions.can_restrict_members: + missing_permissions.append('Управление пользователями (видеть вступления)') + + if missing_permissions: + log.warning( + f'Bot lacks required permissions in channel {input.telegram_id}: {missing_permissions}. ' + f'Attempted by user {input.user_telegram_id}' + ) + permissions_text = '\n'.join(f'• {p}' for p in missing_permissions) + await self.telegram_bot.send_message( + f'⚠️ Бот был добавлен в канал "{input.title}", но не имеет необходимых прав.\n\n' + f'Отсутствующие права:\n{permissions_text}\n\n' + 'Пожалуйста, предоставьте эти права боту в настройках канала.', + input.user_telegram_id, + ) + raise domain.ChannelNoAdminRights() + + user = await self.database.get_user(telegram_id=input.user_telegram_id) + if not user: + log.warning(f'User {input.user_telegram_id} not found when trying to connect channel {input.telegram_id}') + await self.telegram_bot.send_message( + f'⚠️ Канал "{input.title}" не может быть подключен.\n\n' + 'Вы должны сначала авторизоваться в веб-панели перед подключением каналов.', + input.user_telegram_id, + ) + raise domain.UserNotFound() + + memberships = await self.database.get_user_workspaces(user.id) + workspaces: list[domain.Workspace] = [] + for membership in memberships: + workspace: domain.Workspace | None = membership.workspace + if not workspace: + workspace = await self.database.get_workspace(membership.workspace_id) + + if workspace: + workspaces.append(workspace) + + if not workspaces: + workspace = await self.get_or_create_personal_workspace(user) + workspaces = [workspace] + + if len(workspaces) == 1: + workspace = workspaces[0] + channel = domain.TargetChannel( + telegram_id=input.telegram_id, + title=input.title, + username=input.username, + workspace_id=workspace.id, + status=domain.TargetChannelStatus.ACTIVE, + ) + + await self.database.upsert_target_channel(channel) + log.info( + 'Channel %s connected/updated successfully in workspace %s by user %s', + input.telegram_id, + workspace.id, + input.user_telegram_id, + ) + + await self.telegram_bot.send_message( + f'✅ Канал "{input.title}" добавлен в рабочее пространство "{workspace.name}".', input.user_telegram_id + ) + + return dto.ConnectTargetChanOutput( + id=channel.id, + telegram_id=channel.telegram_id, + title=channel.title, + username=channel.username, + status=channel.status, + is_active=channel.status == domain.TargetChannelStatus.ACTIVE, + ) + + current_state = await self.database.get_telegram_state(user.telegram_id) + if current_state and current_state.state == domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE: + await self.telegram_bot.send_message( + '⚠️ Сначала завершите выбор рабочего пространства для предыдущего канала.', user.telegram_id + ) + return None + + context: dict[str, object] = { + 'channel_data': { + 'telegram_id': input.telegram_id, + 'title': input.title, + 'username': input.username, + 'permissions': input.bot_permissions.model_dump(), + } + } + + if current_state: + context['previous_state'] = current_state.state.value + context['previous_context'] = dict(current_state.context or {}) + + await self.database.set_telegram_state( + user.telegram_id, + domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE, + context, + ) + + buttons = [ + [InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_target_chan_2:{workspace.id}')] + for workspace in workspaces + ] + + await self.telegram_bot.send_message_with_inline_keyboard( + ( + '✨ Подключение канала\n\n' + f'Выберите рабочее пространство, в которое добавить канал "{input.title}":' + ), + user.telegram_id, + buttons, + ) + + return None diff --git a/src/usecase/target_channel/tg_add_target_chan_2.py b/src/usecase/target_channel/tg_add_target_chan_2.py new file mode 100644 index 0000000..4d8d2c4 --- /dev/null +++ b/src/usecase/target_channel/tg_add_target_chan_2.py @@ -0,0 +1,93 @@ +import logging +import uuid +from typing import TYPE_CHECKING + +from src import domain + +if TYPE_CHECKING: + from .. import Usecase + +log = logging.getLogger(__name__) + + +async def tg_add_target_chan_2( + self: 'Usecase', telegram_id: int, chat_id: int, callback_data: str, message_id: int +) -> None: + state = await self.database.get_telegram_state(telegram_id) + if not state or state.state != domain.TelegramStateEnum.TARGET_CHANNEL_WAITING_WORKSPACE: + log.debug('User %s has no active target channel workspace selection flow', telegram_id) + return + + context = state.context or {} + previous_state_value = context.get('previous_state') + previous_context = context.get('previous_context') or {} + previous_state = None + if previous_state_value: + try: + previous_state = domain.TelegramStateEnum(previous_state_value) + except ValueError: + previous_state = None + + if not callback_data.startswith('tg_add_target_chan_2:'): + log.warning('Invalid target channel workspace callback data: %s', callback_data) + return + + workspace_id_str = callback_data.split(':', 1)[1] + try: + workspace_id = uuid.UUID(workspace_id_str) + except ValueError: + log.error('Invalid workspace UUID in callback data: %s', workspace_id_str) + await self.telegram_bot.send_message('❌ Некорректное рабочее пространство.', chat_id) + if previous_state: + await self.database.set_telegram_state(telegram_id, previous_state, previous_context) + else: + await self.database.clear_telegram_state(telegram_id) + return + + channel_data = state.context.get('channel_data') if state.context else None + if not channel_data: + log.error('Missing channel data for user %s in target channel flow', telegram_id) + await self.telegram_bot.send_message('❌ Не удалось подключить канал. Попробуйте снова.', chat_id) + if previous_state: + await self.database.set_telegram_state(telegram_id, previous_state, previous_context) + else: + await self.database.clear_telegram_state(telegram_id) + return + + user = await self.database.get_user(telegram_id=telegram_id) + if not user: + await self.telegram_bot.send_message('Вы не зарегистрированы на платформе.', chat_id) + await self.database.clear_telegram_state(telegram_id) + return + + workspace = await self.database.get_workspace_for_user(workspace_id, user.id) + if not workspace: + await self.telegram_bot.send_message('❌ У вас нет доступа к выбранному рабочему пространству.', chat_id) + return + + channel = domain.TargetChannel( + telegram_id=int(channel_data['telegram_id']), + title=str(channel_data['title']), + username=channel_data.get('username'), + workspace_id=workspace.id, + status=domain.TargetChannelStatus.ACTIVE, + ) + + await self.database.upsert_target_channel(channel) + log.info( + 'Channel %s connected/updated successfully in workspace %s by user %s', + channel.telegram_id, + workspace.id, + telegram_id, + ) + + await self.telegram_bot.send_message( + f'✅ Канал "{channel.title}" добавлен в рабочее пространство "{workspace.name}".', telegram_id + ) + + await self.telegram_bot.edit_message_reply_markup(chat_id, message_id) + + if previous_state: + await self.database.set_telegram_state(telegram_id, previous_state, previous_context) + else: + await self.database.clear_telegram_state(telegram_id) diff --git a/src/usecase/workspace/delete_workspace.py b/src/usecase/workspace/delete_workspace.py index d8eb95a..70892c5 100644 --- a/src/usecase/workspace/delete_workspace.py +++ b/src/usecase/workspace/delete_workspace.py @@ -6,6 +6,6 @@ if TYPE_CHECKING: async def delete_workspace(self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID) -> None: - self.ensure_workspace_access(workspace_id, user_id) + await self.ensure_workspace_access(workspace_id, user_id) await self.database.delete_workspace(workspace_id)