CCCPaste

Bot_ATM

import asyncio
import os
from dataclasses import dataclass
from datetime import datetime

import pytz
from aiogram import Bot, Dispatcher, F, Router
from aiogram.client.default import DefaultBotProperties
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, BotCommand
from aiogram.filters import CommandStart, Command, StateFilter
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage

TOKEN = os.getenv("BOT_TOKEN", "8198963636:AAG_o5luzCC6t_rEve34T9FiBC4Z6xHTFiE")
FEEDBACK_CHANNEL_ID = int(os.getenv("FEEDBACK_CHANNEL_ID", "-4931351107"))
SUBMITTED: set[int] = set()

TZ = pytz.timezone("Europe/Moscow")

def has_submitted(user_id: int) -> bool:
    return user_id in SUBMITTED

def mark_submitted(user_id: int) -> None:
    SUBMITTED.add(user_id)

async def guard_single_feedback(m: Message) -> bool:
    u = m.from_user
    if not u:
        return False
    if has_submitted(u.id):
        await m.answer("Ты уже оставил один отзыв. К сожалению, отправить его второй раз нельзя.")
        return True
    return False

RATES = {
    "1": {"emoji": "😞", "label": "Плохо"},
    "2": {"emoji": "😐", "label": "Нормально"},
    "3": {"emoji": "🤩", "label": "Отлично"},
}

def rate_kb() -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup(inline_keyboard=[[
        InlineKeyboardButton(text="😞", callback_data="rate:1"),
        InlineKeyboardButton(text="😐", callback_data="rate:2"),
        InlineKeyboardButton(text="🤩", callback_data="rate:3"),
    ]])

def skip_kb() -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Пропустить", callback_data="skip")]])

class FB(StatesGroup):
    rating = State()
    liked = State()
    disliked = State()
    extra = State()

@dataclass
class Draft:
    rate: str | None = None
    liked: str | None = None
    disliked: str | None = None
    extra: str | None = None

router = Router()

@router.message(CommandStart())
async def start(m: Message, state: FSMContext):
    await state.clear()
    if await guard_single_feedback(m):
        return
    await state.update_data(
        draft=Draft().__dict__,
        user_id=m.from_user.id if m.from_user else None,
        user_name=m.from_user.full_name if m.from_user else "—",
        user_username=m.from_user.username if m.from_user else None
    )
    await state.set_state(FB.rating)
    await m.answer(
        "Привет! Прошло наше занятие — удели, пожалуйста, пару минут на обратную связь.\n\n"
        "Как тебе занятие? Выбери смайлик:",
        reply_markup=rate_kb()
    )

@router.message(Command("feedback"))
async def feedback_cmd(m: Message, state: FSMContext):
    if await guard_single_feedback(m):
        return
    await start(m, state)

@router.callback_query(F.data.startswith("rate:"), FB.rating)
async def choose_rate(q: CallbackQuery, state: FSMContext):
    _, val = q.data.split(":", 1)
    data = await state.get_data()
    draft = data.get("draft") or {}
    draft["rate"] = val
    await state.update_data(draft=draft)
    await state.set_state(FB.liked)
    await q.message.answer("Что тебе понравилось?") 
    await q.answer()

@router.message(StateFilter(FB.liked), F.text)
async def got_liked(m: Message, state: FSMContext):
    try:
        data = await state.get_data()
        draft = data.get("draft") or {}
        draft["liked"] = (m.text or "").strip()
        await state.update_data(draft=draft)
        await state.set_state(FB.disliked)
        await m.answer("Что тебе НЕ понравилось?")
    except Exception:
        await m.answer("Ошибка на шаге «что понравилось». Попробуй ещё раз: /feedback")

@router.message(StateFilter(FB.disliked), F.text)
async def got_disliked(m: Message, state: FSMContext):
    try:
        data = await state.get_data()
        draft = data.get("draft") or {}
        draft["disliked"] = (m.text or "").strip()
        await state.update_data(draft=draft)
        await state.set_state(FB.extra)
        await m.answer("Если остались мысли — напиши. Это необязательно.", reply_markup=skip_kb())
    except Exception:
        await m.answer("Ошибка на шаге «что не понравилось». Попробуй ещё раз: /feedback")

@router.callback_query(F.data == "skip", FB.extra)
async def extra_skip(q: CallbackQuery, state: FSMContext):
    data = await state.get_data()
    draft = data.get("draft") or {}
    draft["extra"] = ""
    await state.update_data(draft=draft)
    await finalize(q.message, state)
    await q.answer()

@router.message(StateFilter(FB.extra), F.text)
async def got_extra(m: Message, state: FSMContext):
    data = await state.get_data()
    draft = data.get("draft") or {}
    draft["extra"] = (m.text or "").strip()
    await state.update_data(draft=draft)
    await finalize(m, state)

async def finalize(msg: Message, state: FSMContext):
    try:
        data = await state.get_data()
        d = Draft(**(data.get("draft") or {}))

        user_id = data.get("user_id")
        name = data.get("user_name", "—")
        user_username = data.get("user_username")

        username = f"@{user_username}" if user_username else f"id:{user_id if user_id else '—'}"
        link = f"tg://user?id={user_id}" if user_id else "—"

        rate = RATES.get(d.rate or "2", RATES["2"])
        dt = datetime.now(TZ).strftime("%Y-%m-%d %H:%M")

        liked = d.liked.strip() if d.liked else "—"
        disliked = d.disliked.strip() if d.disliked else "—"
        extra = d.extra.strip() if d.extra else "—"

        text = (
            "📝 Отзыв о занятии\n\n"
            f"👤 Имя: {name}\n"
            f"🔗 Профиль: {link}\n"
            f"🆔 ID: {user_id if user_id else '—'}\n"
            f"@username: {username}\n"
            f"🗓 {dt} (МСК)\n"
            f"🙂 Оценка: {rate['emoji']} {rate['label']}\n\n"
            f"Что понравилось:\n{liked}\n\n"
            f"Что не понравилось:\n{disliked}\n\n"
            f"Дополнительно:\n{extra}"
        )

        try:
            await msg.bot.send_message(chat_id=FEEDBACK_CHANNEL_ID, text=text)
        except Exception:
            await msg.answer("Не удалось отправить отзыв в канал.\n\n" + text)
        else:
            if user_id:
                mark_submitted(user_id)
            await msg.answer("Спасибо за обратную связь, до новых встреч!")
    except Exception:
        await msg.answer("Не удалось завершить отзыв. Попробуй снова: /feedback")
    finally:
        await state.clear()

async def on_startup(bot: Bot):
    try:
        await bot.delete_webhook(drop_pending_updates=True)
    except Exception:
        pass
    try:
        await bot.set_my_commands([
            BotCommand(command="start", description="Начать опрос"),
            BotCommand(command="feedback", description="Оставить отзыв"),
        ])
    except Exception:
        pass

async def main():
    bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=None))
    dp = Dispatcher(storage=MemoryStorage())
    dp.include_router(router)
    try:
        await on_startup(bot)
        await dp.start_polling(bot)
    finally:
        await bot.session.close()

if __name__ == "__main__":
    asyncio.run(main())