import { Bot, session } from 'grammy' import { conversations, createConversation } from '@grammyjs/conversations' import type { BotContext, SessionData } from './types.js' import { onboardingConversation } from './conversations/onboarding.js' import { createUser, findUserByTelegramId, getActiveQuestForUser } from '../api/_lib/db.js' import { t, getLang } from './i18n/index.js' // Helper: build reply_markup for opening the Mini App // Telegram requires HTTPS for both web_app and url buttons. // In dev (HTTP), we skip the button entirely and append the link to the message text. function questMarkup(label: string): { inline_keyboard: Record[][] } | undefined { const appUrl = process.env.APP_URL || 'https://guidly.example.com' if (appUrl.startsWith('https://')) { return { inline_keyboard: [[ { text: label, web_app: { url: appUrl } } ]] } } return undefined } function appendAppLink(text: string): string { const appUrl = process.env.APP_URL || 'https://guidly.example.com' if (!appUrl.startsWith('https://')) { return text + `\n\nšŸ”— ${appUrl}` } return text } export function createBot(token: string) { const bot = new Bot(token) // Error handler — log but don't crash bot.catch((err) => { console.error('Bot error:', err.message) }) // Session middleware (in-memory for now, can switch to DB later) bot.use(session({ initial: (): SessionData => ({ conversationState: 'idle', }), })) // Conversations plugin bot.use(conversations()) bot.use(createConversation(onboardingConversation)) // /start command bot.command('start', async (ctx) => { const from = ctx.from if (!from) return const lang = getLang(from.language_code) // Upsert user in DB const user = await createUser({ telegram_id: from.id, username: from.username, display_name: [from.first_name, from.last_name].filter(Boolean).join(' '), language_code: from.language_code || 'ru', }) ctx.session.userId = user.id // Check if user has an active quest const activeQuest = await getActiveQuestForUser(user.id) if (activeQuest) { // Resume active quest await ctx.reply(appendAppLink(t(lang, 'welcome_back', { city: activeQuest.city_name })), { reply_markup: questMarkup(t(lang, 'open_quest')), }) return } // New user or no active quest — start onboarding await ctx.reply(t(lang, 'welcome')) await ctx.conversation.enter('onboardingConversation') }) // /help command bot.command('help', async (ctx) => { const lang = getLang(ctx.from?.language_code) await ctx.reply(t(lang, 'help')) }) // /status command bot.command('status', async (ctx) => { const from = ctx.from if (!from) return const lang = getLang(from.language_code) const user = await findUserByTelegramId(from.id) if (!user) { await ctx.reply(t(lang, 'no_profile')) return } const quest = await getActiveQuestForUser(user.id) if (!quest) { await ctx.reply(t(lang, 'no_active_quest')) return } await ctx.reply(t(lang, 'quest_status', { title: quest.title || quest.city_name, city: quest.city_name, status: quest.status, })) }) // /stop command — terminate quest early bot.command('stop', async (ctx) => { const lang = getLang(ctx.from?.language_code) await ctx.reply(t(lang, 'stop_confirm'), { reply_markup: { inline_keyboard: [ [ { text: t(lang, 'yes_stop'), callback_data: 'quest_stop_confirm' }, { text: t(lang, 'no_continue'), callback_data: 'quest_stop_cancel' }, ], ], }, }) }) // Callback: quest stop confirm bot.callbackQuery('quest_stop_confirm', async (ctx) => { const from = ctx.from const lang = getLang(from.language_code) const user = await findUserByTelegramId(from.id) if (user) { const quest = await getActiveQuestForUser(user.id) if (quest) { // Import dynamically to avoid circular deps const { updateQuestStatus } = await import('../api/_lib/db.js') await updateQuestStatus(quest.id, 'completed') await ctx.editMessageText(t(lang, 'quest_stopped')) return } } await ctx.editMessageText(t(lang, 'no_active_quest')) }) bot.callbackQuery('quest_stop_cancel', async (ctx) => { const lang = getLang(ctx.from.language_code) await ctx.editMessageText(t(lang, 'quest_continue')) }) // Handle any text when no conversation is active bot.on('message:text', async (ctx) => { const lang = getLang(ctx.from?.language_code) const user = await findUserByTelegramId(ctx.from.id) if (!user) { await ctx.reply(t(lang, 'use_start')) return } const quest = await getActiveQuestForUser(user.id) if (quest) { await ctx.reply(appendAppLink(t(lang, 'quest_in_progress')), { reply_markup: questMarkup(t(lang, 'open_quest')), }) } else { await ctx.reply(t(lang, 'no_active_quest_start')) } }) return bot }