Files
tg_post_min/app/bot/handlers/drafts.py

195 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# app/bot/handlers/drafts.py
from datetime import datetime
from html import escape as html_escape
import logging
from telegram import Update, InputMediaPhoto, InputMediaVideo, InputMediaAnimation
from telegram.constants import ChatType, ParseMode
from telegram.ext import ContextTypes
from app.db.session import get_session
from app.db.models import User, Draft
from app.bot.messages import ASK_MEDIA, CONFIRM, NEED_START_NEW
from app.bot.keyboards.common import kb_confirm
from app.bot.handlers.add_group import add_group_capture, STATE_KEY # "await_chat_id"
log = logging.getLogger(__name__)
# Состояния редактора в user_data
STATE_DRAFT = "draft_state"
KEY_DRAFT_ID = "draft_id"
STATE_AWAIT_MEDIA = "await_media"
STATE_AWAIT_TEXT = "await_text"
STATE_CONFIRM = "confirm"
def _ud_state(ctx: ContextTypes.DEFAULT_TYPE) -> str:
"""Короткая сводка по user_data для логов."""
ud = ctx.user_data
return (
f"ud(draft_id={ud.get(KEY_DRAFT_ID)}, state={ud.get(STATE_DRAFT)}, "
f"await_dict={bool(ud.get('await_dict_file'))}, await_chat_id={bool(ud.get(STATE_KEY))})"
)
def _start_new_draft(tg_id: int) -> Draft:
"""Создать новый черновик и пометить предыдущие editing как cancelled."""
with get_session() as s:
u = s.query(User).filter_by(tg_id=tg_id).first()
if not u:
u = User(tg_id=tg_id, name="")
s.add(u); s.commit(); s.refresh(u)
# Закрываем старые "editing"
s.query(Draft).filter(
Draft.user_id == u.id, Draft.status == "editing"
).update({"status": "cancelled"})
# Создаём новый
d = Draft(user_id=u.id, status="editing")
s.add(d); s.commit(); s.refresh(d)
return d
async def new_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Старт редактора: сначала медиа, потом текст, потом подтверждение."""
uid = getattr(update.effective_user, "id", None)
cid = getattr(update.effective_chat, "id", None)
log.info("drafts:new_cmd start user=%s chat=%s %s", uid, cid, _ud_state(ctx))
# ЖЁСТКИЙ СБРОС конфликтующих режимов
for k in ("await_dict_file", "dict_params", STATE_KEY):
if ctx.user_data.pop(k, None) is not None:
log.debug("drafts:new_cmd cleared flag=%s user=%s chat=%s", k, uid, cid)
d = _start_new_draft(uid)
ctx.user_data[KEY_DRAFT_ID] = d.id
ctx.user_data[STATE_DRAFT] = STATE_AWAIT_MEDIA
log.info("drafts:new_cmd created draft_id=%s -> state=%s user=%s chat=%s",
d.id, STATE_AWAIT_MEDIA, uid, cid)
# Кнопку «Дальше — текст» показываем под сообщением «Медиа добавлено» (см. on_media).
await update.effective_message.reply_text(ASK_MEDIA)
async def on_text(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""
Обработка текста на шаге 2/3.
"""
uid = getattr(update.effective_user, "id", None)
cid = getattr(update.effective_chat, "id", None)
msg_text = (update.effective_message.text or "")
log.info("drafts:on_text received user=%s chat=%s len=%s %s",
uid, cid, len(msg_text), _ud_state(ctx))
# Не мешаем /spam_import (ждём файл/текст словаря)
if ctx.user_data.get("await_dict_file"):
log.info("drafts:on_text blocked_by_import user=%s chat=%s", uid, cid)
await update.effective_message.reply_text(
"Сейчас активен импорт словаря. Пришлите .txt/.csv как документ "
"или введите /cancel (или /new) для выхода из импорта."
)
return
# В процессе /add_group — делегируем туда
if ctx.user_data.get(STATE_KEY):
log.info("drafts:on_text delegated_to_add_group user=%s chat=%s", uid, cid)
return await add_group_capture(update, ctx)
if update.effective_chat.type != ChatType.PRIVATE:
log.debug("drafts:on_text ignored_non_private user=%s chat=%s", uid, cid)
return
draft_id = ctx.user_data.get(KEY_DRAFT_ID)
state = ctx.user_data.get(STATE_DRAFT)
if not draft_id or not state:
log.warning("drafts:on_text no_draft_or_state user=%s chat=%s %s", uid, cid, _ud_state(ctx))
await update.effective_message.reply_text(NEED_START_NEW)
return
if state == STATE_AWAIT_MEDIA:
log.info("drafts:on_text wrong_state_await_media user=%s chat=%s draft=%s", uid, cid, draft_id)
await update.effective_message.reply_text(
"Сначала добавьте медиа и нажмите «Дальше — текст» (кнопка под сообщением «Медиа добавлено»)."
)
return
if state == STATE_CONFIRM:
log.info("drafts:on_text already_confirm user=%s chat=%s draft=%s", uid, cid, draft_id)
await update.effective_message.reply_text(
"Пост уже готов — нажмите «Отправить» или «Отменить»."
)
return
if state != STATE_AWAIT_TEXT:
log.debug("drafts:on_text unknown_state user=%s chat=%s state=%s draft=%s",
uid, cid, state, draft_id)
return
# ШАГ 2/3 — сохраняем текст
safe_html = html_escape(msg_text)
with get_session() as s:
d = s.get(Draft, draft_id)
before_len = len(d.text or "") if d and d.text else 0
d.text = safe_html
d.updated_at = datetime.utcnow()
s.commit()
media = sorted(d.media, key=lambda m: m.order)
log.info("drafts:on_text saved_text user=%s chat=%s draft=%s old_len=%s new_len=%s media_count=%s",
uid, cid, draft_id, before_len, len(safe_html), len(media))
# Предпросмотр
try:
if media:
if len(media) > 1:
im = []
for i, m in enumerate(media):
cap = d.text if i == 0 else None
if m.kind == "photo":
im.append(InputMediaPhoto(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
elif m.kind == "video":
im.append(InputMediaVideo(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
else:
im.append(InputMediaAnimation(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
await update.effective_chat.send_media_group(media=im)
log.info("drafts:on_text preview_album user=%s chat=%s draft=%s parts=%s",
uid, cid, draft_id, len(im))
else:
m = media[0]
if m.kind == "photo":
await update.effective_chat.send_photo(photo=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
elif m.kind == "video":
await update.effective_chat.send_video(video=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
else:
await update.effective_chat.send_animation(animation=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
log.info("drafts:on_text preview_single user=%s chat=%s draft=%s kind=%s",
uid, cid, draft_id, m.kind)
else:
await update.effective_chat.send_message(text=d.text or "(пусто)", parse_mode=ParseMode.HTML)
log.info("drafts:on_text preview_text user=%s chat=%s draft=%s", uid, cid, draft_id)
except Exception as e:
log.exception("drafts:on_text preview_error user=%s chat=%s draft=%s err=%s",
uid, cid, draft_id, e)
# Переход к подтверждению
ctx.user_data[STATE_DRAFT] = STATE_CONFIRM
log.info("drafts:on_text -> state=%s user=%s chat=%s draft=%s",
STATE_CONFIRM, uid, cid, draft_id)
await update.effective_message.reply_text(
"Шаг 3/3 — подтверждение.\nПроверьте пост и нажмите «Отправить» или «Отменить».",
reply_markup=kb_confirm(draft_id),
)
async def on_text_gate(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if update.effective_chat.type != ChatType.PRIVATE:
return
if ctx.user_data.get("await_dict_file"):
return
if ctx.user_data.get(STATE_DRAFT) == STATE_AWAIT_TEXT:
log.info(
"drafts:on_text_gate capture user=%s chat=%s state=%s draft_id=%s",
getattr(update.effective_user, "id", None),
getattr(update.effective_chat, "id", None),
ctx.user_data.get(STATE_DRAFT),
ctx.user_data.get(KEY_DRAFT_ID),
)
return await on_text(update, ctx)