Files
iDRAC_Info/telegram_bot_service.py
2025-12-19 19:18:16 +09:00

173 lines
6.0 KiB
Python

"""
텔레그램 봇 폴링 서비스
- 백그라운드에서 텔레그램 봇의 업데이트를 폴링
- 인라인 버튼 클릭 처리 (가입 승인/거부)
"""
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")
bot_token: Optional[str] = None
bot_name: Optional[str] = None
bot_id: Optional[int] = None
# DB에서 활성 봇 조회
with flask_app.app_context():
bots = TelegramBot.query.filter_by(is_active=True).all()
if not bots:
logger.warning("No active bots found for polling service.")
return
if len(bots) > 1:
logger.warning("Multiple active bots found. Only the first one (%s) will be used.", bots[0].name)
# 첫 번째 활성 봇 사용
bot = bots[0]
# DB 세션 밖에서도 사용할 수 있도록 필요한 정보만 추출 (Detached Instance 에러 방지)
bot_token = bot.token
bot_name = bot.name
bot_id = bot.id
logger.info("Starting polling for bot: %s (ID: %s)", bot_name, bot_id)
if not bot_token:
logger.error("Bot token not found.")
return
# 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, stop_signals=[])
except Exception as e:
logger.exception("Error in bot polling: %s", e)