98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
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<string, string> = { '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)));
|
|
};
|
|
}
|