"""气氛组 + 小游戏 handler""" import asyncio import logging import config from services.message_utils import record_bot_message from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ApplicationHandlerStop, CallbackQueryHandler, ContextTypes, CommandHandler, MessageHandler, filters from db.models import Action from services.game_service import ( get_trivia_question, check_trivia_answer, get_leaderboard, daily_checkin, get_scores_summary, SHOP_ITEMS, buy_shop_item, list_shop_items, parse_lottery_spec, create_lottery_event, lottery_condition_text, lottery_end_text, count_lottery_participants, join_lottery_event, draw_lottery_event, ) logger = logging.getLogger("spam_guard") PANEL_IDLE_SECONDS = 60 # 关键词触发 def panel_keyboard(): return InlineKeyboardMarkup([ [InlineKeyboardButton("👤 我的信息", callback_data="panel:me"), InlineKeyboardButton("✅ 签到", callback_data="panel:checkin")], [InlineKeyboardButton("🎁 发起抽奖", callback_data="panel:lottery"), InlineKeyboardButton("🛒 商城", callback_data="panel:shop")], [InlineKeyboardButton("📖 帮助", callback_data="panel:help")], ]) def schedule_panel_idle_delete(context: ContextTypes.DEFAULT_TYPE, message, seconds: int = PANEL_IDLE_SECONDS): tasks = context.bot_data.setdefault("panel_idle_tasks", {}) key = (message.chat_id, message.message_id) old_task = tasks.pop(key, None) if old_task and not old_task.done(): old_task.cancel() async def _delete_later(): try: await asyncio.sleep(seconds) await message.delete() except asyncio.CancelledError: return except Exception: pass finally: tasks.pop(key, None) tasks[key] = asyncio.create_task(_delete_later()) TRIGGER_REACTIONS = { "谢谢": "👍❤️", "感谢": "👍❤️🎉", "哈哈": "😂", "笑死": "😂💀", "牛逼": "🔥💪", "nb": "🔥💪", "666": "🔥", "晚安": "🌙✨", } async def handle_trigger(update: Update, context: ContextTypes.DEFAULT_TYPE): """关键词触发气氛组""" msg = update.effective_message if not msg or not msg.text: return text = msg.text.strip() for keyword, emoji in TRIGGER_REACTIONS.items(): if text == keyword or text.startswith(keyword): try: await msg.react(emoji) except Exception: pass break async def cmd_checkin(update: Update, context: ContextTypes.DEFAULT_TYPE): """每日签到""" user = update.effective_user result = await daily_checkin(update.effective_chat.id, user.id, user.username or user.first_name) if result["ok"]: reply = await update.message.reply_text( f"✅ 签到成功!\n" f"📊 积分: {result['score']}\n" f"🔥 连续签到: {result['streak']} 天\n" f"🎁 签到奖励: +{result['bonus']} 分(基础 {result.get('base', 0)} + 连签 {result.get('streak_bonus', 0)})" ) else: reply = await update.message.reply_text("❌ 你今天已经签到过了") asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def cmd_trivia(update: Update, context: ContextTypes.DEFAULT_TYPE): """收割机知识答题""" q = await get_trivia_question() text = f"❓ {q['q']}\n\n" for i, opt in enumerate(q["opts"], 1): text += f"{i}. {opt}\n" text += f"\n💡 请回复序号或答案文字" context.user_data["trivia_answer"] = q["a"] context.user_data["trivia_options"] = q["opts"] reply = await update.message.reply_text(text) asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def cmd_score(update: Update, context: ContextTypes.DEFAULT_TYPE): """查看积分""" from db.models import GameScore user = update.effective_user scores = await GameScore.filter(user_id=user.id).all() if not scores: reply = await update.message.reply_text("📊 你还没有积分记录\n💡 签到 /checkin 或答题 /trivia 开始赚积分") else: text = "📊 你的积分\n━━━━━━━━━━━━\n" for s in scores: text += f"🎮 {s.game_type}: {s.score} 分 (胜率 {s.wins}/{s.total})\n" reply = await update.message.reply_text(text) asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def cmd_rank(update: Update, context: ContextTypes.DEFAULT_TYPE): """排行榜""" top = await get_leaderboard(update.effective_chat.id, "trivia", 10) if not top: reply = await update.message.reply_text("🏆 暂无排行数据") else: text = "🏆 知识答题排行榜\n━━━━━━━━━━━━━━━━━━\n" medals = ["🥇", "🥈", "🥉"] for i, s in enumerate(top): medal = medals[i] if i < 3 else f" {i+1}." text += f"{medal} @{s.username or s.user_id} — {s.score} 分\n" reply = await update.message.reply_text(text) asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def build_lottery_text(event, count: int | None = None) -> str: if count is None: count = await count_lottery_participants(event.id) return ( "🎁 群内抽奖\n" "━━━━━━━━━━━━\n" f"🏷️ 标题:{event.title or event.prize}\n" f"🎯 奖品:{event.prize}\n" f"👤 发起人:@{event.creator_name or event.creator_id}\n" + (f"📝 描述:{event.description}\n" if getattr(event, "description", None) else "") + f"✅ 参与条件:{await lottery_condition_text(event)}\n" f"⏱️ 结束条件:{await lottery_end_text(event)}\n" f"👥 当前参与:{count} 人\n\n" "点击下方按钮参与抽奖。" ) def lottery_keyboard(event_id: int) -> InlineKeyboardMarkup: return InlineKeyboardMarkup([[InlineKeyboardButton("🎁 参与抽奖", callback_data=f"lottery:join:{event_id}")]]) async def schedule_lottery_draw(context: ContextTypes.DEFAULT_TYPE, event_id: int, seconds: int): async def _run(): try: await asyncio.sleep(seconds) result = await draw_lottery_event(event_id) event = result.get("event") if not event or not event.message_id: return if result.get("ok"): winner = result["winner"] text = ( "🎉 抽奖已开奖\n" "━━━━━━━━━━━━\n" f"🏷️ 标题:{event.title or event.prize}\n" f"🎯 奖品:{event.prize}\n" f"👥 参与人数:{result['count']} 人\n" f"🏆 中奖用户:@{winner.username or winner.first_name or winner.user_id}" ) else: text = f"🎁 抽奖已结束\n━━━━━━━━━━━━\n🎯 奖品:{event.prize}\n❌ {result.get('reason', '未开奖')}" await context.bot.edit_message_text(chat_id=event.chat_id, message_id=event.message_id, text=text) except Exception as e: logger.warning(f"⚠️ 自动开奖失败: {type(e).__name__}: {e}") asyncio.create_task(_run()) async def create_lottery_from_text(update: Update, context: ContextTypes.DEFAULT_TYPE, spec_text: str, publish_chat_id: int | None = None): user = update.effective_user msg = update.effective_message spec = parse_lottery_spec(spec_text) target_chat_id = publish_chat_id or msg.chat_id event = await create_lottery_event( chat_id=target_chat_id, creator_id=user.id, creator_name=user.username or user.first_name or str(user.id), spec=spec, ) lottery_text = await build_lottery_text(event) sent = await context.bot.send_message( chat_id=target_chat_id, text=lottery_text, reply_markup=lottery_keyboard(event.id), ) await record_bot_message(sent, lottery_text) event.message_id = sent.message_id await event.save() if event.draw_type == "time" or event.end_type in {"minutes", "time"}: await schedule_lottery_draw(context, event.id, event.end_value * 60) return event async def send_lottery_private入口(update: Update, context: ContextTypes.DEFAULT_TYPE): me = await context.bot.get_me() bot_username = me.username target_chat_id = update.effective_chat.id url = f"https://t.me/{bot_username}?start=lottery_{target_chat_id}" keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("去私聊设置抽奖", url=url)]]) text = ( "🎁 发起群内抽奖\n" "为了避免奖品和条件刷屏,请到 Bot 私聊界面设置。\n\n" f"👉 点击进入:{url}" ) reply = await update.effective_message.reply_text(text, reply_markup=keyboard, disable_web_page_preview=True) asyncio.create_task(auto_delete(update.effective_message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) return reply async def cmd_lottery(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.type != "private": await send_lottery_private入口(update, context) return context.user_data["awaiting_lottery_spec"] = True context.user_data["lottery_target_chat_id"] = config.TG_CHAT_ID reply = await update.message.reply_text( "🎁 发起群内抽奖\n" "请发送:奖品 | 参与条件 | 结束条件\n\n" "示例:月卡一张 | all | people:3\n" "参与条件:all / checkin / points:N\n" "结束条件:people:N / minutes:N" ) await record_bot_message(reply) async def cmd_shop(update: Update, context: ContextTypes.DEFAULT_TYPE): user = update.effective_user summary = await get_scores_summary(user.id, query.message.chat_id) text = "🛒 积分商城\n━━━━━━━━━━━━\n" text += f"📊 当前总积分:{summary['total']} 分\n\n" for item_id, item in SHOP_ITEMS.items(): text += f"{item_id}. {item['name']} — {item['cost']} 分\n {item['desc']}\n" text += "\n购买:/buy 商品编号,例如 /buy 1" reply = await update.message.reply_text(text) asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def cmd_buy(update: Update, context: ContextTypes.DEFAULT_TYPE): if not context.args: reply = await update.message.reply_text("用法:/buy 商品编号,例如 /buy 1") else: result = await buy_shop_item(update.effective_chat.id, update.effective_user.id, context.args[0], update.effective_user.username or update.effective_user.first_name or str(update.effective_user.id)) if result["ok"]: item = result["item"] await Action.create(action="SHOP_BUY", chat_id=update.effective_chat.id, user_id=update.effective_user.id, username=update.effective_user.username, details={"item": item, "chat_id": update.effective_chat.id}) reply = await update.message.reply_text(f"✅ 兑换成功:{item['name']}\n📊 剩余总积分:{result['total']} 分") else: item = result.get("item") extra = f",需要 {item['cost']} 分" if item else "" reply = await update.message.reply_text(f"❌ 兑换失败:{result['reason']}{extra}") asyncio.create_task(auto_delete(update.message, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) async def cmd_panel(update: Update, context: ContextTypes.DEFAULT_TYPE): reply = await update.message.reply_text( "🎛️ 操作面板\n请选择要执行的操作:\n\n⏱️ 60 秒无操作后自动消失。", reply_markup=panel_keyboard(), ) asyncio.create_task(auto_delete(update.message, seconds=60)) schedule_panel_idle_delete(context, reply) async def handle_panel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() user = query.from_user action = query.data.split(":", 1)[1] if action == "checkin": result = await daily_checkin(query.message.chat_id, user.id, user.username or user.first_name or str(user.id)) text = "❌ 你今天已经签到过了" if not result["ok"] else f"✅ 签到成功!奖励 +{result['bonus']} 分,连续 {result['streak']} 天" elif action == "lottery": me = await context.bot.get_me() url = f"https://t.me/{me.username}?start=lottery_{query.message.chat_id}" keyboard = InlineKeyboardMarkup([[InlineKeyboardButton("去私聊设置抽奖", url=url)]]) await query.edit_message_text( "🎁 发起群内抽奖\n" "请到 Bot 私聊界面设置奖品、参与条件和结束条件。\n\n" f"👉 点击进入:{url}\n\n" "⏱️ 60 秒无操作后自动消失。", reply_markup=keyboard, disable_web_page_preview=True, ) schedule_panel_idle_delete(context, query.message) return elif action == "shop": summary = await get_scores_summary(user.id, query.message.chat_id) text = f"🛒 积分商城\n当前总积分:{summary['total']} 分\n" + "\n".join([f"{v['item_key']}. {v['name']} — {v['cost']} 分" for v in await list_shop_items(query.message.chat_id)]) + "\n\n购买请发送 /buy 商品编号" elif action == "me": summary = await get_scores_summary(user.id, query.message.chat_id) text = f"👤 我的积分\n总积分:{summary['total']} 分\n" + "\n".join([f"{s.game_type}: {s.score} 分" for s in summary['scores']]) else: text = "📖 常用:/me /checkin /trivia /lottery /shop /panel" await query.edit_message_text( text + "\n\n⏱️ 60 秒无操作后自动消失。", reply_markup=panel_keyboard(), ) schedule_panel_idle_delete(context, query.message) async def handle_trivia_answer(update: Update, context: ContextTypes.DEFAULT_TYPE): """处理 /trivia 后的用户答题。""" msg = update.effective_message user = update.effective_user if not msg or not msg.text or not user or user.is_bot: return correct_answer = context.user_data.get("trivia_answer") options = context.user_data.get("trivia_options") or [] if not correct_answer: return raw_answer = msg.text.strip() answer = raw_answer if raw_answer.isdigit(): idx = int(raw_answer) - 1 if 0 <= idx < len(options): answer = options[idx] result = await check_trivia_answer(update.effective_chat.id, user.id, user.username or user.first_name or str(user.id), answer, correct_answer) context.user_data.pop("trivia_answer", None) context.user_data.pop("trivia_options", None) if result["correct"]: text = ( "✅ 回答正确!\n" f"🎯 正确答案:{correct_answer}\n" f"📊 当前积分:{result['score']}\n" f"🔥 连击:{result['streak']}" ) else: text = ( "❌ 回答错误\n" f"🎯 正确答案:{correct_answer}\n" f"📊 当前积分:{result['score']}" ) reply = await msg.reply_text(text) asyncio.create_task(auto_delete(msg, seconds=60)) await record_bot_message(reply) asyncio.create_task(auto_delete(reply, seconds=60)) raise ApplicationHandlerStop async def handle_lottery_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() parts = query.data.split(":") event_id = int(parts[-1]) user = query.from_user result = await join_lottery_event(event_id, user.id, user.username or "", user.first_name or "") if not result["ok"]: await query.answer(result["reason"], show_alert=True) return event = result["event"] if result.get("won"): remain = max(0, int(getattr(event, "remaining_count", 0) or 0)) text = ( "🎉 即开即中\n" "━━━━━━━━━━━━\n" f"🏷️ 标题:{event.title or event.prize}\n" f"🎯 奖品:{event.prize}\n" f"🏆 中奖用户:@{user.username or user.first_name or user.id}\n" f"🎁 剩余奖品:{remain}" ) if result.get("ended"): text += "\n✅ 奖品已抽完,活动结束" await query.edit_message_text(text) else: await query.edit_message_text(await build_lottery_text(event, result["count"]), reply_markup=lottery_keyboard(event.id)) await query.answer("恭喜中奖!", show_alert=True) elif result["should_draw"]: draw = await draw_lottery_event(event.id) if draw.get("ok"): winner = draw.get("winner") or {} winner_name = winner.get("username") or winner.get("first_name") or winner.get("user_id") text = ( "🎉 抽奖已开奖\n" "━━━━━━━━━━━━\n" f"🏷️ 标题:{event.title or event.prize}\n" f"🎯 奖品:{event.prize}\n" f"👥 参与人数:{draw['count']} 人\n" f"🏆 中奖用户:@{winner_name}" ) await query.edit_message_text(text) else: await query.edit_message_text(f"🎁 抽奖已结束\n❌ {draw.get('reason', '未开奖')}") else: await query.edit_message_text(await build_lottery_text(event, result["count"]), reply_markup=lottery_keyboard(event.id)) await query.answer(f"参与成功,当前 {result['count']} 人") async def cmd_lottery_start(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.type != "private" or not context.args: return arg = context.args[0] if not arg.startswith("lottery_"): return try: target_chat_id = int(arg.split("_", 1)[1]) except Exception: target_chat_id = config.TG_CHAT_ID context.user_data["awaiting_lottery_spec"] = True context.user_data["lottery_target_chat_id"] = target_chat_id reply = await update.message.reply_text( "🎁 发起群内抽奖\n" "请发送:奖品 | 参与条件 | 结束条件\n\n" "示例:月卡一张 | all | people:3\n" "参与条件:all / checkin / points:N\n" "结束条件:people:N / minutes:N" ) async def handle_private_lottery_text(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_chat.type != "private" or not context.user_data.get("awaiting_lottery_spec"): return try: target_chat_id = int(context.user_data.get("lottery_target_chat_id") or config.TG_CHAT_ID) event = await create_lottery_from_text(update, context, update.message.text.strip(), publish_chat_id=target_chat_id) context.user_data.pop("awaiting_lottery_spec", None) context.user_data.pop("lottery_target_chat_id", None) await update.message.reply_text( "✅ 抽奖活动已发布到群里\n" f"🏷️ 标题:{event.title or event.prize}\n" f"🎯 奖品:{event.prize}\n" f"✅ 参与条件:{await lottery_condition_text(event)}\n" f"⏱️ 结束条件:{await lottery_end_text(event)}" ) except Exception as e: await update.message.reply_text( f"❌ 抽奖格式错误:{e}\n\n" "请重新发送:奖品 | 参与条件 | 结束条件\n" "示例:月卡一张 | all | people:3" ) async def auto_delete(message, seconds=60): try: await asyncio.sleep(seconds) await message.delete() except Exception: pass def register_fun(app): app.add_handler(CommandHandler("checkin", cmd_checkin, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("trivia", cmd_trivia, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("score", cmd_score, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("rank", cmd_rank, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("lottery", cmd_lottery)) app.add_handler(CommandHandler("shop", cmd_shop, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("buy", cmd_buy, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("panel", cmd_panel, filters.ChatType.GROUPS)) app.add_handler(CommandHandler("start", cmd_lottery_start, filters.ChatType.PRIVATE), group=1) app.add_handler(CallbackQueryHandler(handle_panel_callback, pattern=r"^panel:")) app.add_handler(CallbackQueryHandler(handle_lottery_callback, pattern=r"^lottery:")) app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, handle_private_lottery_text), group=1) app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS & ~filters.COMMAND, handle_trivia_answer), group=-1) app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.GROUPS, handle_trigger), group=2)