- Replace `any` types with proper interfaces (QuestDetailResponse, etc.) - Remove unused imports (createUser, findUserByTelegramId, updatePointContent) - Use `unknown` in catch blocks with instanceof narrowing - Type Express handlers with Request/Response/NextFunction - Add argsIgnorePattern for underscore-prefixed params in ESLint config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
209 lines
5.7 KiB
TypeScript
209 lines
5.7 KiB
TypeScript
import type { Bot } from 'grammy'
|
|
import type { BotContext } from '../types.js'
|
|
import {
|
|
getUnprocessedEvents, markEventProcessed,
|
|
getPointById,
|
|
getActiveQuestForUser, getActiveDayForQuest,
|
|
} 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: Record<string, unknown>) {
|
|
// 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
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|