Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910d6cf2b2 | ||
|
|
8033377cdf | ||
|
|
a8cab41a00 |
@ -57,6 +57,24 @@ 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
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@ -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 { getActiveCities, findUserByTelegramId } from '../../api/_lib/db.js'
|
import { getOrCreateCityByName, 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,24 +19,50 @@ 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 selection
|
// Step 1: City input
|
||||||
const cities = await conversation.external(() => getActiveCities())
|
await ctx.reply(t(lang, 'choose_city'))
|
||||||
|
|
||||||
const cityButtons = cities.map((city: { id: string; name: string }) => ([
|
let cityName = ''
|
||||||
{ text: city.name, callback_data: `city_${city.id}` }
|
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'))
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.reply(t(lang, 'choose_city'), {
|
const city = await conversation.external(() => getOrCreateCityByName(cityName))
|
||||||
reply_markup: { inline_keyboard: cityButtons },
|
const cityId = city.id
|
||||||
})
|
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'))
|
||||||
@ -155,8 +181,16 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await conversation.external(() =>
|
const chatId = ctx.chat?.id
|
||||||
createQuestFromOnboarding({
|
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,
|
userId,
|
||||||
cityId,
|
cityId,
|
||||||
cityName,
|
cityName,
|
||||||
@ -166,10 +200,9 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
|
|||||||
userComment,
|
userComment,
|
||||||
lang,
|
lang,
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
await ctx.reply('Something went wrong. Please try /start again.')
|
await sendTelegramMessage(chatId, 'Something went wrong. Please try /start again.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,12 +213,21 @@ export async function onboardingConversation(conversation: BotConversation, ctx:
|
|||||||
description: result.description,
|
description: result.description,
|
||||||
}) + (isHttps ? '' : `\n\n🔗 ${appUrl}`)
|
}) + (isHttps ? '' : `\n\n🔗 ${appUrl}`)
|
||||||
|
|
||||||
await ctx.reply(
|
await sendTelegramMessage(chatId, questText, isHttps ? {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,8 @@ 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: 'Choose a city for your adventure:',
|
choose_city: 'Which city are you planning your quest in? Type the city name.',
|
||||||
|
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?',
|
||||||
|
|||||||
@ -9,7 +9,8 @@ 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: 'С кем путешествуешь?',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user