new telegram bot

This commit is contained in:
Artem Tsyrulnikov
2025-12-28 15:50:06 +03:00
parent 69179c64be
commit 2fb8c83041
75 changed files with 3719 additions and 1046 deletions

View File

@@ -0,0 +1,24 @@
from typing import TYPE_CHECKING
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
async def get_project(self: 'Usecase', input: dto.GetProjectInput) -> dto.ProjectOutput:
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
if not project:
raise domain.ProjectNotFound()
await project.fetch_related('channel')
return dto.ProjectOutput(
id=project.id,
telegram_id=project.channel.telegram_id,
title=project.channel.title,
username=project.channel.username,
status=project.status,
invite_link_type=project.invite_link_type,
)

View File

@@ -24,6 +24,7 @@ async def get_workspace_projects(
title=project.channel.title,
username=project.channel.username,
status=project.status,
invite_link_type=project.invite_link_type,
)
for project in projects
]

View File

@@ -11,7 +11,7 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> dto.ProjectOutput | None:
async def tg_add_project(self: 'Usecase', input: dto.ConnectProjectInput) -> dto.ProjectOutput | None:
permissions = input.bot_permissions
if not permissions.is_admin:
@@ -125,41 +125,40 @@ async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> d
status=project.status,
)
current_state = await self.database.get_telegram_state(user.telegram_id)
if current_state and current_state.state == domain.TelegramStateEnum.PROJECT_WAITING_WORKSPACE:
await self.telegram_bot.send_message(
'⚠️ Сначала завершите выбор рабочего пространства для предыдущего канала.', user.telegram_id
# Если >1 workspace - создаем/обновляем канал и отправляем уведомление
channel = await self.database.get_channel(telegram_id=input.telegram_id)
if channel:
channel.title = input.title
if input.username is not None:
channel.username = input.username
await self.database.update_channel(channel)
else:
if input.username is None:
log.warning('Cannot create channel %s without username', input.telegram_id)
await self.telegram_bot.send_message(
f'⚠️ Канал "{input.title}" не может быть подключен.\n\nУ канала отсутствует публичный username.',
input.user_telegram_id,
)
return None
channel = domain.Channel(
telegram_id=input.telegram_id,
title=input.title,
username=input.username,
)
return None
await self.database.create_channel(channel)
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.PROJECT_WAITING_WORKSPACE,
context,
)
buttons = [
[InlineKeyboardButton(text=workspace.name, callback_data=f'tg_add_project_2:{workspace.id}')]
for workspace in workspaces
]
# Callback будет обработан Golang ботом который покажет экран выбора workspace
buttons = [[InlineKeyboardButton(text='Выбрать workspace', callback_data=f'pending_channel:{channel.id}')]]
await self.telegram_bot.send_message_with_inline_keyboard(
(f'✨ Подключение канала\n\nВыберите рабочее пространство, в которое добавить канал "{input.title}":'),
f'Бот добавлен в канал "{input.title}".\n\n'
f'У вас {len(workspaces)} рабочих пространств. '
'Нажмите кнопку ниже чтобы выбрать, где создать проект.',
user.telegram_id,
buttons,
)
log.info('Pending channel notification sent to user %s for channel %s', user.telegram_id, channel.id)
return None

View File

@@ -1,108 +0,0 @@
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_project_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.PROJECT_WAITING_WORKSPACE:
log.debug('User %s has no active project 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_project_2:'):
log.warning('Invalid project 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 = context.get('channel_data')
if not channel_data:
log.error('Missing channel data for user %s in project 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
telegram_channel_id = int(channel_data['telegram_id'])
channel = await self.database.get_channel(telegram_id=telegram_channel_id)
if channel:
channel.title = str(channel_data['title'])
channel.username = channel_data.get('username')
await self.database.update_channel(channel)
else:
channel = domain.Channel(
telegram_id=telegram_channel_id,
title=str(channel_data['title']),
username=channel_data.get('username'),
)
await self.database.create_channel(channel)
project = await self.database.get_project(workspace.id, channel_id=channel.id)
if project:
project.status = domain.ProjectStatus.ACTIVE
await self.database.update_project(project)
else:
project = domain.Project(workspace_id=workspace.id, channel_id=channel.id, status=domain.ProjectStatus.ACTIVE)
await self.database.create_project(project)
await self.ensure_active_purchase_plan(project)
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)

View File

@@ -0,0 +1,42 @@
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 update_project_invite_link_type(
self: 'Usecase',
workspace_id: uuid.UUID,
project_id: uuid.UUID,
invite_link_type: domain.InviteLinkType,
user_id: uuid.UUID,
) -> domain.Project:
await self.ensure_workspace_permission(
workspace_id,
user_id,
domain.PermissionKey.PROJECTS_WRITE,
for_project_id=project_id,
)
async with self.database.transaction():
project = await self.database.get_project(workspace_id, project_id=project_id)
if not project:
raise domain.ProjectNotFound(project_id)
project.invite_link_type = invite_link_type
await project.save()
log.info(
'Project %s invite_link_type updated to %s by user %s',
project_id,
invite_link_type,
user_id,
)
return project