guidly/bot/services/event-processor.ts
laruevin d5ed7fdcf9 Initial commit: Guidly project with CI/CD pipeline
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>
2026-02-11 11:42:42 +07:00

210 lines
5.8 KiB
TypeScript

import type { Bot } from 'grammy'
import type { BotContext } from '../types.js'
import {
getUnprocessedEvents, markEventProcessed,
getPointById, findUserByTelegramId,
getActiveQuestForUser, getActiveDayForQuest,
updatePointContent,
} from '../../api/_lib/db.js'
import { aiService } from './ai.service.js'
import { generateNextPoint, generatePointContent } from './quest.service.js'
import { t, getLang } from '../i18n/index.js'
let processorInterval: ReturnType<typeof setInterval> | null = null
export function startEventProcessor(bot: Bot<BotContext>) {
// Poll every 3 seconds for unprocessed events
processorInterval = setInterval(async () => {
try {
await processEvents(bot)
} catch (error) {
console.error('Event processor error:', error)
}
}, 3000)
console.log('Event processor started')
}
export function stopEventProcessor() {
if (processorInterval) {
clearInterval(processorInterval)
processorInterval = null
}
}
async function processEvents(bot: Bot<BotContext>) {
const events = await getUnprocessedEvents()
for (const event of events) {
try {
await processEvent(bot, event)
await markEventProcessed(event.id)
} catch (error) {
console.error(`Failed to process event ${event.id}:`, error)
// Still mark as processed to avoid infinite retry
await markEventProcessed(event.id)
}
}
}
async function processEvent(bot: Bot<BotContext>, event: any) {
// Look up the user to get their telegram_id for sending messages
const { pool } = await import('../../api/_lib/db.js')
const { rows: users } = await pool.query('SELECT * FROM users WHERE id = $1', [event.user_id])
const user = users[0]
if (!user) return
const lang = getLang(user.language_code)
const quest = await getActiveQuestForUser(user.id)
if (!quest) return
const point = event.point_id ? await getPointById(event.point_id) : null
switch (event.event_type) {
case 'arrived': {
if (point && !point.content_text) {
// Generate full content for the point
const content = await generatePointContent(point.id, {
city: quest.city_name,
pointTitle: point.title,
dayNumber: 1, // TODO: get actual day number
companions: quest.companions,
pace: quest.pace,
lang,
})
if (content) {
// Send a reaction to the chat
const reaction = await aiService.generateReaction({
eventType: 'arrived',
pointTitle: point.title,
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
await bot.api.sendMessage(user.telegram_id, reaction)
}
}
}
break
}
case 'point_completed': {
// Generate next point
const day = await getActiveDayForQuest(quest.id)
if (!day) break
const result = await generateNextPoint(quest.id, {
city: quest.city_name,
dayNumber: day.day_number,
totalDays: quest.number_of_days,
dayTheme: day.theme || '',
pace: quest.pace,
companions: quest.companions,
userComment: quest.user_comment,
lang,
})
const reaction = await aiService.generateReaction({
eventType: 'point_completed',
pointTitle: point?.title,
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
const appUrl = process.env.APP_URL || 'https://guidly.example.com'
const isHttps = appUrl.startsWith('https://')
const msgText = isHttps ? reaction : reaction + `\n\n🔗 ${appUrl}`
await bot.api.sendMessage(user.telegram_id, msgText, isHttps ? {
reply_markup: {
inline_keyboard: [[ { text: t(lang, 'open_quest'), web_app: { url: appUrl } } ]],
},
} : {})
}
break
}
case 'skipped': {
const reaction = await aiService.generateReaction({
eventType: 'skipped',
pointTitle: point?.title,
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
await bot.api.sendMessage(user.telegram_id, reaction)
}
// Generate next point
const day = await getActiveDayForQuest(quest.id)
if (day) {
await generateNextPoint(quest.id, {
city: quest.city_name,
dayNumber: day.day_number,
totalDays: quest.number_of_days,
dayTheme: day.theme || '',
pace: quest.pace,
companions: quest.companions,
userComment: quest.user_comment,
lang,
})
}
break
}
case 'finished_today': {
const reaction = await aiService.generateReaction({
eventType: 'finished_today',
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
await bot.api.sendMessage(user.telegram_id, reaction)
}
break
}
case 'day_completed': {
const reaction = await aiService.generateReaction({
eventType: 'day_completed',
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
await bot.api.sendMessage(user.telegram_id, reaction)
}
break
}
case 'quest_completed': {
const reaction = await aiService.generateReaction({
eventType: 'quest_completed',
city: quest.city_name,
lang,
companions: quest.companions,
pace: quest.pace,
})
if (reaction) {
await bot.api.sendMessage(user.telegram_id, reaction)
}
break
}
}
}