bot rafactor and bugfix
This commit is contained in:
93
app/services/channels.py
Normal file
93
app/services/channels.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Сервис для работы с каналами."""
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select
|
||||
from app.models.channel import Channel, BotChannel
|
||||
from app.models.bot import Bot
|
||||
from app.db.session import async_session_maker
|
||||
|
||||
class ChannelService:
|
||||
"""Сервис для работы с каналами."""
|
||||
|
||||
@staticmethod
|
||||
async def get_user_channels(user_id: int) -> List[Channel]:
|
||||
"""Получает список каналов пользователя."""
|
||||
async with async_session_maker() as session:
|
||||
stmt = select(Channel).where(Channel.owner_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def get_channel(channel_id: int) -> Optional[Channel]:
|
||||
"""Получает канал по ID."""
|
||||
async with async_session_maker() as session:
|
||||
stmt = select(Channel).where(Channel.id == channel_id)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalars().first()
|
||||
|
||||
@staticmethod
|
||||
async def get_bot_channels(bot_id: int) -> List[Channel]:
|
||||
"""Получает список каналов бота."""
|
||||
async with async_session_maker() as session:
|
||||
stmt = select(Channel).where(Channel.bot_id == bot_id)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def add_channel(
|
||||
owner_id: int,
|
||||
bot_id: int,
|
||||
chat_id: int,
|
||||
title: Optional[str] = None,
|
||||
username: Optional[str] = None
|
||||
) -> Channel:
|
||||
"""Добавляет новый канал."""
|
||||
async with async_session_maker() as session:
|
||||
channel = Channel(
|
||||
owner_id=owner_id,
|
||||
bot_id=bot_id,
|
||||
chat_id=chat_id,
|
||||
title=title,
|
||||
username=username
|
||||
)
|
||||
session.add(channel)
|
||||
await session.commit()
|
||||
await session.refresh(channel)
|
||||
return channel
|
||||
|
||||
@staticmethod
|
||||
async def update_channel(
|
||||
channel_id: int,
|
||||
title: Optional[str] = None,
|
||||
username: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Обновляет данные канала."""
|
||||
async with async_session_maker() as session:
|
||||
stmt = select(Channel).where(Channel.id == channel_id)
|
||||
result = await session.execute(stmt)
|
||||
channel = result.scalars().first()
|
||||
|
||||
if not channel:
|
||||
return False
|
||||
|
||||
if title is not None:
|
||||
channel.title = title
|
||||
if username is not None:
|
||||
channel.username = username
|
||||
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def delete_channel(channel_id: int) -> bool:
|
||||
"""Удаляет канал."""
|
||||
async with async_session_maker() as session:
|
||||
stmt = select(Channel).where(Channel.id == channel_id)
|
||||
result = await session.execute(stmt)
|
||||
channel = result.scalars().first()
|
||||
|
||||
if not channel:
|
||||
return False
|
||||
|
||||
await session.delete(channel)
|
||||
await session.commit()
|
||||
return True
|
||||
@@ -1,19 +1,214 @@
|
||||
from typing import Iterable
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Iterable, Tuple
|
||||
|
||||
def make_keyboard_payload(buttons: Iterable[tuple[str, str]] | None):
|
||||
from telegram import Bot, Message, InlineKeyboardMarkup
|
||||
from telegram.error import InvalidToken, TelegramError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def make_keyboard_payload(buttons: Optional[Iterable[Tuple[str, str]]]) -> Optional[Dict]:
|
||||
"""
|
||||
Создает структуру inline-клавиатуры для API Telegram.
|
||||
|
||||
Args:
|
||||
buttons: Список кнопок в формате [(text, url), ...]
|
||||
|
||||
Returns:
|
||||
Dict в формате {"rows": [[{"text": text, "url": url}], ...]}
|
||||
"""
|
||||
if not buttons:
|
||||
return None
|
||||
rows = [[{"text": t, "url": u}] for t, u in buttons]
|
||||
return {"rows": rows}
|
||||
|
||||
def build_payload(
|
||||
ptype: str,
|
||||
text: Optional[str] = None,
|
||||
media_file_id: Optional[str] = None,
|
||||
parse_mode: Optional[str] = None,
|
||||
keyboard: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Строит payload для отправки поста.
|
||||
|
||||
Args:
|
||||
ptype: Тип поста (text/photo/video/animation)
|
||||
text: Текст сообщения
|
||||
media_file_id: ID медиафайла в Telegram
|
||||
parse_mode: Формат разметки (HTML/MarkdownV2)
|
||||
keyboard: Inline клавиатура
|
||||
|
||||
Returns:
|
||||
Dict содержащий все необходимые поля для отправки
|
||||
"""
|
||||
payload: Dict[str, Any] = {
|
||||
"type": str(ptype),
|
||||
"text": text if text is not None else "",
|
||||
"parse_mode": str(parse_mode) if parse_mode is not None else "html",
|
||||
"keyboard": keyboard if keyboard is not None else {},
|
||||
}
|
||||
if media_file_id:
|
||||
payload["media_file_id"] = media_file_id
|
||||
return payload
|
||||
|
||||
def build_payload(ptype: str, text: str | None, media_file_id: str | None,
|
||||
parse_mode: str | None, keyboard: dict | None) -> dict:
|
||||
# ptype: "text" | "photo" | "video" | "animation"
|
||||
return {
|
||||
"type": ptype,
|
||||
"text": text,
|
||||
"media_file_id": media_file_id,
|
||||
"parse_mode": parse_mode,
|
||||
"keyboard": keyboard,
|
||||
}
|
||||
async def validate_bot_token(token: str) -> Tuple[bool, Optional[str], Optional[int]]:
|
||||
"""
|
||||
Проверяет валидность токена бота и возвращает его username и ID.
|
||||
|
||||
Args:
|
||||
token: Токен бота для проверки
|
||||
|
||||
Returns:
|
||||
tuple[bool, Optional[str], Optional[int]]: (is_valid, username, bot_id)
|
||||
"""
|
||||
try:
|
||||
bot = Bot(token)
|
||||
me = await bot.get_me()
|
||||
return True, me.username, me.id
|
||||
except InvalidToken:
|
||||
logger.warning(f"Invalid bot token provided: {token[:10]}...")
|
||||
return False, None, None
|
||||
except TelegramError as e:
|
||||
logger.error(f"Telegram error while validating bot token: {e}")
|
||||
return False, None, None
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error while validating bot token: {e}")
|
||||
return False, None, None
|
||||
finally:
|
||||
if 'bot' in locals():
|
||||
await bot.close()
|
||||
|
||||
def validate_message_length(text: str) -> bool:
|
||||
"""
|
||||
Проверяет длину сообщения на соответствие лимитам Telegram.
|
||||
|
||||
Args:
|
||||
text: Текст для проверки
|
||||
|
||||
Returns:
|
||||
bool: True если длина в пределах лимита
|
||||
"""
|
||||
return len(text) <= 4096 # Максимальная длина текста в Telegram
|
||||
|
||||
def is_valid_webhook_url(url: str) -> bool:
|
||||
"""Проверяет соответствие URL требованиям Telegram для вебхуков.
|
||||
|
||||
Args:
|
||||
url: URL для проверки
|
||||
|
||||
Returns:
|
||||
bool: True если URL валидный, иначе False
|
||||
"""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
return True # TODO: implement proper validation
|
||||
|
||||
|
||||
class PostService:
|
||||
"""Сервис для работы с постами."""
|
||||
|
||||
@staticmethod
|
||||
async def preview_post(message: Message, post_data: Dict[str, Any]) -> None:
|
||||
"""Показывает предпросмотр поста.
|
||||
|
||||
Args:
|
||||
message (Message): Telegram сообщение
|
||||
post_data (Dict[str, Any]): Данные поста из сессии
|
||||
"""
|
||||
text = post_data.get('text', '')
|
||||
parse_mode = post_data.get('parse_mode', 'HTML')
|
||||
keyboard = post_data.get('keyboard')
|
||||
|
||||
if keyboard:
|
||||
# Создаем разметку клавиатуры
|
||||
rows = keyboard.get('rows', [])
|
||||
markup = InlineKeyboardMarkup(rows) if rows else None
|
||||
else:
|
||||
markup = None
|
||||
|
||||
media_file_id = post_data.get('media_file_id')
|
||||
if media_file_id:
|
||||
# Отправляем медиафайл с подписью
|
||||
try:
|
||||
await message.reply_photo(
|
||||
photo=media_file_id,
|
||||
caption=text,
|
||||
parse_mode=parse_mode,
|
||||
reply_markup=markup
|
||||
)
|
||||
except TelegramError as e:
|
||||
# В случае ошибки отправляем только текст
|
||||
logger.error(f"Error sending photo preview: {e}")
|
||||
await message.reply_text(
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
reply_markup=markup
|
||||
)
|
||||
else:
|
||||
# Отправляем только текст
|
||||
await message.reply_text(
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def create_post(bot: Bot, chat_id: int, post_data: Dict[str, Any]) -> bool:
|
||||
"""Создает новый пост в канале.
|
||||
|
||||
Args:
|
||||
bot (Bot): Экземпляр бота
|
||||
chat_id (int): ID канала
|
||||
post_data (Dict[str, Any]): Данные поста
|
||||
|
||||
Returns:
|
||||
bool: Успешность создания
|
||||
"""
|
||||
try:
|
||||
text = post_data.get('text', '')
|
||||
parse_mode = post_data.get('parse_mode', 'HTML')
|
||||
keyboard = post_data.get('keyboard')
|
||||
|
||||
if keyboard:
|
||||
rows = keyboard.get('rows', [])
|
||||
markup = InlineKeyboardMarkup(rows) if rows else None
|
||||
else:
|
||||
markup = None
|
||||
|
||||
media_file_id = post_data.get('media_file_id')
|
||||
if media_file_id:
|
||||
await bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
photo=media_file_id,
|
||||
caption=text,
|
||||
parse_mode=parse_mode,
|
||||
reply_markup=markup
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
reply_markup=markup
|
||||
)
|
||||
return True
|
||||
|
||||
except TelegramError as e:
|
||||
logger.error(f"Error creating post: {e}")
|
||||
return False
|
||||
def validate_url(url: str) -> bool:
|
||||
"""Проверяет соответствие URL требованиям.
|
||||
|
||||
Args:
|
||||
url (str): URL для проверки
|
||||
|
||||
Returns:
|
||||
bool: True если URL соответствует требованиям
|
||||
"""
|
||||
return (
|
||||
url.startswith("https://") and
|
||||
not url.startswith("https://telegram.org") and
|
||||
len(url) <= 512
|
||||
)
|
||||
121
app/services/template.py
Normal file
121
app/services/template.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Логика работы с шаблонами."""
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.db.session import async_session_maker
|
||||
from app.models.templates import Template
|
||||
from app.models.post import PostType
|
||||
from app.bots.editor.messages import MessageType
|
||||
|
||||
class TemplateService:
|
||||
@staticmethod
|
||||
async def list_user_templates(owner_id: int) -> List[Template]:
|
||||
"""Получить список шаблонов пользователя."""
|
||||
async with async_session_maker() as session:
|
||||
query = select(Template).where(Template.owner_id == owner_id)
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars())
|
||||
|
||||
@staticmethod
|
||||
async def get_template(template_id: str) -> Optional[Template]:
|
||||
"""Получить шаблон по ID."""
|
||||
async with async_session_maker() as session:
|
||||
query = select(Template).where(Template.id == template_id)
|
||||
result = await session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_templates(owner_id: Optional[int] = None, limit: Optional[int] = None, offset: Optional[int] = None) -> list[Template]:
|
||||
"""Получить список всех шаблонов.
|
||||
|
||||
Args:
|
||||
owner_id: Опциональный ID владельца
|
||||
|
||||
Returns:
|
||||
List[Template]: Список шаблонов
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
query = Template.__table__.select()
|
||||
if owner_id is not None:
|
||||
query = query.where(Template.__table__.c.owner_id == owner_id)
|
||||
if offset is not None:
|
||||
query = query.offset(offset)
|
||||
if limit is not None:
|
||||
query = query.limit(limit)
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars())
|
||||
|
||||
async def create_template(template_data: Dict[str, Any]) -> Template:
|
||||
"""Создать новый шаблон.
|
||||
|
||||
Args:
|
||||
template_data: Данные шаблона
|
||||
|
||||
Returns:
|
||||
Template: Созданный шаблон
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
template = Template(**template_data)
|
||||
session.add(template)
|
||||
await session.commit()
|
||||
return template
|
||||
|
||||
async def render_template_by_name(
|
||||
name: str,
|
||||
template_vars: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Рендеринг шаблона по имени.
|
||||
|
||||
Args:
|
||||
name: Имя шаблона
|
||||
template_vars: Переменные для подстановки
|
||||
context: Дополнительный контекст
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Отрендеренные данные для поста
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
stmt = Template.__table__.select().where(Template.__table__.c.name == name)
|
||||
result = await session.execute(stmt)
|
||||
template = result.scalar_one_or_none()
|
||||
|
||||
if not template:
|
||||
raise ValueError(f"Шаблон {name} не найден")
|
||||
|
||||
text = template.content
|
||||
keyboard = template.keyboard_tpl
|
||||
|
||||
# Подстановка переменных
|
||||
for key, value in template_vars.items():
|
||||
text = text.replace(f"{{${key}}}", str(value))
|
||||
|
||||
# Проверяем тип и конвертируем в MessageType
|
||||
message_type = MessageType.TEXT
|
||||
if template.type == PostType.photo:
|
||||
message_type = MessageType.PHOTO
|
||||
elif template.type == PostType.video:
|
||||
message_type = MessageType.VIDEO
|
||||
|
||||
return {
|
||||
"type": message_type,
|
||||
"text": text,
|
||||
"keyboard": keyboard,
|
||||
"parse_mode": template.parse_mode
|
||||
}
|
||||
|
||||
async def count_templates(owner_id: Optional[int] = None) -> int:
|
||||
"""Посчитать количество шаблонов.
|
||||
|
||||
Args:
|
||||
owner_id: Опциональный ID владельца
|
||||
|
||||
Returns:
|
||||
int: Количество шаблонов
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
query = Template.__table__.select()
|
||||
if owner_id is not None:
|
||||
query = query.where(Template.__table__.c.owner_id == owner_id)
|
||||
result = await session.execute(query)
|
||||
return len(list(result.scalars()))
|
||||
Reference in New Issue
Block a user