import type { Conversation } from '@grammyjs/conversations' import type { BotContext } from '../types.js' type BotConversation = Conversation import { getOrCreateCityByName, findUserByTelegramId } from '../../api/_lib/db.js' import { t, getLang } from '../i18n/index.js' import { createQuestFromOnboarding } from '../services/quest.service.js' const COMPANIONS_MAP: Record = { solo: 'solo', couple: 'couple', family: 'family', friends: 'friends', } const PACE_MAP: Record = { slow: 'slow', normal: 'normal', active: 'active', } async function sendTelegramMessage( chatId: number, text: string, extra?: Record ) { const token = process.env.TELEGRAM_BOT_TOKEN if (!token) throw new Error('TELEGRAM_BOT_TOKEN is not set') const response = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, ...extra, }), }) if (!response.ok) { const body = await response.text() throw new Error(`Telegram sendMessage failed (${response.status}): ${body}`) } } export async function onboardingConversation(conversation: BotConversation, ctx: BotContext) { const lang = getLang(ctx.from?.language_code) // Step 1: City input await ctx.reply(t(lang, 'choose_city')) let cityName = '' while (true) { const cityResponse = await conversation.waitFor('message:text') const input = cityResponse.message.text.trim().replace(/\s+/g, ' ') if (input.length >= 2 && !input.startsWith('/')) { cityName = input break } await ctx.reply(t(lang, 'invalid_city')) } const city = await conversation.external(() => getOrCreateCityByName(cityName)) const cityId = city.id cityName = city.name // Step 2: Number of days await ctx.reply(t(lang, 'choose_days')) let days: number = 0 while (true) { const daysResponse = await conversation.waitFor('message:text') const parsed = parseInt(daysResponse.message.text) if (parsed >= 1 && parsed <= 30) { days = parsed break } await ctx.reply(t(lang, 'invalid_days')) } // Step 3: Companions await ctx.reply(t(lang, 'choose_companions'), { reply_markup: { inline_keyboard: [ [ { text: t(lang, 'companions_solo'), callback_data: 'comp_solo' }, { text: t(lang, 'companions_couple'), callback_data: 'comp_couple' }, ], [ { text: t(lang, 'companions_family'), callback_data: 'comp_family' }, { text: t(lang, 'companions_friends'), callback_data: 'comp_friends' }, ], ], }, }) const compResponse = await conversation.waitForCallbackQuery(/^comp_/) const companions = COMPANIONS_MAP[compResponse.callbackQuery!.data!.replace('comp_', '')] || 'solo' await compResponse.answerCallbackQuery() // Step 4: Pace await ctx.reply(t(lang, 'choose_pace'), { reply_markup: { inline_keyboard: [ [{ text: t(lang, 'pace_slow'), callback_data: 'pace_slow' }], [{ text: t(lang, 'pace_normal'), callback_data: 'pace_normal' }], [{ text: t(lang, 'pace_active'), callback_data: 'pace_active' }], ], }, }) const paceResponse = await conversation.waitForCallbackQuery(/^pace_/) const pace = PACE_MAP[paceResponse.callbackQuery!.data!.replace('pace_', '')] || 'normal' await paceResponse.answerCallbackQuery() // Step 5: Wishes (optional) await ctx.reply(t(lang, 'add_wishes'), { reply_markup: { inline_keyboard: [ [{ text: t(lang, 'skip'), callback_data: 'wishes_skip' }], ], }, }) let userComment: string | undefined // Wait for either text or callback const wishesCtx = await conversation.wait() if (wishesCtx.callbackQuery?.data === 'wishes_skip') { await wishesCtx.answerCallbackQuery() userComment = undefined } else if (wishesCtx.message?.text) { userComment = wishesCtx.message.text } // Step 6: Confirmation const companionsLabel = t(lang, `companions_${companions}`) const paceLabel = t(lang, `pace_${pace}`) const wishesLine = userComment ? t(lang, 'wishes_line', { comment: userComment }) : '' await ctx.reply( t(lang, 'onboarding_summary', { city: cityName, days: days, companions: companionsLabel, pace: paceLabel, wishes: wishesLine, }), { reply_markup: { inline_keyboard: [ [ { text: t(lang, 'confirm_yes'), callback_data: 'onboarding_confirm' }, { text: t(lang, 'confirm_restart'), callback_data: 'onboarding_restart' }, ], ], }, } ) const confirmResponse = await conversation.waitForCallbackQuery(/^onboarding_/) await confirmResponse.answerCallbackQuery() if (confirmResponse.callbackQuery!.data === 'onboarding_restart') { // Restart the conversation await ctx.reply(t(lang, 'welcome')) return } // Step 7: Create quest await ctx.reply(t(lang, 'creating_quest')) const userId = await conversation.external(async () => { const user = await findUserByTelegramId(ctx.from!.id) return user?.id }) if (!userId) { await ctx.reply(t(lang, 'use_start')) return } const chatId = ctx.chat?.id if (!chatId) { await ctx.reply('Something went wrong. Please try /start again.') return } // Run heavy generation in background so webhook handlers can return quickly. void (async () => { try { const result = await createQuestFromOnboarding({ userId, cityId, cityName, days, companions, pace, userComment, lang, }) if (!result) { await sendTelegramMessage(chatId, 'Something went wrong. Please try /start again.') return } const appUrl = process.env.APP_URL || 'https://guidly.example.com' const isHttps = appUrl.startsWith('https://') const questText = t(lang, 'quest_created', { title: result.title, description: result.description, }) + (isHttps ? '' : `\n\nšŸ”— ${appUrl}`) await sendTelegramMessage(chatId, questText, isHttps ? { reply_markup: { inline_keyboard: [[{ text: t(lang, 'open_quest'), web_app: { url: appUrl } }]], }, } : {}) } catch (error) { console.error('Background quest creation failed:', error) try { await sendTelegramMessage(chatId, 'Something went wrong. Please try /start again.') } catch (sendError) { console.error('Failed to send fallback error message:', sendError) } } })() // Conversation finishes immediately; final quest message will be sent by background task. return }