guidly/server/index.ts
laruevin cddf96b154 fix: resolve TypeScript build errors
- Use InlineKeyboardMarkup from grammy/types for proper typing
- Add QuestEvent interface for event processor
- Accept Promise<unknown> in wrapHandler for flexible return types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:30:56 +07:00

118 lines
3.6 KiB
TypeScript

import 'dotenv/config'
import express, { type Request, type Response, type NextFunction } from 'express'
import cookieParser from 'cookie-parser'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const app = express()
const PORT = process.env.PORT || 3000
// Middleware
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
// Wrap handler to catch errors and merge params
function wrapHandler(handler: (req: Request, res: Response) => Promise<unknown>) {
return async (req: Request, res: Response, next: NextFunction) => {
const mergedQuery = { ...req.query, ...req.params }
Object.defineProperty(req, 'query', {
value: mergedQuery,
writable: true,
configurable: true,
})
try {
await handler(req, res)
} catch (error) {
next(error)
}
}
}
async function setupRoutes() {
// Quest API (Mini App)
const questCurrent = (await import('../api/quest/current.js')).default
const questEvents = (await import('../api/quest/events.js')).default
const questHistory = (await import('../api/quest/history.js')).default
// Internal API (Bot)
const botInternal = (await import('../api/bot/internal.js')).default
// Quest routes (Mini App)
app.all('/api/quest/current', wrapHandler(questCurrent))
app.all('/api/quest/events', wrapHandler(questEvents))
app.all('/api/quest/history', wrapHandler(questHistory))
app.all('/api/quest/history/:id', wrapHandler(questHistory))
// Internal bot routes
app.all('/api/bot/internal', wrapHandler(botInternal))
// In production: start bot in webhook mode inside this server process
const isProduction = process.env.NODE_ENV === 'production' && process.env.BOT_WEBHOOK_URL
if (isProduction) {
const { webhookCallback } = await import('grammy')
const { createBot } = await import('../bot/bot.js')
const { startEventProcessor } = await import('../bot/services/event-processor.js')
const token = process.env.TELEGRAM_BOT_TOKEN
if (!token) {
console.error('TELEGRAM_BOT_TOKEN is not set!')
process.exit(1)
}
const bot = createBot(token)
await bot.init()
// Register grammY webhook handler
app.post('/api/telegram/webhook', webhookCallback(bot, 'express'))
// Set webhook with Telegram
await bot.api.setWebhook(process.env.BOT_WEBHOOK_URL!, {
secret_token: process.env.BOT_WEBHOOK_SECRET,
})
// Start event processing loop
startEventProcessor(bot)
console.log('Bot started in webhook mode')
} else {
// In development: register a placeholder webhook route
const telegramWebhook = (await import('../api/telegram/webhook.js')).default
app.all('/api/telegram/webhook', wrapHandler(telegramWebhook))
}
// Serve static files from dist/ in production
const distPath = path.join(__dirname, '..', '..', 'dist')
app.use(express.static(distPath))
// SPA fallback
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.path.startsWith('/api/')) {
return next()
}
res.sendFile(path.join(distPath, 'index.html'))
})
}
// Error handler
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
console.error('Server error:', err)
res.status(500).json({ error: 'Internal server error' })
})
async function start() {
try {
await setupRoutes()
app.listen(PORT, () => {
console.log(`Guidly server running on http://localhost:${PORT}`)
})
} catch (error) {
console.error('Failed to start server:', error)
process.exit(1)
}
}
start()