Telegram Bot + Mini App for city walking quests. - React 19 + TypeScript + Vite 6 frontend - Express 5 + PostgreSQL backend - grammY Telegram bot with DeepSeek AI - GitLab CI/CD: lint, build, deploy to production Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import type { ApiRequest, ApiResponse } from '../_lib/types.js'
|
|
import { withInternalAuth } from '../_lib/auth.js'
|
|
import {
|
|
createQuest, createQuestDay, createQuestPoint,
|
|
updateQuestStatus, updateDayStatus, updatePointStatus,
|
|
updatePointContent, markPointArrived,
|
|
getActiveQuestForUser, getActiveDayForQuest,
|
|
getPointsForDay, getFullQuestState,
|
|
saveChatMessage, createAchievement, getUserAchievements,
|
|
findUserByTelegramId,
|
|
} from '../_lib/db.js'
|
|
|
|
async function handler(req: ApiRequest, res: ApiResponse) {
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ error: 'Method not allowed' })
|
|
}
|
|
|
|
const { action, data } = req.body
|
|
|
|
try {
|
|
switch (action) {
|
|
case 'create_quest': {
|
|
const quest = await createQuest({
|
|
city_id: data.city_id,
|
|
user_id: data.user_id,
|
|
title: data.title,
|
|
description: data.description,
|
|
number_of_days: data.number_of_days,
|
|
pace: data.pace,
|
|
companions: data.companions,
|
|
user_comment: data.user_comment,
|
|
})
|
|
|
|
// Create day skeletons
|
|
const days = []
|
|
for (let i = 1; i <= data.number_of_days; i++) {
|
|
const day = await createQuestDay({
|
|
quest_id: quest.id,
|
|
day_number: i,
|
|
theme: data.day_themes?.[i - 1] || null,
|
|
})
|
|
days.push(day)
|
|
}
|
|
|
|
return res.json({ quest, days })
|
|
}
|
|
|
|
case 'generate_step': {
|
|
const point = await createQuestPoint({
|
|
day_id: data.day_id,
|
|
title: data.title,
|
|
location_lat: data.location_lat,
|
|
location_lon: data.location_lon,
|
|
teaser_text: data.teaser_text,
|
|
order_in_day: data.order_in_day,
|
|
status: data.status || 'active',
|
|
})
|
|
return res.json({ point })
|
|
}
|
|
|
|
case 'update_point_content': {
|
|
const point = await updatePointContent(data.point_id, data.content_text)
|
|
return res.json({ point })
|
|
}
|
|
|
|
case 'update_state': {
|
|
let result = null
|
|
if (data.entity === 'quest') {
|
|
result = await updateQuestStatus(data.id, data.status)
|
|
} else if (data.entity === 'day') {
|
|
result = await updateDayStatus(data.id, data.status)
|
|
} else if (data.entity === 'point') {
|
|
result = await updatePointStatus(data.id, data.status)
|
|
if (data.status === 'active' && data.arrived) {
|
|
await markPointArrived(data.id)
|
|
}
|
|
}
|
|
return res.json({ result })
|
|
}
|
|
|
|
case 'get_user_state': {
|
|
const user = await findUserByTelegramId(data.telegram_id)
|
|
if (!user) {
|
|
return res.json({ state: null })
|
|
}
|
|
const state = await getFullQuestState(user.id)
|
|
return res.json({ state })
|
|
}
|
|
|
|
case 'save_chat_message': {
|
|
const message = await saveChatMessage({
|
|
user_id: data.user_id,
|
|
quest_id: data.quest_id,
|
|
point_id: data.point_id,
|
|
message_type: data.message_type,
|
|
content: data.content,
|
|
})
|
|
return res.json({ message })
|
|
}
|
|
|
|
case 'check_achievements': {
|
|
const existing = await getUserAchievements(data.user_id)
|
|
const newAchievements = []
|
|
|
|
// First quest completed
|
|
if (!existing.find((a: any) => a.type === 'first_quest')) {
|
|
const ach = await createAchievement({
|
|
user_id: data.user_id,
|
|
type: 'first_quest',
|
|
city_id: data.city_id,
|
|
})
|
|
if (ach) newAchievements.push(ach)
|
|
}
|
|
|
|
return res.json({ achievements: newAchievements })
|
|
}
|
|
|
|
default:
|
|
return res.status(400).json({ error: `Unknown action: ${action}` })
|
|
}
|
|
} catch (error: any) {
|
|
console.error(`Internal API error (${action}):`, error)
|
|
return res.status(500).json({ error: error.message })
|
|
}
|
|
}
|
|
|
|
export default withInternalAuth(handler)
|