Compare commits

..

No commits in common. "main" and "develop" have entirely different histories.

4 changed files with 44 additions and 106 deletions

View File

@ -57,24 +57,6 @@ export async function getCityById(id: string) {
return rows[0] || null return rows[0] || null
} }
export async function getOrCreateCityByName(name: string) {
const normalizedName = name.trim().replace(/\s+/g, ' ')
const { rows: existing } = await pool.query(
'SELECT * FROM cities WHERE lower(name) = lower($1) LIMIT 1',
[normalizedName]
)
if (existing[0]) return existing[0]
const { rows } = await pool.query(
`INSERT INTO cities (name, country, description, status)
VALUES ($1, $2, $3, 'active')
RETURNING *`,
[normalizedName, 'Unknown', 'User-added city']
)
return rows[0]
}
// ============================================ // ============================================
// Quests // Quests
// ============================================ // ============================================

View File

@ -2,7 +2,7 @@ import type { Conversation } from '@grammyjs/conversations'
import type { BotContext } from '../types.js' import type { BotContext } from '../types.js'
type BotConversation = Conversation<BotContext, BotContext> type BotConversation = Conversation<BotContext, BotContext>
import { getOrCreateCityByName, findUserByTelegramId } from '../../api/_lib/db.js' import { getActiveCities, findUserByTelegramId } from '../../api/_lib/db.js'
import { t, getLang } from '../i18n/index.js' import { t, getLang } from '../i18n/index.js'
import { createQuestFromOnboarding } from '../services/quest.service.js' import { createQuestFromOnboarding } from '../services/quest.service.js'
@ -19,50 +19,24 @@ const PACE_MAP: Record<string, string> = {
active: 'active', active: 'active',
} }
async function sendTelegramMessage(
chatId: number,
text: string,
extra?: Record<string, unknown>
) {
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) { export async function onboardingConversation(conversation: BotConversation, ctx: BotContext) {
const lang = getLang(ctx.from?.language_code) const lang = getLang(ctx.from?.language_code)
// Step 1: City input // Step 1: City selection
await ctx.reply(t(lang, 'choose_city')) const cities = await conversation.external(() => getActiveCities())
let cityName = '' const cityButtons = cities.map((city: { id: string; name: string }) => ([
while (true) { { text: city.name, callback_data: `city_${city.id}` }
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)) await ctx.reply(t(lang, 'choose_city'), {
const cityId = city.id reply_markup: { inline_keyboard: cityButtons },
cityName = city.name })
const cityResponse = await conversation.waitForCallbackQuery(/^city_/)
const cityId = cityResponse.callbackQuery!.data!.replace('city_', '')
const cityName = cities.find((c: { id: string }) => c.id === cityId)?.name || 'Unknown'
await cityResponse.answerCallbackQuery()
// Step 2: Number of days // Step 2: Number of days
await ctx.reply(t(lang, 'choose_days')) await ctx.reply(t(lang, 'choose_days'))
@ -181,16 +155,8 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
return return
} }
const chatId = ctx.chat?.id const result = await conversation.external(() =>
if (!chatId) { createQuestFromOnboarding({
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, userId,
cityId, cityId,
cityName, cityName,
@ -200,9 +166,10 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
userComment, userComment,
lang, lang,
}) })
)
if (!result) { if (!result) {
await sendTelegramMessage(chatId, 'Something went wrong. Please try /start again.') await ctx.reply('Something went wrong. Please try /start again.')
return return
} }
@ -213,21 +180,12 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
description: result.description, description: result.description,
}) + (isHttps ? '' : `\n\n🔗 ${appUrl}`) }) + (isHttps ? '' : `\n\n🔗 ${appUrl}`)
await sendTelegramMessage(chatId, questText, isHttps ? { await ctx.reply(
questText,
isHttps ? {
reply_markup: { reply_markup: {
inline_keyboard: [[ { text: t(lang, 'open_quest'), web_app: { url: appUrl } } ]], 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
} }

View File

@ -9,8 +9,7 @@ Let's begin! Where are you headed?`,
welcome_back: `Welcome back! You have an active quest in {city}. Open the app to continue.`, welcome_back: `Welcome back! You have an active quest in {city}. Open the app to continue.`,
// Onboarding // Onboarding
choose_city: 'Which city are you planning your quest in? Type the city name.', choose_city: 'Choose a city for your adventure:',
invalid_city: 'Please enter a valid city name (at least 2 characters).',
choose_days: 'How many days are you planning? (1-30)', choose_days: 'How many days are you planning? (1-30)',
invalid_days: 'Please enter a number from 1 to 30.', invalid_days: 'Please enter a number from 1 to 30.',
choose_companions: 'Who are you traveling with?', choose_companions: 'Who are you traveling with?',

View File

@ -9,8 +9,7 @@ export const ru: Record<string, string> = {
welcome_back: `С возвращением! У тебя есть активный квест по городу {city}. Открой приложение, чтобы продолжить.`, welcome_back: `С возвращением! У тебя есть активный квест по городу {city}. Открой приложение, чтобы продолжить.`,
// Onboarding // Onboarding
choose_city: 'В каком городе планируешь квест? Напиши название города.', choose_city: 'Выбери город для приключения:',
invalid_city: 'Введи корректное название города (минимум 2 символа).',
choose_days: 'На сколько дней планируешь поездку? (1-30)', choose_days: 'На сколько дней планируешь поездку? (1-30)',
invalid_days: 'Введи число от 1 до 30.', invalid_days: 'Введи число от 1 до 30.',
choose_companions: 'С кем путешествуешь?', choose_companions: 'С кем путешествуешь?',