import type { Context } from 'telegraf'; import type { Message } from 'telegraf/types'; import type { FeedbackStore } from '../stores/feedback'; import type { SessionStore } from '../stores/session'; import fs from 'node:fs'; import path from 'node:path'; import { QuestAdapter } from '../quest/adapter'; import { buildChoicesKeyboard, buildMainMenuKeyboard } from './keyboards'; const LABELS: Record = { 'feedback.ql': 'Обратная связь' }; function getScenarios(rootDir: string) { const dir = path.join(rootDir, 'scenarios'); const files = fs.readdirSync(dir, { withFileTypes: true }) .filter(d => d.isFile() && d.name.endsWith('.ql')); const scenarios = files.map(d => ({ label: LABELS[d.name] ?? path.basename(d.name, '.ql'), file: d.name, })); return scenarios.sort((a, b) => a.file === 'feedback.ql' ? -1 : b.file === 'feedback.ql' ? 1 : a.label.localeCompare(b.label, 'ru'), ); } function startScenario(chatId: string, file: string, adapter: QuestAdapter, sessions: SessionStore) { const render = adapter.getTextAndChoices(); if (render.choices.length > 0) { sessions.upsert({ chatId, entryFile: file, currentNode: adapter.getCurrentNodeId(), updatedAt: Date.now() }); } return render; } export function onMessage(rootDir: string, sessions: SessionStore, feedback?: FeedbackStore) { return async (ctx: Context) => { const chatId = String(ctx.chat?.id ?? ''); const text = 'text' in (ctx.message ?? {}) ? (ctx.message as Message.TextMessage).text : ''; if (!chatId || !text) return; const scenarios = getScenarios(rootDir); const sess = sessions.get(chatId); const scenariosRoot = path.join(rootDir, 'scenarios'); // Start new scenario if (!sess) { const normalized = text.trim().toLowerCase(); const chosen = normalized === 'обратная связь' ? { file: 'feedback.ql' } : scenarios.find(s => s.label.trim().toLowerCase() === normalized || path.basename(s.file, '.ql').trim().toLowerCase() === normalized); if (!chosen) { await ctx.reply('Привет!', buildMainMenuKeyboard(scenarios.map(s => s.label))); return; } const adapter = new QuestAdapter(scenariosRoot, chosen.file); const render = startScenario(chatId, chosen.file, adapter, sessions); await ctx.reply(render.text, render.choices.length ? buildChoicesKeyboard(render.choices) : undefined); return; } // Continue active scenario const adapter = new QuestAdapter(scenariosRoot, sess.entryFile); adapter.moveTo(sess.currentNode); const current = adapter.getTextAndChoices(); const choiceIdx = current.choices.findIndex(c => c.text === text); if (choiceIdx < 0) { await ctx.reply(current.text, current.choices.length ? buildChoicesKeyboard(current.choices) : undefined); return; } const fromNode = adapter.getCurrentNodeId(); const { done } = adapter.choose(choiceIdx); const render = adapter.getTextAndChoices(); feedback?.addEvent(chatId, fromNode, text); await ctx.reply( render.text, done ? buildMainMenuKeyboard(scenarios.map(s => s.label)) : (render.choices.length ? buildChoicesKeyboard(render.choices) : undefined), ); done ? sessions.delete(chatId) : sessions.upsert({ chatId, entryFile: sess.entryFile, currentNode: adapter.getCurrentNodeId(), updatedAt: Date.now() }); }; } export function onStart(rootDir: string) { return async (ctx: Context) => { if (!ctx.chat?.id) return; const scenarios = getScenarios(rootDir); await ctx.reply('Привет!', buildMainMenuKeyboard(scenarios.map(s => s.label))); }; }