1586 lines
57 KiB
Python
1586 lines
57 KiB
Python
from __future__ import annotations
|
||
|
||
import re
|
||
from datetime import datetime
|
||
from urllib.parse import urlparse
|
||
from typing import Optional, Dict, List, Any, Union, cast, Literal, TypedDict, TypeAlias, Awaitable, Callable
|
||
|
||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, Message
|
||
from telegram.ext import ConversationHandler, ContextTypes
|
||
from telegram.constants import ParseMode
|
||
from sqlalchemy import select, delete, or_
|
||
|
||
from .states import BotStates
|
||
from app.models.templates import Template, TemplateVisibility, PostStatus
|
||
from app.models.post import PostType
|
||
from app.db.session import async_session_maker
|
||
|
||
async def choose_template_navigate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||
"""Обработчик навигации по списку шаблонов."""
|
||
query = update.callback_query
|
||
if not query or not query.data:
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
await query.answer()
|
||
data = query.data
|
||
|
||
if data == "back":
|
||
# Вернуться в предыдущее меню
|
||
kb = []
|
||
kb.append([InlineKeyboardButton(text="📝 Создать пост", callback_data="post:new")])
|
||
kb.append([InlineKeyboardButton(text="📋 Мои шаблоны", callback_data="tpl:list")])
|
||
kb.append([InlineKeyboardButton(text="🔧 Управление", callback_data="settings")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
await query.edit_message_text(
|
||
text="Выберите действие:",
|
||
reply_markup=reply_markup
|
||
)
|
||
return BotStates.MAIN_MENU
|
||
|
||
# Обработка выбора шаблона
|
||
template_parts = data.split(":")
|
||
if len(template_parts) == 3 and template_parts[0] == "tpl" and template_parts[1] == "select":
|
||
try:
|
||
template_id = int(template_parts[2])
|
||
async with async_session_maker() as session:
|
||
stmt = select(Template).where(Template.id == template_id)
|
||
template = (await session.execute(stmt)).scalar_one_or_none()
|
||
|
||
if template:
|
||
if context.user_data is None:
|
||
context.user_data = {}
|
||
|
||
context.user_data["current_template"] = {
|
||
"id": template.id,
|
||
"name": template.name,
|
||
"query": query
|
||
}
|
||
|
||
# Показать предпросмотр и действия
|
||
kb = []
|
||
kb.append([InlineKeyboardButton(text="✅ Использовать", callback_data="tpl:use")])
|
||
kb.append([InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
preview = f"Шаблон: {template.name}\n\n{template.content}"
|
||
|
||
await query.edit_message_text(
|
||
text=preview,
|
||
reply_markup=reply_markup
|
||
)
|
||
return BotStates.TEMPLATE_PREVIEW
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
async def choose_template_apply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||
"""Применить выбранный шаблон."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return BotStates.TEMPLATE_PREVIEW
|
||
|
||
await query.answer()
|
||
if context.user_data is None:
|
||
context.user_data = {}
|
||
template = context.user_data.get("current_template")
|
||
|
||
if not template:
|
||
await query.edit_message_text(
|
||
text="Ошибка: шаблон не выбран",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")
|
||
]])
|
||
)
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
# Подготовка переменных для шаблона
|
||
kb = []
|
||
kb.append([InlineKeyboardButton(text="✅ Подтвердить", callback_data="tpl:confirm")])
|
||
kb.append([InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:preview")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
text = "Укажите значения для переменных шаблона:"
|
||
|
||
await query.edit_message_text(text=text, reply_markup=reply_markup)
|
||
return BotStates.TEMPLATE_VARS
|
||
|
||
async def choose_channel_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||
"""Обработчик выбора канала."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
template = context.user_data.get("current_template") if context.user_data else None
|
||
|
||
if not template:
|
||
await query.edit_message_text(
|
||
text="Ошибка: шаблон не выбран",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")
|
||
]])
|
||
)
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
# Подготовка переменных для шаблона
|
||
kb = []
|
||
kb.append([InlineKeyboardButton(text="✅ Подтвердить", callback_data="tpl:confirm")])
|
||
kb.append([InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:preview")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
text = "Укажите значения для переменных шаблона:"
|
||
|
||
await query.edit_message_text(text=text, reply_markup=reply_markup)
|
||
return BotStates.TEMPLATE_VARS
|
||
|
||
async def choose_template_preview(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||
"""Предпросмотр шаблона."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return BotStates.TEMPLATE_PREVIEW
|
||
|
||
await query.answer()
|
||
if context.user_data is None:
|
||
context.user_data = {}
|
||
template = context.user_data.get("current_template")
|
||
|
||
if not template:
|
||
await query.edit_message_text(
|
||
text="Ошибка: шаблон не выбран",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")
|
||
]])
|
||
)
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
# Показать предпросмотр и действия
|
||
kb = []
|
||
kb.append([InlineKeyboardButton(text="✅ Использовать", callback_data="tpl:use")])
|
||
kb.append([InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
preview = f"Шаблон: {template['name']}\n\n{template['content']}"
|
||
|
||
await query.edit_message_text(
|
||
text=preview,
|
||
reply_markup=reply_markup
|
||
)
|
||
return BotStates.TEMPLATE_PREVIEW
|
||
|
||
async def choose_template_cancel(update: Update, context: CallbackContext) -> int:
|
||
"""Отмена выбора шаблона."""
|
||
query = update.callback_query
|
||
if query:
|
||
await query.answer()
|
||
|
||
if context.user_data is None:
|
||
context.user_data = {}
|
||
if "current_template" in context.user_data:
|
||
del context.user_data["current_template"]
|
||
|
||
await choose_template_open(update, context)
|
||
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
async def preview_collect_vars(update: Update, context: CallbackContext) -> int:
|
||
"""Сбор переменных для шаблона."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return BotStates.TEMPLATE_VARS
|
||
|
||
await query.answer()
|
||
# Здесь будет логика сбора переменных
|
||
return BotStates.TEMPLATE_VARS
|
||
|
||
async def preview_confirm(update: Update, context: CallbackContext) -> int:
|
||
"""Подтверждение использования шаблона."""
|
||
query = update.callback_query
|
||
if not query:
|
||
return BotStates.TEMPLATE_VARS
|
||
|
||
await query.answer()
|
||
if context.user_data is None:
|
||
context.user_data = {}
|
||
template = context.user_data.get("current_template")
|
||
|
||
if not template:
|
||
await query.edit_message_text(
|
||
text="Ошибка: шаблон не выбран",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton(text="🔙 Назад", callback_data="tpl:choose")
|
||
]])
|
||
)
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
# Применяем шаблон и возвращаемся к созданию поста
|
||
context.user_data["current_post"] = {
|
||
"type": template["type"],
|
||
"content": template["content"],
|
||
"keyboard": template["keyboard"]
|
||
}
|
||
|
||
await query.edit_message_text(
|
||
text="Шаблон применен. Выберите канал для публикации:",
|
||
reply_markup=InlineKeyboardMarkup([[
|
||
InlineKeyboardButton(text="Выбрать канал", callback_data="channel:select")
|
||
]])
|
||
)
|
||
return BotStates.CHANNEL_SELECT_BOT
|
||
|
||
async def choose_template_open(update: Update, context: CallbackContext) -> int:
|
||
"""Открыть список шаблонов для выбора."""
|
||
query = update.callback_query
|
||
if query:
|
||
await query.answer()
|
||
|
||
kb = []
|
||
user_data = context.user_data or {}
|
||
user_id = user_data.get("user_id")
|
||
|
||
async with async_session_maker() as session:
|
||
# Получаем все публичные шаблоны и приватные шаблоны пользователя
|
||
conditions = [Template.visibility == TemplateVisibility.public]
|
||
if user_id:
|
||
conditions.append(Template.owner_id == user_id)
|
||
|
||
stmt = select(Template).where(or_(*conditions))
|
||
templates = (await session.execute(stmt)).scalars().all()
|
||
|
||
for template in templates:
|
||
kb.append([
|
||
InlineKeyboardButton(
|
||
text=f"📝 {template.name}",
|
||
callback_data=f"tpl:select:{template.id}"
|
||
)
|
||
])
|
||
|
||
kb.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back")])
|
||
|
||
reply_markup = InlineKeyboardMarkup(kb)
|
||
text = "Выберите шаблон из списка:"
|
||
|
||
if query:
|
||
await query.edit_message_text(text=text, reply_markup=reply_markup)
|
||
elif update.message:
|
||
await update.message.reply_text(text=text, reply_markup=reply_markup)
|
||
|
||
return BotStates.SELECT_TEMPLATE
|
||
|
||
def validate_url(url: str | None) -> bool:
|
||
"""
|
||
Проверка корректности URL.
|
||
|
||
Args:
|
||
url: URL для проверки
|
||
|
||
Returns:
|
||
True если URL корректен, False в противном случае
|
||
"""
|
||
if not url:
|
||
return False
|
||
|
||
try:
|
||
result = urlparse(url)
|
||
return bool(result.scheme in ('http', 'https') and result.netloc)
|
||
except Exception:
|
||
return False
|
||
|
||
async def tpl_new_start(update: Update, context: CallbackContext) -> int:
|
||
"""Начало создания нового шаблона."""
|
||
message = update.effective_message
|
||
if not message:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await message.reply_text(
|
||
"📝 Создание нового шаблона\n\n"
|
||
"Введите название шаблона (максимум 100 символов):"
|
||
)
|
||
return BotStates.TPL_NEW_NAME
|
||
|
||
async def tpl_new_name(update: Update, context: CallbackContext) -> int:
|
||
"""Обработка ввода имени шаблона."""
|
||
message = update.effective_message
|
||
if not message or not message.text:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
name = message.text.strip()
|
||
if len(name) > 100:
|
||
await message.reply_text("❌ Слишком длинное название (максимум 100 символов)")
|
||
return BotStates.TPL_NEW_NAME
|
||
|
||
if not isinstance(context.user_data, dict):
|
||
context.user_data = {}
|
||
|
||
context.user_data["template_name"] = name
|
||
|
||
keyboard = [
|
||
[{"text": "📝 Текст", "callback_data": "type:text"}],
|
||
[{"text": "🖼 Фото", "callback_data": "type:photo"}],
|
||
[{"text": "🎥 Видео", "callback_data": "type:video"}],
|
||
[{"text": "🎭 Анимация", "callback_data": "type:animation"}]
|
||
]
|
||
markup = create_inline_keyboard(keyboard)
|
||
|
||
await message.reply_text(
|
||
"📋 Выберите тип шаблона:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.TPL_NEW_TYPE
|
||
|
||
async def tpl_new_type(update: Update, context: CallbackContext) -> int:
|
||
"""Обработка выбора типа шаблона."""
|
||
query = update.callback_query
|
||
if not query or not query.data or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await query.answer()
|
||
type_name = query.data.split(":", 1)[1]
|
||
|
||
context.user_data["template_type"] = type_name
|
||
|
||
keyboard = [
|
||
[{"text": "HTML", "callback_data": "format:html"}],
|
||
[{"text": "Markdown", "callback_data": "format:markdown"}],
|
||
[{"text": "Простой текст", "callback_data": "format:plain"}]
|
||
]
|
||
markup = create_inline_keyboard(keyboard)
|
||
|
||
await query.edit_message_text(
|
||
"📋 Выберите формат текста:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.TPL_NEW_FORMAT
|
||
|
||
async def tpl_new_format(update: Update, context: CallbackContext) -> int:
|
||
"""Обработка выбора формата текста."""
|
||
query = update.callback_query
|
||
if not query or not query.data or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await query.answer()
|
||
format_name = query.data.split(":", 1)[1]
|
||
|
||
context.user_data["template_format"] = format_name
|
||
|
||
await query.edit_message_text(
|
||
"📝 Введите содержимое шаблона\n\n"
|
||
"Используйте следующий синтаксис для переменных:\n"
|
||
"{{variable_name}}\n\n"
|
||
"Например: Здравствуйте, {{name}}!"
|
||
)
|
||
return BotStates.TPL_NEW_CONTENT
|
||
|
||
async def tpl_new_content(update: Update, context: CallbackContext) -> int:
|
||
"""Обработка ввода содержимого шаблона."""
|
||
message = update.effective_message
|
||
if not message or not message.text or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
context.user_data["template_content"] = message.text
|
||
|
||
await message.reply_text(
|
||
"⌨️ Введите разметку клавиатуры в формате:\n"
|
||
"Кнопка 1 -> http://example1.com\n"
|
||
"Кнопка 2 -> http://example2.com\n\n"
|
||
"Или /skip чтобы пропустить"
|
||
)
|
||
return BotStates.TPL_NEW_KB
|
||
|
||
async def tpl_new_kb(update: Update, context: CallbackContext) -> int:
|
||
"""Обработка ввода клавиатуры для шаблона."""
|
||
message = update.effective_message
|
||
if not message or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
if message.text == "/skip":
|
||
keyboard_tpl = None
|
||
else:
|
||
# Парсим разметку клавиатуры
|
||
lines = validate_text(message.text)
|
||
keyboard_tpl = []
|
||
row = []
|
||
|
||
try:
|
||
for line in lines:
|
||
if not line:
|
||
continue
|
||
|
||
parts = line.split("->", 1)
|
||
if len(parts) != 2:
|
||
raise ValueError("Неверный формат кнопки")
|
||
|
||
text = parts[0].strip()
|
||
url = parts[1].strip()
|
||
|
||
# Проверяем URL
|
||
if not validate_url(url):
|
||
raise ValueError(f"Недопустимый URL: {url}")
|
||
|
||
row.append({"text": text, "url": url})
|
||
|
||
if len(row) >= 2:
|
||
keyboard_tpl.append(row)
|
||
row = []
|
||
|
||
if row:
|
||
keyboard_tpl.append(row)
|
||
|
||
except ValueError as e:
|
||
await message.reply_text(f"❌ Ошибка в разметке клавиатуры: {str(e)}")
|
||
return BotStates.TPL_NEW_KB
|
||
|
||
# Сохраняем шаблон
|
||
try:
|
||
template = Template(
|
||
owner_id=message.from_user.id if message.from_user else 0,
|
||
name=context.user_data["template_name"],
|
||
title=context.user_data["template_name"],
|
||
content=context.user_data["template_content"],
|
||
type=context.user_data["template_type"],
|
||
parse_mode=context.user_data["template_format"].upper(),
|
||
keyboard_tpl=keyboard_tpl if keyboard_tpl else None
|
||
)
|
||
|
||
async with async_session_maker() as session:
|
||
session.add(template)
|
||
await session.commit()
|
||
|
||
await message.reply_text("✅ Шаблон успешно создан!")
|
||
|
||
except Exception as e:
|
||
await message.reply_text(f"❌ Ошибка при сохранении шаблона: {str(e)}")
|
||
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def tpl_list(update: Update, context: CallbackContext) -> int:
|
||
"""Просмотр списка шаблонов."""
|
||
message = update.effective_message
|
||
if not message:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
# Получаем шаблоны пользователя
|
||
user_id = message.from_user.id if message.from_user else 0
|
||
result = await session.execute(
|
||
select(Template).where(Template.owner_id == user_id)
|
||
)
|
||
templates = list(result.scalars())
|
||
|
||
if not templates:
|
||
await message.reply_text(
|
||
"📂 У вас пока нет шаблонов\n\n"
|
||
"Используйте /new_template чтобы создать"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Формируем список
|
||
text = "📂 Ваши шаблоны:\n\n"
|
||
for tpl in templates:
|
||
text += f"📑 {tpl.name} ({tpl.type})\n"
|
||
|
||
await message.reply_text(text)
|
||
|
||
except Exception as e:
|
||
await message.reply_text("❌ Ошибка при получении списка шаблонов")
|
||
|
||
return BotStates.CONVERSATION_END
|
||
|
||
from telegram import (
|
||
Update,
|
||
InlineKeyboardMarkup,
|
||
InlineKeyboardButton,
|
||
Message,
|
||
Chat,
|
||
)
|
||
from telegram.constants import MessageType, ParseMode, ChatType
|
||
from telegram.ext import CallbackContext, ConversationHandler
|
||
|
||
from sqlalchemy import select
|
||
from app.models.bot import Bot as BotModel
|
||
from app.models.channel import Channel as ChannelModel
|
||
from app.bots.editor.states import BotStates
|
||
|
||
# Message states and types
|
||
TYPE_PHOTO = "photo"
|
||
TYPE_VIDEO = "video"
|
||
TYPE_ANIMATION = "animation"
|
||
|
||
# Type definitions
|
||
ButtonData = Dict[str, str] # {'text': str, 'url': str} | {'text': str, 'callback_data': str}
|
||
UserData = Dict[str, Any] # Type alias for telegram context user_data
|
||
|
||
# Message type mappings
|
||
TYPE_MAP = {
|
||
TYPE_PHOTO: 'фотографию',
|
||
TYPE_VIDEO: 'видео',
|
||
TYPE_ANIMATION: 'анимацию'
|
||
}
|
||
|
||
def get_media_type_text(post_type: str | None) -> str:
|
||
"""Get human-readable media type description."""
|
||
if not post_type:
|
||
return 'медиафайл'
|
||
return TYPE_MAP.get(post_type, 'медиафайл')
|
||
|
||
def create_keyboard_button(text: str, url: str | None = None, callback_data: str | None = None) -> InlineKeyboardButton:
|
||
"""Create an InlineKeyboardButton with either url or callback_data."""
|
||
if url:
|
||
return InlineKeyboardButton(text=text, url=url)
|
||
return InlineKeyboardButton(text=text, callback_data=callback_data or text)
|
||
|
||
def create_inline_markup(buttons: list[list[ButtonData]]) -> InlineKeyboardMarkup:
|
||
"""
|
||
Create an inline keyboard from button data.
|
||
|
||
Args:
|
||
buttons: List of button rows, each containing dicts with 'text' and either 'url' or 'callback_data'
|
||
|
||
Returns:
|
||
Formatted InlineKeyboardMarkup ready to use in messages
|
||
"""
|
||
keyboard = []
|
||
for row in buttons:
|
||
kb_row = []
|
||
for btn in row:
|
||
text = btn.get('text', '')
|
||
url = btn.get('url')
|
||
callback_data = btn.get('callback_data', text)
|
||
kb_row.append(create_keyboard_button(text, url, callback_data))
|
||
if kb_row:
|
||
keyboard.append(kb_row)
|
||
|
||
return InlineKeyboardMarkup(keyboard)
|
||
|
||
# Message states and types
|
||
TYPE_PHOTO = "photo"
|
||
TYPE_VIDEO = "video"
|
||
TYPE_ANIMATION = "animation"
|
||
|
||
MediaType = Literal[TYPE_PHOTO, TYPE_VIDEO, TYPE_ANIMATION]
|
||
|
||
type_map: Dict[str, str] = {
|
||
TYPE_PHOTO: 'фотографию',
|
||
TYPE_VIDEO: 'видео',
|
||
TYPE_ANIMATION: 'анимацию'
|
||
}
|
||
|
||
# Helper functions
|
||
def extract_text(msg: Message | None) -> str | None:
|
||
"""Extract text from message."""
|
||
try:
|
||
return msg.text if msg and msg.text else None
|
||
except AttributeError:
|
||
return None
|
||
|
||
def validate_text(text: str | None) -> list[str]:
|
||
"""Validate and split text into lines, removing empty lines."""
|
||
if not text:
|
||
return []
|
||
|
||
try:
|
||
return [line for line in text.strip().split("\n") if line.strip()]
|
||
except (AttributeError, Exception):
|
||
return []
|
||
|
||
def validate_keyboard_data(data: Any) -> list[list[dict[str, str]]]:
|
||
"""
|
||
Validate and normalize keyboard data.
|
||
|
||
Args:
|
||
data: Raw keyboard data dictionary that should contain 'keyboard' key
|
||
with a list of button rows
|
||
|
||
Returns:
|
||
List of valid keyboard rows, where each row is a list of button dictionaries
|
||
with at least a 'text' key
|
||
"""
|
||
if not data or not isinstance(data, dict):
|
||
return []
|
||
|
||
keyboard = data.get('keyboard', [])
|
||
if not keyboard or not isinstance(keyboard, list):
|
||
return []
|
||
|
||
result = []
|
||
for row in keyboard:
|
||
if isinstance(row, list):
|
||
valid_row = []
|
||
for btn in row:
|
||
if isinstance(btn, dict) and 'text' in btn:
|
||
valid_row.append(btn)
|
||
if valid_row:
|
||
result.append(valid_row)
|
||
|
||
return result
|
||
|
||
def create_inline_keyboard(buttons: list[list[dict[str, str]]]) -> InlineKeyboardMarkup:
|
||
"""
|
||
Create an inline keyboard from validated button data.
|
||
|
||
Args:
|
||
buttons: List of button rows, each containing button data dictionaries
|
||
|
||
Returns:
|
||
InlineKeyboardMarkup object ready to be sent with a message
|
||
"""
|
||
keyboard = []
|
||
for row in buttons:
|
||
kb_row = []
|
||
for btn in row:
|
||
text = btn.get('text', '')
|
||
callback_data = btn.get('callback_data', text)
|
||
url = btn.get('url')
|
||
if url:
|
||
kb_row.append(InlineKeyboardButton(text=text, url=url))
|
||
else:
|
||
kb_row.append(InlineKeyboardButton(text=text, callback_data=callback_data))
|
||
if kb_row:
|
||
keyboard.append(kb_row)
|
||
|
||
return InlineKeyboardMarkup(keyboard)
|
||
|
||
def extract_chat_info(msg: Message | None) -> tuple[int | None, str | None]:
|
||
"""Extract chat info from message."""
|
||
try:
|
||
chat = msg.chat if msg else None
|
||
if chat and chat.type == ChatType.CHANNEL.value:
|
||
return chat.id, chat.title
|
||
except AttributeError:
|
||
pass
|
||
return None, None
|
||
|
||
def is_valid_url(text: str | None) -> bool:
|
||
"""Check if text is a valid URL."""
|
||
try:
|
||
if not text:
|
||
return False
|
||
result = urlparse(text.strip())
|
||
return bool(result.scheme and result.netloc)
|
||
except (AttributeError, Exception):
|
||
return False
|
||
|
||
# User data and keyboard types
|
||
UserData = Dict[str, Any]
|
||
KeyboardType = List[List[Dict[str, str]]]
|
||
|
||
def get_message_media_type(msg: Message) -> str | None:
|
||
"""Get media type from message."""
|
||
if not msg:
|
||
return None
|
||
|
||
# Check message contents
|
||
if msg.photo:
|
||
return TYPE_PHOTO
|
||
elif msg.video:
|
||
return TYPE_VIDEO
|
||
elif msg.animation:
|
||
return TYPE_ANIMATION
|
||
return None
|
||
|
||
def get_message_text(msg: Message) -> str | None:
|
||
"""Get text content from message."""
|
||
return msg.text if msg and msg.text else None
|
||
|
||
def get_message_media_text(msg: Message) -> str:
|
||
"""Get human-readable media type description."""
|
||
media_type = get_message_media_type(msg)
|
||
if not media_type:
|
||
return 'медиафайл'
|
||
return type_map.get(media_type, 'медиафайл')
|
||
|
||
def get_media_type_str(msg: Message) -> str:
|
||
"""Get media type as string."""
|
||
media_type = get_message_media_type(msg)
|
||
if not media_type:
|
||
return 'медиафайл'
|
||
return type_map.get(media_type, 'медиафайл')
|
||
|
||
def get_post_type_text(post_type: str | None) -> str:
|
||
"""Get human-readable text for media type."""
|
||
if not post_type:
|
||
return 'медиафайл'
|
||
return type_map.get(str(post_type), 'медиафайл')
|
||
|
||
def check_media_type(msg: Message) -> str | None:
|
||
"""Check message media type."""
|
||
if not msg:
|
||
return None
|
||
|
||
if msg.photo:
|
||
return TYPE_PHOTO
|
||
elif msg.video:
|
||
return TYPE_VIDEO
|
||
elif msg.animation:
|
||
return TYPE_ANIMATION
|
||
return None
|
||
|
||
def get_channel_info(msg: Message) -> tuple[int | None, str | None]:
|
||
"""Extract channel info from message."""
|
||
if not msg or not msg.chat:
|
||
return None, None
|
||
|
||
chat = msg.chat
|
||
if not isinstance(chat, Chat) or chat.type != ChatType.CHANNEL.value:
|
||
return None, None
|
||
|
||
return chat.id, chat.title
|
||
|
||
def process_message_text(text: str | None) -> list[str]:
|
||
"""Process and split message text into lines."""
|
||
if not text:
|
||
return []
|
||
|
||
try:
|
||
return [line for line in text.strip().split("\n") if line]
|
||
except AttributeError:
|
||
return []
|
||
|
||
def process_keyboard_data(data: Any) -> list[list[dict[str, str]]]:
|
||
"""Process and validate keyboard data."""
|
||
if not data or not isinstance(data, dict):
|
||
return []
|
||
|
||
keyboard = data.get('keyboard', [])
|
||
if not isinstance(keyboard, list):
|
||
return []
|
||
|
||
return [
|
||
[btn for btn in row if isinstance(btn, dict)]
|
||
for row in keyboard
|
||
if isinstance(row, list)
|
||
]
|
||
|
||
def count_keyboard_buttons(data: Any) -> int:
|
||
"""Count total buttons in keyboard data."""
|
||
keyboard = process_keyboard_data(data)
|
||
return sum(len(row) for row in keyboard)
|
||
|
||
def extract_message_text(msg: Message) -> str | None:
|
||
"""Extract text content from message."""
|
||
return msg.text if msg and hasattr(msg, 'text') else None
|
||
|
||
def check_url(text: str) -> bool:
|
||
"""Check if text is a valid URL."""
|
||
if not text:
|
||
return False
|
||
|
||
try:
|
||
result = urlparse(text)
|
||
return bool(result.scheme and result.netloc)
|
||
except Exception:
|
||
return False
|
||
|
||
from app.models.bot import Bot as BotModel
|
||
from app.models.channel import Channel as ChannelModel
|
||
from app.bots.editor.states import BotStates
|
||
|
||
|
||
# Type hints
|
||
PostType = Union[MessageType.PHOTO, MessageType.VIDEO, MessageType.ANIMATION, str, None]
|
||
UserData = Dict[str, Any]
|
||
|
||
type_map = {
|
||
MessageType.PHOTO: 'фотографию',
|
||
MessageType.VIDEO: 'видео',
|
||
MessageType.ANIMATION: 'анимацию'
|
||
}
|
||
|
||
from app.db.session import async_session_maker
|
||
from app.models.bot import Bot as BotModel
|
||
from app.models.channel import Channel as ChannelModel
|
||
from app.services.telegram import validate_bot_token
|
||
|
||
from ..states.base import BotStates
|
||
from .messages import Messages
|
||
from .session import UserSession
|
||
|
||
|
||
async def start(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработчик команды /start. Показывает приветственное сообщение.
|
||
"""
|
||
message = update.effective_message
|
||
if not message:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await message.reply_text(Messages.WELCOME_MESSAGE)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def help_command(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработчик команды /help. Показывает справочное сообщение.
|
||
"""
|
||
message = update.effective_message
|
||
if not message:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await message.reply_text(Messages.HELP_MESSAGE)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def newpost(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Начало создания нового поста.
|
||
Команда: /newpost
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not user:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
# Получаем список каналов пользователя
|
||
async with async_session_maker() as session:
|
||
result = await session.execute(
|
||
select(ChannelModel).where(ChannelModel.owner_id == user.id)
|
||
)
|
||
channels = list(result.scalars())
|
||
|
||
if not channels:
|
||
await message.reply_text(
|
||
"❌ У вас нет добавленных каналов.\n"
|
||
"Используйте /add_channel чтобы добавить канал."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Создаем клавиатуру выбора канала
|
||
keyboard = []
|
||
for channel in channels:
|
||
if channel.bot:
|
||
title = f"{channel.title or channel.chat_id} (@{channel.bot.username})"
|
||
else:
|
||
title = str(channel.chat_id)
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
title,
|
||
callback_data=f"channel:{channel.id}"
|
||
)
|
||
])
|
||
markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await message.reply_text(
|
||
"📢 Выберите канал для публикации:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.CHOOSE_CHANNEL
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка при получении списка каналов."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def choose_channel(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка выбора канала.
|
||
"""
|
||
query = update.callback_query
|
||
if not query or not query.data:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
await query.answer()
|
||
channel_id = int(query.data.split(":", 1)[1])
|
||
|
||
# Сохраняем выбранный канал
|
||
if isinstance(context.user_data, dict):
|
||
context.user_data["channel_id"] = channel_id
|
||
|
||
# Показываем выбор типа поста
|
||
keyboard = [
|
||
[InlineKeyboardButton("📝 Текст", callback_data="type:text")],
|
||
[InlineKeyboardButton("🖼️ Фото", callback_data="type:photo")],
|
||
[InlineKeyboardButton("🎥 Видео", callback_data="type:video")],
|
||
[InlineKeyboardButton("📹 Анимация", callback_data="type:animation")],
|
||
]
|
||
markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await query.edit_message_text(
|
||
"📝 Выберите тип поста:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.CHOOSE_TYPE
|
||
|
||
except ValueError:
|
||
await query.edit_message_text("❌ Неверный формат данных")
|
||
return BotStates.CONVERSATION_END
|
||
except Exception as e:
|
||
await query.edit_message_text("❌ Произошла ошибка.")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def choose_type(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка выбора типа поста.
|
||
"""
|
||
query = update.callback_query
|
||
if not query or not query.data or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
await query.answer()
|
||
post_type = query.data.split(":", 1)[1]
|
||
context.user_data["post_type"] = post_type
|
||
|
||
# Спрашиваем формат текста
|
||
keyboard = [
|
||
[InlineKeyboardButton("📄 Обычный текст", callback_data="fmt:plain")],
|
||
[InlineKeyboardButton("🔠 Markdown", callback_data="fmt:markdown")],
|
||
[InlineKeyboardButton("🌐 HTML", callback_data="fmt:html")],
|
||
]
|
||
markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await query.edit_message_text(
|
||
"📝 Выберите формат текста:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.CHOOSE_FORMAT
|
||
|
||
except ValueError:
|
||
await query.edit_message_text("❌ Неверный формат данных")
|
||
return BotStates.CONVERSATION_END
|
||
except Exception as e:
|
||
await query.edit_message_text("❌ Произошла ошибка.")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def choose_format(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка выбора формата текста.
|
||
"""
|
||
query = update.callback_query
|
||
if not query or not query.data or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
await query.answer()
|
||
text_format = query.data.split(":", 1)[1]
|
||
context.user_data["text_format"] = text_format
|
||
|
||
keyboard = [
|
||
[InlineKeyboardButton("📋 Из шаблона", callback_data="tpl:choose")]
|
||
]
|
||
markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await query.edit_message_text(
|
||
"📝 Отправьте текст поста\n"
|
||
"Или выберите шаблон:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.ENTER_TEXT
|
||
|
||
except ValueError:
|
||
await query.edit_message_text("❌ Неверный формат данных")
|
||
return BotStates.CONVERSATION_END
|
||
except Exception as e:
|
||
await query.edit_message_text("❌ Произошла ошибка.")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def enter_text(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка ввода текста поста.
|
||
"""
|
||
message = update.effective_message
|
||
if not message or not message.text or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
text = message.text
|
||
post_type = context.user_data.get("post_type")
|
||
|
||
if not post_type:
|
||
await message.reply_text("❌ Ошибка: тип поста не выбран")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
context.user_data["text"] = text
|
||
|
||
if post_type == "text":
|
||
# Для текстового поста сразу переходим к клавиатуре
|
||
await message.reply_text(
|
||
"⌨️ Отправьте разметку клавиатуры в формате:\n"
|
||
"Кнопка 1 -> http://example1.com\n"
|
||
"Кнопка 2 -> http://example2.com\n\n"
|
||
"Или /skip чтобы пропустить"
|
||
)
|
||
return BotStates.EDIT_KEYBOARD
|
||
else:
|
||
# Для медиа постов запрашиваем файл
|
||
msg = {
|
||
"photo": "🖼️ Отправьте фотографию",
|
||
"video": "🎥 Отправьте видео",
|
||
"animation": "📹 Отправьте анимацию"
|
||
}
|
||
await message.reply_text(msg.get(post_type, "❌ Неизвестный тип поста"))
|
||
return BotStates.ENTER_MEDIA
|
||
|
||
|
||
async def add_bot_start(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Начало процесса добавления нового бота.
|
||
Команда: /add_bot
|
||
"""
|
||
message = update.effective_message
|
||
if not message:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
await message.reply_text(
|
||
"🤖 Отправьте токен бота, полученный от @BotFather\n"
|
||
"Или /cancel для отмены"
|
||
)
|
||
return BotStates.BOT_TOKEN
|
||
|
||
async def add_bot_token(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка токена бота.
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not message.text or not user:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
token = message.text.strip()
|
||
|
||
# Валидация токена
|
||
is_valid, username, bot_id = await validate_bot_token(token)
|
||
if not is_valid or not username or not bot_id:
|
||
await message.reply_text(
|
||
"❌ Неверный токен бота. Проверьте токен и попробуйте снова.\n"
|
||
"Или /cancel для отмены"
|
||
)
|
||
return BotStates.BOT_TOKEN
|
||
|
||
try:
|
||
# Проверяем, не добавлен ли уже этот бот
|
||
async with async_session_maker() as session:
|
||
existing = await session.execute(
|
||
select(BotModel).where(BotModel.bot_id == bot_id)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
await message.reply_text(
|
||
"❌ Этот бот уже добавлен в систему."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Сохраняем бота
|
||
new_bot = BotModel(
|
||
owner_id=user.id,
|
||
bot_id=bot_id,
|
||
username=username,
|
||
token=token,
|
||
)
|
||
session.add(new_bot)
|
||
await session.commit()
|
||
|
||
await message.reply_text(
|
||
f"✅ Бот @{username} успешно добавлен!\n\n"
|
||
"Теперь вы можете:\n"
|
||
"1. Добавить каналы через /add_channel\n"
|
||
"2. Создать шаблоны через /tpl_new\n"
|
||
"3. Начать создание поста через /newpost"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка при добавлении бота. Попробуйте позже."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def list_bots(update: Update, context: CallbackContext) -> None:
|
||
"""
|
||
Список ботов пользователя.
|
||
Команда: /bots
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not user:
|
||
return
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
result = await session.execute(
|
||
select(BotModel).where(BotModel.owner_id == user.id)
|
||
)
|
||
bots = list(result.scalars())
|
||
|
||
if not bots:
|
||
await message.reply_text(
|
||
"🤖 У вас пока нет добавленных ботов.\n"
|
||
"Используйте /add_bot чтобы добавить бота."
|
||
)
|
||
return
|
||
|
||
text = "🤖 Ваши боты:\n\n"
|
||
for bot in bots:
|
||
text += f"@{bot.username}\n"
|
||
|
||
await message.reply_text(text)
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка при получении списка ботов."
|
||
)
|
||
|
||
|
||
async def enter_media(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка загрузки медиафайла.
|
||
|
||
Args:
|
||
update: Telegram update
|
||
context: Контекст с данными пользователя
|
||
|
||
Returns:
|
||
Следующее состояние разговора
|
||
"""
|
||
message = update.effective_message
|
||
if not message or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Проверяем тип медиа
|
||
post_type = context.user_data.get("post_type")
|
||
file_id = None
|
||
|
||
if post_type == TYPE_PHOTO and message.photo:
|
||
# Берем самую большую версию фото
|
||
file_id = message.photo[-1].file_id
|
||
elif post_type == TYPE_VIDEO and message.video:
|
||
file_id = message.video.file_id
|
||
elif post_type == TYPE_ANIMATION and message.animation:
|
||
file_id = message.animation.file_id
|
||
|
||
if not file_id:
|
||
media_type = get_media_type_text(post_type)
|
||
await message.reply_text(
|
||
f"❌ Пожалуйста, отправьте {media_type}"
|
||
)
|
||
return BotStates.ENTER_MEDIA
|
||
|
||
# Сохраняем файл
|
||
context.user_data["media_file_id"] = file_id
|
||
|
||
# Переходим к клавиатуре
|
||
await message.reply_text(
|
||
"⌨️ Отправьте разметку клавиатуры в формате:\n"
|
||
"Кнопка 1 -> http://example1.com\n"
|
||
"Кнопка 2 -> http://example2.com\n\n"
|
||
"Или /skip чтобы пропустить"
|
||
)
|
||
return BotStates.EDIT_KEYBOARD
|
||
|
||
|
||
async def edit_keyboard(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка разметки клавиатуры.
|
||
|
||
Args:
|
||
update: Telegram update объект
|
||
context: Контекст обработчика с user_data типа UserData
|
||
|
||
Returns:
|
||
Следующее состояние разговора
|
||
"""
|
||
message = update.effective_message
|
||
if not message or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
text = message.text
|
||
if not text:
|
||
await message.reply_text("❌ Отправьте текст разметки клавиатуры или /skip для пропуска")
|
||
return BotStates.EDIT_KEYBOARD
|
||
|
||
# Пропускаем клавиатуру
|
||
if text == "/skip":
|
||
context.user_data["keyboard"] = None
|
||
else:
|
||
# Парсим разметку
|
||
keyboard: list[list[ButtonData]] = []
|
||
row: list[ButtonData] = []
|
||
try:
|
||
# Обрабатываем каждую строку разметки
|
||
for line in text.strip().split("\n"):
|
||
if not line:
|
||
continue
|
||
|
||
# Разбираем строку на текст и URL
|
||
parts = line.split("->", 1)
|
||
if len(parts) != 2:
|
||
raise ValueError("Неверный формат кнопки. Используйте: Текст -> URL")
|
||
|
||
label = parts[0].strip()
|
||
url = parts[1].strip()
|
||
|
||
# Проверяем URL
|
||
if not validate_url(url):
|
||
raise ValueError(f"Недопустимый URL: {url}")
|
||
|
||
# Проверяем длину текста кнопки
|
||
if len(label) > 64:
|
||
raise ValueError(f"Текст кнопки слишком длинный (макс. 64 символа): {label}")
|
||
|
||
# Добавляем кнопку в текущий ряд
|
||
row.append({"text": label, "url": url})
|
||
|
||
# Переход на новую строку после 2 кнопок
|
||
if len(row) >= 2: # Максимум 2 кнопки в ряду
|
||
keyboard.append(row)
|
||
row = []
|
||
|
||
# Добавляем оставшиеся кнопки
|
||
if row:
|
||
keyboard.append(row)
|
||
|
||
# Проверяем общее количество кнопок
|
||
total_buttons = sum(len(row) for row in keyboard)
|
||
if total_buttons > 10:
|
||
raise ValueError("Слишком много кнопок (максимум 10)")
|
||
|
||
context.user_data["keyboard"] = keyboard
|
||
|
||
except ValueError as e:
|
||
await message.reply_text(f"❌ Ошибка в разметке клавиатуры: {str(e)}")
|
||
return BotStates.EDIT_KEYBOARD
|
||
|
||
# Готовим предпросмотр
|
||
if not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
preview_text = context.user_data.get("text", "")
|
||
post_type = context.user_data.get("post_type")
|
||
|
||
# Обрезаем длинный текст
|
||
if len(preview_text) > 50:
|
||
preview_text = preview_text[:47] + "..."
|
||
|
||
# Создаем клавиатуру выбора действия
|
||
keyboard_data = [
|
||
[{"text": "✅ Отправить сейчас", "callback_data": "send:now"}],
|
||
[{"text": "⏰ Отложить", "callback_data": "send:schedule"}]
|
||
]
|
||
markup = create_inline_keyboard(keyboard_data)
|
||
|
||
# Информация о медиафайле
|
||
media_info = ""
|
||
if post_type:
|
||
media_info = f"\n📎 Прикреплен файл: {get_media_type_text(post_type)}"
|
||
|
||
# Информация о клавиатуре
|
||
kb_info = ""
|
||
keyboard = context.user_data.get("keyboard", [])
|
||
if keyboard and isinstance(keyboard, list):
|
||
try:
|
||
kb_count = sum(len(row) for row in keyboard)
|
||
kb_info = f"\n⌨️ Клавиатура: {kb_count} кнопок"
|
||
except (TypeError, AttributeError):
|
||
pass
|
||
|
||
await message.reply_text(
|
||
f"📝 Предпросмотр:\n\n{preview_text}\n{media_info}{kb_info}\n\n"
|
||
"Выберите действие:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.CONFIRM_SEND
|
||
|
||
|
||
async def confirm_send(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка подтверждения отправки.
|
||
"""
|
||
query = update.callback_query
|
||
if not query or not query.data or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
await query.answer()
|
||
action = query.data.split(":", 1)[1]
|
||
|
||
if action == "now":
|
||
# TODO: Отправка поста
|
||
await query.edit_message_text(
|
||
"✅ Пост успешно отправлен!"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
elif action == "schedule":
|
||
await query.edit_message_text(
|
||
"⏰ Отправьте дату и время публикации в формате:\n"
|
||
"ДД.ММ.ГГГГ ЧЧ:ММ\n"
|
||
"Например: 31.12.2025 23:59"
|
||
)
|
||
return BotStates.ENTER_SCHEDULE
|
||
|
||
else:
|
||
await query.edit_message_text("❌ Неизвестное действие")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
except (ValueError, AttributeError):
|
||
await query.edit_message_text("❌ Неверный формат данных")
|
||
return BotStates.CONVERSATION_END
|
||
except Exception:
|
||
await query.edit_message_text("❌ Произошла ошибка")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
|
||
async def enter_schedule(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка времени отложенной публикации.
|
||
|
||
Args:
|
||
update: Telegram update
|
||
context: Контекст с данными пользователя
|
||
|
||
Returns:
|
||
Следующее состояние разговора
|
||
"""
|
||
message = update.effective_message
|
||
if not message or not message.text:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
# Парсим дату и время
|
||
text = message.text.strip()
|
||
dt = datetime.strptime(text, "%d.%m.%Y %H:%M")
|
||
now = datetime.now()
|
||
|
||
if dt <= now:
|
||
await message.reply_text(
|
||
"❌ Дата публикации должна быть в будущем"
|
||
)
|
||
return BotStates.ENTER_SCHEDULE
|
||
|
||
if isinstance(context.user_data, dict):
|
||
context.user_data["schedule"] = dt.isoformat()
|
||
|
||
# TODO: Сохранение отложенного поста
|
||
|
||
await message.reply_text(
|
||
f"✅ Пост запланирован на {dt.strftime('%d.%m.%Y %H:%M')}"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
except ValueError:
|
||
await message.reply_text(
|
||
"❌ Неверный формат даты и времени.\n"
|
||
"Используйте формат ДД.ММ.ГГГГ ЧЧ:ММ"
|
||
)
|
||
return BotStates.ENTER_SCHEDULE
|
||
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def add_channel_start(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Начало процесса добавления канала.
|
||
Команда: /add_channel
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not user:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
# Получаем список ботов пользователя
|
||
async with async_session_maker() as session:
|
||
result = await session.execute(
|
||
select(BotModel).where(BotModel.owner_id == user.id)
|
||
)
|
||
bots = list(result.scalars())
|
||
|
||
if not bots:
|
||
await message.reply_text(
|
||
"❌ Сначала добавьте бота через /add_bot"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Создаем клавиатуру выбора бота
|
||
keyboard = []
|
||
for bot in bots:
|
||
keyboard.append([
|
||
InlineKeyboardButton(
|
||
f"@{bot.username}",
|
||
callback_data=f"addch_bot:{bot.id}"
|
||
)
|
||
])
|
||
markup = InlineKeyboardMarkup(keyboard)
|
||
|
||
await message.reply_text(
|
||
"🤖 Выберите бота для добавления канала:",
|
||
reply_markup=markup
|
||
)
|
||
return BotStates.CHANNEL_SELECT_BOT
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка. Попробуйте позже."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def add_channel_bot_selected(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка выбора бота для канала.
|
||
"""
|
||
query = update.callback_query
|
||
user = update.effective_user
|
||
if not query or not query.data or not user:
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
await query.answer()
|
||
bot_id = int(query.data.split(":", 1)[1])
|
||
|
||
# Сохраняем выбранного бота в контекст
|
||
if isinstance(context.user_data, dict):
|
||
context.user_data["selected_bot_id"] = bot_id
|
||
|
||
await query.edit_message_text(
|
||
"📢 Добавьте бота в канал как администратора и отправьте:\n\n"
|
||
"1. Username канала (например @channel)\n"
|
||
"2. Или ID канала (например -100...)\n"
|
||
"3. Или перешлите любое сообщение из канала\n\n"
|
||
"Или /cancel для отмены"
|
||
)
|
||
return BotStates.CHANNEL_INPUT
|
||
|
||
except ValueError:
|
||
await query.edit_message_text("❌ Неверный формат данных")
|
||
return BotStates.CONVERSATION_END
|
||
except Exception as e:
|
||
await query.edit_message_text("❌ Произошла ошибка. Попробуйте позже.")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def add_channel_input(update: Update, context: CallbackContext) -> int:
|
||
"""
|
||
Обработка ввода канала.
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not user or not isinstance(context.user_data, dict):
|
||
return BotStates.CONVERSATION_END
|
||
|
||
bot_id = context.user_data.get("selected_bot_id")
|
||
if not bot_id:
|
||
await message.reply_text("❌ Ошибка: не выбран бот")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
try:
|
||
# Определяем ID канала
|
||
channel_id: Optional[int] = None
|
||
|
||
# Проверяем информацию о канале
|
||
channel_id, channel_title = extract_chat_info(message)
|
||
|
||
# Если не нашли, пробуем из текста
|
||
if not channel_id:
|
||
text = extract_text(message)
|
||
if text:
|
||
if text.startswith('@'):
|
||
# TODO: Получить chat_id по username через Bot API
|
||
pass
|
||
elif text.startswith('-100'):
|
||
try:
|
||
channel_id = int(text)
|
||
except ValueError:
|
||
pass
|
||
|
||
if not channel_id:
|
||
await message.reply_text(
|
||
"❌ Не удалось определить ID канала.\n"
|
||
"Попробуйте переслать сообщение из канала\n"
|
||
"Или /cancel для отмены"
|
||
)
|
||
return BotStates.CHANNEL_INPUT
|
||
|
||
# Проверяем права бота в канале
|
||
async with async_session_maker() as session:
|
||
bot_result = await session.execute(
|
||
select(BotModel).where(BotModel.id == bot_id)
|
||
)
|
||
bot = bot_result.scalar_one_or_none()
|
||
if not bot:
|
||
await message.reply_text("❌ Ошибка: бот не найден")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# TODO: Проверить права бота в канале через Bot API
|
||
|
||
# Проверяем, не добавлен ли уже канал
|
||
existing = await session.execute(
|
||
select(ChannelModel).where(
|
||
ChannelModel.chat_id == channel_id,
|
||
ChannelModel.bot_id == bot_id
|
||
)
|
||
)
|
||
if existing.scalar_one_or_none():
|
||
await message.reply_text("❌ Этот канал уже добавлен")
|
||
return BotStates.CONVERSATION_END
|
||
|
||
# Сохраняем канал
|
||
new_channel = ChannelModel(
|
||
owner_id=user.id,
|
||
bot_id=bot_id,
|
||
chat_id=channel_id,
|
||
title=channel_title if channel_title else None
|
||
)
|
||
session.add(new_channel)
|
||
await session.commit()
|
||
|
||
await message.reply_text(
|
||
"✅ Канал успешно добавлен!\n"
|
||
"Теперь вы можете создать пост через /newpost"
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка при добавлении канала.\n"
|
||
"Убедитесь, что бот добавлен в канал как администратор."
|
||
)
|
||
return BotStates.CONVERSATION_END
|
||
|
||
async def list_channels(update: Update, context: CallbackContext) -> None:
|
||
"""
|
||
Список каналов пользователя.
|
||
Команда: /channels
|
||
"""
|
||
message = update.effective_message
|
||
user = update.effective_user
|
||
if not message or not user:
|
||
return
|
||
|
||
try:
|
||
async with async_session_maker() as session:
|
||
result = await session.execute(
|
||
select(ChannelModel)
|
||
.where(ChannelModel.owner_id == user.id)
|
||
.order_by(ChannelModel.bot_id)
|
||
)
|
||
channels = list(result.scalars())
|
||
|
||
if not channels:
|
||
await message.reply_text(
|
||
"📢 У вас пока нет добавленных каналов.\n"
|
||
"Используйте /add_channel чтобы добавить канал."
|
||
)
|
||
return
|
||
|
||
text = "📢 Ваши каналы:\n\n"
|
||
current_bot_id = None
|
||
|
||
for channel in channels:
|
||
if channel.bot_id != current_bot_id:
|
||
current_bot_id = channel.bot_id
|
||
if channel.bot:
|
||
text += f"\n🤖 Бот @{channel.bot.username}:\n"
|
||
|
||
title = channel.title or str(channel.chat_id)
|
||
text += f"- {title}\n"
|
||
|
||
await message.reply_text(text)
|
||
|
||
except Exception as e:
|
||
await message.reply_text(
|
||
"❌ Произошла ошибка при получении списка каналов."
|
||
)
|