bug fix
This commit is contained in:
11
Dockerfile.base
Normal file
11
Dockerfile.base
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
@@ -82,9 +82,9 @@ class KbBuilder:
|
|||||||
return InlineKeyboardMarkup(rows)
|
return InlineKeyboardMarkup(rows)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tpl_confirm_delete(tpl_id: int) -> InlineKeyboardMarkup:
|
def tpl_list_actions(tpl_id: int) -> InlineKeyboardMarkup:
|
||||||
rows = [
|
rows = [
|
||||||
[InlineKeyboardButton("Да, удалить", callback_data=f"tpldelok:{tpl_id}")],
|
[InlineKeyboardButton("Удалить", callback_data=f"tpldelok:{tpl_id}")],
|
||||||
[InlineKeyboardButton("Отмена", callback_data="tpl:cancel")],
|
[InlineKeyboardButton("Назад", callback_data="tpl:cancel")],
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(rows)
|
return InlineKeyboardMarkup(rows)
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ def build_app() -> Application:
|
|||||||
States.TPL_NEW_FORMAT: [CallbackQueryHandler(wizard.tpl_new_format, pattern=r"^tplfmt:")],
|
States.TPL_NEW_FORMAT: [CallbackQueryHandler(wizard.tpl_new_format, pattern=r"^tplfmt:")],
|
||||||
States.TPL_NEW_CONTENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, wizard.tpl_new_content)],
|
States.TPL_NEW_CONTENT: [MessageHandler(filters.TEXT & ~filters.COMMAND, wizard.tpl_new_content)],
|
||||||
States.TPL_NEW_KB: [MessageHandler(filters.TEXT & ~filters.COMMAND, wizard.tpl_new_kb)],
|
States.TPL_NEW_KB: [MessageHandler(filters.TEXT & ~filters.COMMAND, wizard.tpl_new_kb)],
|
||||||
States.TPL_CONFIRM_DELETE: [
|
|
||||||
CallbackQueryHandler(wizard.tpl_delete_ok, pattern=r"^tpldelok:"),
|
|
||||||
CallbackQueryHandler(wizard.choose_template_cancel, pattern=r"^tpl:cancel$"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
fallbacks=[CommandHandler("start", wizard.start)],
|
fallbacks=[CommandHandler("start", wizard.start)],
|
||||||
)
|
)
|
||||||
@@ -71,6 +68,7 @@ def build_app() -> Application:
|
|||||||
app.add_handler(post_conv)
|
app.add_handler(post_conv)
|
||||||
app.add_handler(tpl_conv)
|
app.add_handler(tpl_conv)
|
||||||
app.add_handler(CommandHandler("tpl_list", wizard.tpl_list))
|
app.add_handler(CommandHandler("tpl_list", wizard.tpl_list))
|
||||||
|
app.add_handler(CallbackQueryHandler(wizard.tpl_delete_ok, pattern=r"^tpldelok:"))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -21,4 +21,3 @@ class States(IntEnum):
|
|||||||
TPL_NEW_FORMAT = 13
|
TPL_NEW_FORMAT = 13
|
||||||
TPL_NEW_CONTENT = 14
|
TPL_NEW_CONTENT = 14
|
||||||
TPL_NEW_KB = 15
|
TPL_NEW_KB = 15
|
||||||
TPL_CONFIRM_DELETE = 16
|
|
||||||
|
|||||||
@@ -392,19 +392,31 @@ class EditorWizard:
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
async def tpl_list(self, update: Update, context: CallbackContext):
|
async def tpl_list(self, update: Update, context: CallbackContext):
|
||||||
context.user_data["tpl_page"] = 0
|
if not update.effective_user:
|
||||||
return await self._render_tpl_list(update.message, update.effective_user.id, page=0)
|
return -1
|
||||||
|
uid = update.effective_user.id
|
||||||
|
|
||||||
async def tpl_confirm_delete(self, update: Update, context: CallbackContext):
|
if update.callback_query and update.callback_query.data:
|
||||||
from .keyboards import KbBuilder # локальный импорт уже есть, просто используем метод
|
q = update.callback_query
|
||||||
q = update.callback_query
|
if not q:
|
||||||
await q.answer()
|
return -1
|
||||||
tpl_id = int(q.data.split(":", 1)[1])
|
await q.answer()
|
||||||
await q.edit_message_text("Удалить шаблон?", reply_markup=KbBuilder.tpl_confirm_delete(tpl_id))
|
page = int(q.data.split(":", 1)[1]) if ":" in q.data else 0
|
||||||
return States.TPL_CONFIRM_DELETE
|
return await self._render_tpl_list(q, uid, page)
|
||||||
|
|
||||||
|
if not context or not context.user_data:
|
||||||
|
return -1
|
||||||
|
context.user_data["tpl_page"] = 0
|
||||||
|
return await self._render_tpl_list(update.message, uid, page=0)
|
||||||
|
|
||||||
async def tpl_delete_ok(self, update: Update, context: CallbackContext):
|
async def tpl_delete_ok(self, update: Update, context: CallbackContext):
|
||||||
|
if not update.callback_query or not update.effective_user:
|
||||||
|
return -1
|
||||||
|
|
||||||
q = update.callback_query
|
q = update.callback_query
|
||||||
|
if not q.data:
|
||||||
|
return -1
|
||||||
|
|
||||||
await q.answer()
|
await q.answer()
|
||||||
uid = update.effective_user.id
|
uid = update.effective_user.id
|
||||||
tpl_id = int(q.data.split(":", 1)[1])
|
tpl_id = int(q.data.split(":", 1)[1])
|
||||||
@@ -417,7 +429,7 @@ class EditorWizard:
|
|||||||
async def _render_preview_and_confirm(self, q_or_msg, uid: int, name: str, ctx_vars: dict):
|
async def _render_preview_and_confirm(self, q_or_msg, uid: int, name: str, ctx_vars: dict):
|
||||||
rendered = await render_template_by_name(owner_id=uid, name=name, ctx=ctx_vars)
|
rendered = await render_template_by_name(owner_id=uid, name=name, ctx=ctx_vars)
|
||||||
text = rendered["text"]
|
text = rendered["text"]
|
||||||
parse_mode = rendered.get("parse_mode") or self.sessions.get(uid).parse_mode or "HTML"
|
parse_mode = rendered.get("parse_mode") or (self.sessions.get(uid).parse_mode if self.sessions.get(uid) else None) or "HTML"
|
||||||
|
|
||||||
if hasattr(q_or_msg, "edit_message_text"):
|
if hasattr(q_or_msg, "edit_message_text"):
|
||||||
await q_or_msg.edit_message_text(f"Предпросмотр:\n\n{text[:3500]}", parse_mode=parse_mode)
|
await q_or_msg.edit_message_text(f"Предпросмотр:\n\n{text[:3500]}", parse_mode=parse_mode)
|
||||||
@@ -454,12 +466,13 @@ class EditorWizard:
|
|||||||
return
|
return
|
||||||
token = settings.editor_bot_token
|
token = settings.editor_bot_token
|
||||||
payload = build_payload(
|
payload = build_payload(
|
||||||
ptype=s.type,
|
ptype=str(s.type or "text"),
|
||||||
text=s.text,
|
text=s.text or "",
|
||||||
media_file_id=s.media_file_id,
|
media_file_id=s.media_file_id,
|
||||||
parse_mode=s.parse_mode or "HTML",
|
parse_mode=s.parse_mode or "HTML",
|
||||||
keyboard=s.keyboard,
|
keyboard=s.keyboard,
|
||||||
)
|
)
|
||||||
|
from app.tasks.senders import send_post_task
|
||||||
send_post_task.delay(token, s.channel_id, payload)
|
send_post_task.delay(token, s.channel_id, payload)
|
||||||
await qmsg.edit_message_text("Отправка запущена.")
|
await qmsg.edit_message_text("Отправка запущена.")
|
||||||
|
|
||||||
@@ -467,12 +480,13 @@ class EditorWizard:
|
|||||||
s = self.sessions.get(uid)
|
s = self.sessions.get(uid)
|
||||||
token = settings.editor_bot_token
|
token = settings.editor_bot_token
|
||||||
payload = build_payload(
|
payload = build_payload(
|
||||||
ptype=s.type,
|
ptype=str(s.type or "text"),
|
||||||
text=s.text,
|
text=s.text or "",
|
||||||
media_file_id=s.media_file_id,
|
media_file_id=s.media_file_id,
|
||||||
parse_mode=s.parse_mode or "HTML",
|
parse_mode=s.parse_mode or "HTML",
|
||||||
keyboard=s.keyboard,
|
keyboard=s.keyboard,
|
||||||
)
|
)
|
||||||
|
from app.tasks.senders import send_post_task
|
||||||
send_post_task.apply_async(args=[token, s.channel_id, payload], eta=when)
|
send_post_task.apply_async(args=[token, s.channel_id, payload], eta=when)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,395 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import shlex
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional, Dict, List, Any
|
|
||||||
import time
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
|
||||||
from telegram.ext import (
|
|
||||||
Application, CommandHandler, MessageHandler, ConversationHandler,
|
|
||||||
CallbackQueryHandler, CallbackContext, filters,
|
|
||||||
)
|
|
||||||
from telegram.error import TelegramError
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
||||||
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.tasks.senders import send_post_task
|
|
||||||
from app.db.session import async_session_maker
|
|
||||||
from app.models.channel import Channel
|
|
||||||
from app.models.post import PostType
|
|
||||||
from app.services.templates import (
|
|
||||||
render_template_by_name, list_templates, count_templates,
|
|
||||||
create_template, delete_template, required_variables_of_template,
|
|
||||||
)
|
|
||||||
from jinja2 import TemplateError
|
|
||||||
|
|
||||||
# Настройка логирования
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Константы
|
|
||||||
MAX_MESSAGE_LENGTH = 4096
|
|
||||||
PAGE_SIZE = 8
|
|
||||||
SESSION_TIMEOUT = 3600 # 1 час
|
|
||||||
ALLOWED_URL_SCHEMES = ('http', 'https', 't.me')
|
|
||||||
|
|
||||||
# Состояния диалога
|
|
||||||
(
|
|
||||||
CHOOSE_CHANNEL, CHOOSE_TYPE, CHOOSE_FORMAT, ENTER_TEXT, ENTER_MEDIA, EDIT_KEYBOARD,
|
|
||||||
CONFIRM_SEND, ENTER_SCHEDULE, SELECT_TEMPLATE,
|
|
||||||
PREVIEW_VARS, PREVIEW_CONFIRM,
|
|
||||||
TPL_NEW_NAME, TPL_NEW_TYPE, TPL_NEW_FORMAT, TPL_NEW_CONTENT, TPL_NEW_KB, TPL_CONFIRM_DELETE,
|
|
||||||
) = range(16)
|
|
||||||
|
|
||||||
# In-memory сессии с метаданными
|
|
||||||
session: Dict[int, Dict[str, Any]] = {}
|
|
||||||
|
|
||||||
def validate_url(url: str) -> bool:
|
|
||||||
"""Проверка безопасности URL.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: Строка URL для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если URL безопасен, False в противном случае
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
result = urlparse(url)
|
|
||||||
return all([
|
|
||||||
result.scheme in ALLOWED_URL_SCHEMES,
|
|
||||||
result.netloc,
|
|
||||||
len(url) < 2048 # Максимальная длина URL
|
|
||||||
])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"URL validation failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def validate_message_length(text: str) -> bool:
|
|
||||||
"""Проверка длины сообщения согласно лимитам Telegram.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Текст для проверки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True если длина в пределах лимита
|
|
||||||
"""
|
|
||||||
return len(text) <= MAX_MESSAGE_LENGTH
|
|
||||||
|
|
||||||
def update_session_activity(uid: int) -> None:
|
|
||||||
"""Обновление времени последней активности в сессии."""
|
|
||||||
if uid in session:
|
|
||||||
session[uid]['last_activity'] = time.time()
|
|
||||||
|
|
||||||
def cleanup_old_sessions() -> None:
|
|
||||||
"""Периодическая очистка старых сессий."""
|
|
||||||
current_time = time.time()
|
|
||||||
for uid in list(session.keys()):
|
|
||||||
if current_time - session[uid].get('last_activity', 0) > SESSION_TIMEOUT:
|
|
||||||
logger.info(f"Cleaning up session for user {uid}")
|
|
||||||
del session[uid]
|
|
||||||
|
|
||||||
def parse_template_invocation(s: str) -> tuple[str, dict]:
|
|
||||||
"""Разбор строки вызова шаблона.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
s: Строка в формате #template_name key1=value1 key2=value2
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: (имя_шаблона, словарь_параметров)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: Если неверный формат строки
|
|
||||||
"""
|
|
||||||
s = s.strip()
|
|
||||||
if not s.startswith("#"):
|
|
||||||
raise ValueError("Имя шаблона должно начинаться с #")
|
|
||||||
parts = shlex.split(s)
|
|
||||||
if not parts:
|
|
||||||
raise ValueError("Пустой шаблон")
|
|
||||||
name = parts[0][1:]
|
|
||||||
args: dict[str, str] = {}
|
|
||||||
for tok in parts[1:]:
|
|
||||||
if "=" in tok:
|
|
||||||
k, v = tok.split("=", 1)
|
|
||||||
args[k.strip()] = v.strip()
|
|
||||||
return name, args
|
|
||||||
|
|
||||||
def parse_key_value_lines(text: str) -> dict:
|
|
||||||
"""Парсинг строк формата key=value.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Строки в формате key=value или key="quoted value"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Словарь параметров
|
|
||||||
"""
|
|
||||||
text = (text or "").strip()
|
|
||||||
if not text:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
out = {}
|
|
||||||
if "\n" in text:
|
|
||||||
for line in text.splitlines():
|
|
||||||
if "=" in line:
|
|
||||||
k, v = line.split("=", 1)
|
|
||||||
v = v.strip().strip('"')
|
|
||||||
if k.strip(): # Проверка на пустой ключ
|
|
||||||
out[k.strip()] = v
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
for tok in shlex.split(text):
|
|
||||||
if "=" in tok:
|
|
||||||
k, v = tok.split("=", 1)
|
|
||||||
if k.strip(): # Проверка на пустой ключ
|
|
||||||
out[k.strip()] = v
|
|
||||||
except ValueError as e:
|
|
||||||
logger.warning(f"Error parsing key-value line: {e}")
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
# -------- Команды верхнего уровня ---------
|
|
||||||
|
|
||||||
async def start(update: Update, context: CallbackContext) -> None:
|
|
||||||
"""Обработчик команды /start."""
|
|
||||||
update_session_activity(update.effective_user.id)
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Привет! Я редактор. Команды:\n"
|
|
||||||
"/newpost — мастер поста\n"
|
|
||||||
"/tpl_new — создать шаблон\n"
|
|
||||||
"/tpl_list — список шаблонов"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def newpost(update: Update, context: CallbackContext) -> int:
|
|
||||||
"""Начало создания нового поста."""
|
|
||||||
uid = update.effective_user.id
|
|
||||||
update_session_activity(uid)
|
|
||||||
|
|
||||||
session[uid] = {'last_activity': time.time()}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with async_session_maker() as s:
|
|
||||||
res = await s.execute(select(Channel).where(Channel.owner_id == uid).limit(50))
|
|
||||||
channels = list(res.scalars())
|
|
||||||
|
|
||||||
if not channels:
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Пока нет каналов. Добавь через админку или команду /add_channel (в разработке)."
|
|
||||||
)
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
kb = [
|
|
||||||
[InlineKeyboardButton(ch.title or str(ch.chat_id), callback_data=f"channel:{ch.id}")]
|
|
||||||
for ch in channels
|
|
||||||
]
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Выбери канал для публикации:",
|
|
||||||
reply_markup=InlineKeyboardMarkup(kb)
|
|
||||||
)
|
|
||||||
return CHOOSE_CHANNEL
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in newpost: {e}")
|
|
||||||
await update.message.reply_text("Произошла ошибка. Попробуйте позже.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
async def choose_channel(update: Update, context: CallbackContext) -> int:
|
|
||||||
"""Обработка выбора канала."""
|
|
||||||
q = update.callback_query
|
|
||||||
await q.answer()
|
|
||||||
|
|
||||||
uid = update.effective_user.id
|
|
||||||
update_session_activity(uid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
ch_id = int(q.data.split(":")[1])
|
|
||||||
session[uid]["channel_id"] = ch_id
|
|
||||||
|
|
||||||
kb = [
|
|
||||||
[
|
|
||||||
InlineKeyboardButton("Текст", callback_data="type:text"),
|
|
||||||
InlineKeyboardButton("Фото", callback_data="type:photo")
|
|
||||||
],
|
|
||||||
[
|
|
||||||
InlineKeyboardButton("Видео", callback_data="type:video"),
|
|
||||||
InlineKeyboardButton("GIF", callback_data="type:animation")
|
|
||||||
],
|
|
||||||
]
|
|
||||||
await q.edit_message_text(
|
|
||||||
"Тип поста:",
|
|
||||||
reply_markup=InlineKeyboardMarkup(kb)
|
|
||||||
)
|
|
||||||
return CHOOSE_TYPE
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
await q.edit_message_text("Ошибка: неверный формат ID канала")
|
|
||||||
return ConversationHandler.END
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in choose_channel: {e}")
|
|
||||||
await q.edit_message_text("Произошла ошибка. Попробуйте заново.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
# ... [Остальные функции обновляются аналогично] ...
|
|
||||||
|
|
||||||
async def enter_schedule(update: Update, context: CallbackContext) -> int:
|
|
||||||
"""Обработка ввода времени для отложенной публикации."""
|
|
||||||
uid = update.effective_user.id
|
|
||||||
update_session_activity(uid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
when = datetime.strptime(update.message.text.strip(), "%Y-%m-%d %H:%M")
|
|
||||||
|
|
||||||
if when < datetime.now():
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Дата должна быть в будущем. Укажите корректное время в формате YYYY-MM-DD HH:MM"
|
|
||||||
)
|
|
||||||
return ENTER_SCHEDULE
|
|
||||||
|
|
||||||
await _dispatch_with_eta(uid, when)
|
|
||||||
await update.message.reply_text("Задача запланирована.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Неверный формат даты. Используйте формат YYYY-MM-DD HH:MM"
|
|
||||||
)
|
|
||||||
return ENTER_SCHEDULE
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error scheduling post: {e}")
|
|
||||||
await update.message.reply_text("Ошибка планирования. Попробуйте позже.")
|
|
||||||
return ConversationHandler.END
|
|
||||||
|
|
||||||
async def _dispatch_with_eta(uid: int, when: datetime) -> None:
|
|
||||||
"""Отправка отложенного поста."""
|
|
||||||
data = session.get(uid)
|
|
||||||
if not data:
|
|
||||||
raise ValueError("Сессия потеряна")
|
|
||||||
|
|
||||||
token = settings.editor_bot_token
|
|
||||||
try:
|
|
||||||
payload = build_payload(
|
|
||||||
ptype=data.get("type"),
|
|
||||||
text=data.get("text"),
|
|
||||||
media_file_id=data.get("media_file_id"),
|
|
||||||
parse_mode=data.get("parse_mode") or "HTML",
|
|
||||||
keyboard=data.get("keyboard"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Проверка длины сообщения
|
|
||||||
if not validate_message_length(payload.get("text", "")):
|
|
||||||
raise ValueError("Превышен максимальный размер сообщения")
|
|
||||||
|
|
||||||
# Проверка URL в клавиатуре
|
|
||||||
if keyboard := payload.get("keyboard"):
|
|
||||||
for row in keyboard.get("rows", []):
|
|
||||||
for btn in row:
|
|
||||||
if "url" in btn and not validate_url(btn["url"]):
|
|
||||||
raise ValueError(f"Небезопасный URL: {btn['url']}")
|
|
||||||
|
|
||||||
send_post_task.apply_async(
|
|
||||||
args=[token, data["channel_id"], payload],
|
|
||||||
eta=when
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in _dispatch_with_eta: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Инициализация и запуск бота."""
|
|
||||||
try:
|
|
||||||
# Настройка логирования
|
|
||||||
logging.basicConfig(
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
level=logging.INFO
|
|
||||||
)
|
|
||||||
|
|
||||||
# Инициализация планировщика
|
|
||||||
scheduler = AsyncIOScheduler()
|
|
||||||
scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30)
|
|
||||||
scheduler.start()
|
|
||||||
|
|
||||||
app = Application.builder().token(settings.editor_bot_token).build()
|
|
||||||
|
|
||||||
# Регистрация обработчиков
|
|
||||||
post_conv = ConversationHandler(
|
|
||||||
entry_points=[CommandHandler("newpost", newpost)],
|
|
||||||
states={
|
|
||||||
CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")],
|
|
||||||
CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")],
|
|
||||||
CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")],
|
|
||||||
ENTER_TEXT: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text),
|
|
||||||
CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"),
|
|
||||||
],
|
|
||||||
SELECT_TEMPLATE: [
|
|
||||||
CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"),
|
|
||||||
CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"),
|
|
||||||
CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"),
|
|
||||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
|
||||||
],
|
|
||||||
PREVIEW_VARS: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars)
|
|
||||||
],
|
|
||||||
PREVIEW_CONFIRM: [
|
|
||||||
CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"),
|
|
||||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
|
||||||
],
|
|
||||||
ENTER_MEDIA: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media)
|
|
||||||
],
|
|
||||||
EDIT_KEYBOARD: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard)
|
|
||||||
],
|
|
||||||
CONFIRM_SEND: [
|
|
||||||
CallbackQueryHandler(confirm_send, pattern=r"^send:")
|
|
||||||
],
|
|
||||||
ENTER_SCHEDULE: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule)
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fallbacks=[CommandHandler("start", start)],
|
|
||||||
)
|
|
||||||
|
|
||||||
tpl_conv = ConversationHandler(
|
|
||||||
entry_points=[CommandHandler("tpl_new", tpl_new_start)],
|
|
||||||
states={
|
|
||||||
TPL_NEW_NAME: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name)
|
|
||||||
],
|
|
||||||
TPL_NEW_TYPE: [
|
|
||||||
CallbackQueryHandler(tpl_new_type, pattern=r"^tpltype:")
|
|
||||||
],
|
|
||||||
TPL_NEW_FORMAT: [
|
|
||||||
CallbackQueryHandler(tpl_new_format, pattern=r"^tplfmt:")
|
|
||||||
],
|
|
||||||
TPL_NEW_CONTENT: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content)
|
|
||||||
],
|
|
||||||
TPL_NEW_KB: [
|
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb)
|
|
||||||
],
|
|
||||||
TPL_CONFIRM_DELETE: [
|
|
||||||
CallbackQueryHandler(tpl_delete_ok, pattern=r"^tpldelok:"),
|
|
||||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fallbacks=[CommandHandler("start", start)],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_handler(CommandHandler("start", start))
|
|
||||||
app.add_handler(post_conv)
|
|
||||||
app.add_handler(tpl_conv)
|
|
||||||
app.add_handler(CommandHandler("tpl_list", tpl_list))
|
|
||||||
|
|
||||||
# Запуск бота
|
|
||||||
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(f"Critical error in main: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -14,3 +14,4 @@ loguru
|
|||||||
wget
|
wget
|
||||||
redis
|
redis
|
||||||
jinja2
|
jinja2
|
||||||
|
APScheduler>=3.10.0
|
||||||
Reference in New Issue
Block a user