""" 텔레그램 봇 폴링 서비스 - 백그라운드에서 텔레그램 봇의 업데이트를 폴링 - 인라인 버튼 클릭 처리 (가입 승인/거부) """ import logging from typing import Optional from telegram import Update from telegram.ext import Application, CallbackQueryHandler, ContextTypes from flask import Flask from backend.models.telegram_bot import TelegramBot from backend.models.user import User, db logger = logging.getLogger(__name__) async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ 텔레그램 인라인 버튼 클릭 처리 callback_data 형식: "approve_{token}" 또는 "reject_{token}" """ query = update.callback_query await query.answer() data = query.data or "" logger.info("Received callback: %s", data) # Flask app 객체는 bot_data에 저장해둔 것을 사용 flask_app: Optional[Flask] = context.application.bot_data.get("flask_app") if flask_app is None: logger.error("Flask app context is missing in bot_data") await query.edit_message_text( text="❌ 내부 설정 오류로 요청을 처리할 수 없습니다. 관리자에게 문의해 주세요." ) return # callback_data 형식 검증 if "_" not in data: logger.warning("Invalid callback data format: %s", data) await query.edit_message_text( text="❌ 유효하지 않은 요청입니다." ) return try: action, token = data.split("_", 1) except ValueError: logger.warning("Failed to split callback data: %s", data) await query.edit_message_text( text="❌ 유효하지 않은 요청입니다." ) return try: with flask_app.app_context(): # 토큰으로 사용자 찾기 user = User.query.filter_by(approval_token=token).first() if not user: await query.edit_message_text( text="❌ 유효하지 않은 승인 요청입니다.\n(이미 처리되었거나 만료된 요청)" ) return if action == "approve": # 승인 처리 user.is_approved = True user.is_active = True user.approval_token = None # 토큰 무효화 db.session.commit() await query.edit_message_text( text=( "✅ 승인 완료!\n\n" f"👤 사용자: {user.username}\n" f"📧 이메일: {user.email}\n\n" "사용자가 이제 로그인할 수 있습니다." ) ) logger.info("User %s approved", user.username) elif action == "reject": # 거부 처리 - 사용자 삭제 username = user.username email = user.email db.session.delete(user) db.session.commit() await query.edit_message_text( text=( "❌ 가입 거부됨\n\n" f"👤 사용자: {username}\n" f"📧 이메일: {email}\n\n" "계정이 삭제되었습니다." ) ) logger.info("User %s rejected and deleted", username) else: logger.warning("Unknown action in callback: %s", action) await query.edit_message_text( text="❌ 유효하지 않은 요청입니다." ) except Exception as e: logger.exception("Error handling callback: %s", e) # 예외 내용은 사용자에게 직접 노출하지 않음 try: db.session.rollback() except Exception: logger.exception("DB rollback failed") await query.edit_message_text( text="❌ 요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도하거나 관리자에게 문의해 주세요." ) def run_polling(flask_app: Flask) -> None: """ 동기 함수: 백그라운드 스레드에서 직접 호출됨 Application.run_polling() 이 내부에서 asyncio 이벤트 루프를 관리하므로 여기서는 asyncio.run 을 사용하지 않는다. """ if flask_app is None: raise ValueError("flask_app is required for run_polling") # DB에서 활성 봇 조회 with flask_app.app_context(): bots = TelegramBot.query.filter_by(is_active=True).all() if not bots: logger.warning("No active bots found") return # 첫 번째 활성 봇만 사용 (여러 봇이 동시에 폴링하면 충돌 가능) bot = bots[0] flask_app.logger.info("Starting polling for bot: %s (ID: %s)", bot.name, bot.id) # Application 생성 application = Application.builder().token(bot.token).build() # Flask app을 bot_data에 넣어서 핸들러에서 사용할 수 있게 함 application.bot_data["flask_app"] = flask_app # 콜백 쿼리 핸들러 등록 application.add_handler(CallbackQueryHandler(handle_approval_callback)) try: # v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함 application.run_polling(drop_pending_updates=True) except Exception as e: flask_app.logger.exception("Error in bot polling: %s", e)