173 lines
6.0 KiB
Python
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) |