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())