import { answerCallbackQuery, editMessageText } from '../../telegram'; import { UserService } from '../../services/user-service'; import { executeDomainRegister } from '../../domain-register'; import type { Env, TelegramUpdate } from '../../types'; /** * 도메인 형식 검증 정규식 * - 최소 2글자 이상 * - 숫자/문자로 시작, 숫자/문자로 끝 * - 중간에 하이픈, 점 허용 * - TLD 2글자 이상 */ const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,251}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/; /** * Callback Query 처리 (인라인 버튼 클릭) */ export async function handleCallbackQuery( env: Env, callbackQuery: TelegramUpdate['callback_query'] ): Promise { if (!callbackQuery) return; const { id: queryId, from, message, data } = callbackQuery; if (!data || !message) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 요청입니다.' }); return; } const chatId = message.chat.id; const messageId = message.message_id; const telegramUserId = from.id.toString(); const userService = new UserService(env.DB); const user = await userService.getUserByTelegramId(telegramUserId); if (!user) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '사용자를 찾을 수 없습니다.' }); return; } // 도메인 등록 처리 if (data.startsWith('domain_reg:')) { const parts = data.split(':'); if (parts.length !== 3) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' }); return; } const domain = parts[1]; const priceStr = parts[2]; // 도메인 형식 검증 if (!domain || domain.length > 253 || !DOMAIN_REGEX.test(domain)) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 도메인 형식입니다.' }); return; } const price = parseInt(priceStr, 10); if (isNaN(price) || price < 0 || price > 10000000) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 가격 정보입니다.' }); return; } // SECURITY NOTE: Price from callback_data is range-validated here (0-10M) // but real-time price verification happens in executeDomainRegister() // which fetches current price from Namecheap API before charging await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '등록 처리 중...' }); await editMessageText( env.BOT_TOKEN, chatId, messageId, `⏳ ${domain} 등록 처리 중...` ); const result = await executeDomainRegister(env, user.id, telegramUserId, domain, price); if (result.success) { const expiresInfo = result.expiresAt ? `\n• 만료일: ${result.expiresAt}` : ''; const nsInfo = result.nameservers && result.nameservers.length > 0 ? `\n\n🌐 현재 네임서버:\n${result.nameservers.map(ns => `• ${ns}`).join('\n')}` : ''; await editMessageText( env.BOT_TOKEN, chatId, messageId, `✅ 도메인 등록 완료! • 도메인: ${result.domain} • 결제 금액: ${result.price?.toLocaleString()}원 • 현재 잔액: ${result.newBalance?.toLocaleString()}원${expiresInfo}${nsInfo} 🎉 축하합니다! 도메인이 성공적으로 등록되었습니다. 네임서버 변경이 필요하면 말씀해주세요.` ); } else { await editMessageText( env.BOT_TOKEN, chatId, messageId, `❌ 등록 실패 ${result.error} 다시 시도하시려면 도메인 등록을 요청해주세요.` ); } return; } // 도메인 등록 취소 if (data === 'domain_cancel') { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' }); await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 도메인 등록이 취소되었습니다.' ); return; } // 서버 주문 확인 if (data.startsWith('server_order:')) { const parts = data.split(':'); if (parts.length !== 3) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' }); return; } const callbackUserId = parts[1]; const index = parseInt(parts[2], 10); // SECURITY: Verify callback userId matches the actual user if (callbackUserId !== telegramUserId) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '⚠️ 권한이 없습니다.', show_alert: true }); return; } if (isNaN(index) || index < 0 || index > 2) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 선택입니다.' }); return; } await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '처리 중...' }); await editMessageText( env.BOT_TOKEN, chatId, messageId, '⏳ 서버 주문 처리 중...' ); // 세션 조회 const { getServerSession, deleteServerSession } = await import('../../server-agent'); if (!env.DB) { await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 세션 저장소가 설정되지 않았습니다.' ); return; } // Use verified telegramUserId instead of callback userId const session = await getServerSession(env.DB, telegramUserId); if (!session || !session.lastRecommendation) { await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 세션이 만료되었습니다.\n다시 "서버 추천"을 시작해주세요.' ); return; } const selected = session.lastRecommendation.recommendations[index]; if (!selected) { await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 선택한 서버를 찾을 수 없습니다.' ); await deleteServerSession(env.DB, telegramUserId); return; } // 잔액 확인 const deposit = await env.DB.prepare( 'SELECT balance FROM user_deposits WHERE user_id = ?' ).bind(user.id).first<{ balance: number }>(); const price = selected.price?.monthly_krw || 0; if (!deposit || deposit.balance < price) { await editMessageText( env.BOT_TOKEN, chatId, messageId, `❌ 잔액이 부족합니다. • 서버 가격: ${price.toLocaleString()}원/월 • 현재 잔액: ${(deposit?.balance || 0).toLocaleString()}원 • 부족 금액: ${(price - (deposit?.balance || 0)).toLocaleString()}원 잔액을 충전 후 다시 시도해주세요.` ); return; } // Queue 확인 if (!env.SERVER_PROVISION_QUEUE) { await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 서버 프로비저닝 시스템이 준비되지 않았습니다.' ); return; } // 주문 생성 (DB INSERT) const { createServerOrder, sendProvisionMessage } = await import('../../server-provision'); const orderId = await createServerOrder( env.DB, user.id, telegramUserId, selected.pricing_id, selected.region.code, 'anvil', price, `${selected.plan_name} - ${session.collectedInfo?.useCase || 'server'}` ); // Queue에 메시지 전송 await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, user.id, telegramUserId); // 즉시 응답 await editMessageText( env.BOT_TOKEN, chatId, messageId, `📋 서버 주문 접수 완료! (주문 #${orderId}) • 서버: ${selected.plan_name} • 리전: ${selected.region.name} (${selected.region.code}) • 가격: ${price.toLocaleString()}원/월 ⏳ 서버를 생성하고 있습니다... (1-2분 소요) 완료되면 메시지로 알려드릴게요.` ); // 세션 삭제 await deleteServerSession(env.DB, telegramUserId); return; } // 서버 주문 취소 if (data.startsWith('server_cancel:')) { const parts = data.split(':'); if (parts.length !== 2) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' }); return; } const callbackUserId = parts[1]; // SECURITY: Verify callback userId matches the actual user if (callbackUserId !== telegramUserId) { await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '⚠️ 권한이 없습니다.', show_alert: true }); return; } await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' }); await editMessageText( env.BOT_TOKEN, chatId, messageId, '❌ 서버 신청이 취소되었습니다.' ); // 세션 삭제 const { deleteServerSession } = await import('../../server-agent'); if (env.DB) { await deleteServerSession(env.DB, telegramUserId); } return; } // Note: server_delete callback handler removed - now using text-based confirmation // 알 수 없는 callback data await answerCallbackQuery(env.BOT_TOKEN, queryId); }