feat(bot): implement conversation flow for start command

add conversation handler for start command with name input and welcome message
update locales for new conversation flow
add @grammyjs/conversations dependency
This commit is contained in:
Yuu Ottosoka
2025-07-12 17:15:32 +03:00
parent df8bfa049f
commit 0ceba57012
10 changed files with 57 additions and 4 deletions

View File

@@ -1 +1,2 @@
start = Hello, how can I help you start-ask-name = Hello, what is your name?
start-welcome = Welcome, { $user }!

View File

@@ -1 +1,2 @@
start = Привет, как я могу тебе помочь start-ask-name = Привет, как тебя зовут
start-welcome = Привет, { $user }!

View File

@@ -11,6 +11,7 @@
"clean": "rm -rf dist" "clean": "rm -rf dist"
}, },
"dependencies": { "dependencies": {
"@grammyjs/conversations": "^2.1.0",
"@grammyjs/i18n": "^1.1.2", "@grammyjs/i18n": "^1.1.2",
"@hono/node-server": "^1.15.0", "@hono/node-server": "^1.15.0",
"@repo/config": "*", "@repo/config": "*",

View File

@@ -0,0 +1,3 @@
export enum ConversationSlug {
START = 'start',
}

View File

@@ -0,0 +1,2 @@
export * from './consts';
export * from './start.conversation';

View File

@@ -0,0 +1,23 @@
import { Conversation, createConversation } from '@grammyjs/conversations';
import { BotContext } from '../types';
import { ConversationSlug } from './consts';
async function enterConversation(conversation: Conversation, ctx: BotContext) {
const askNameText = await conversation.external((ctx: BotContext) =>
ctx.t('start-ask-name')
);
await ctx.reply(askNameText);
const { message } = await conversation.waitFor('message:text');
const welcomeText = await conversation.external((ctx: BotContext) =>
ctx.t('start-welcome', { user: message.text })
);
await ctx.reply(welcomeText);
}
export const startConversation = createConversation(
enterConversation,
ConversationSlug.START
);

View File

@@ -8,6 +8,8 @@ import { BotContext } from './types';
import { upsertUser } from './middlewares/upsert-user'; import { upsertUser } from './middlewares/upsert-user';
import { onError } from './middlewares/on-error'; import { onError } from './middlewares/on-error';
import { I18n } from '@grammyjs/i18n'; import { I18n } from '@grammyjs/i18n';
import { conversations } from '@grammyjs/conversations';
import { ConversationSlug, startConversation } from './conversations';
const bot = new Bot<BotContext>(config.BOT_TOKEN); const bot = new Bot<BotContext>(config.BOT_TOKEN);
const storage = new PrismaStorageAdapter(); const storage = new PrismaStorageAdapter();
@@ -20,6 +22,7 @@ const i18n = new I18n<BotContext>({
defaultLocale: 'ru', defaultLocale: 'ru',
useSession: true, // whether to store user language in session useSession: true, // whether to store user language in session
directory: 'locales', // Load all translation files from locales/. directory: 'locales', // Load all translation files from locales/.
fluentBundleOptions: { useIsolating: false },
localeNegotiator: async (ctx) => { localeNegotiator: async (ctx) => {
const session = await ctx.session; const session = await ctx.session;
return session.__language_code ?? ctx.from?.language_code ?? 'ru'; return session.__language_code ?? ctx.from?.language_code ?? 'ru';
@@ -27,9 +30,11 @@ const i18n = new I18n<BotContext>({
}); });
bot.use(i18n); bot.use(i18n);
bot.use(conversations());
bot.use(startConversation);
bot.command('start', async (ctx) => { bot.command('start', async (ctx) => {
await ctx.reply(ctx.t('start')); await ctx.conversation.enter(ConversationSlug.START);
}); });
if (config.MODE === 'webhook') { if (config.MODE === 'webhook') {

View File

@@ -1,6 +1,9 @@
import { ConversationFlavor } from '@grammyjs/conversations';
import { I18nFlavor } from '@grammyjs/i18n'; import { I18nFlavor } from '@grammyjs/i18n';
import { Context, LazySessionFlavor } from 'grammy'; import { Context, LazySessionFlavor } from 'grammy';
// config your session data // config your session data
export type SessionData = { __language_code?: string }; export type SessionData = { __language_code?: string };
export type BotContext = Context & LazySessionFlavor<SessionData> & I18nFlavor; export type BotContext = ConversationFlavor<
Context & LazySessionFlavor<SessionData> & I18nFlavor
>;

14
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"@hono/zod-openapi": "^0.9.0", "@hono/zod-openapi": "^0.9.0",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@repo/config": "*", "@repo/config": "*",
"@repo/db": "*",
"@repo/logger": "*", "@repo/logger": "*",
"@repo/typescript-config": "*", "@repo/typescript-config": "*",
"dotenv": "^17.1.0", "dotenv": "^17.1.0",
@@ -149,6 +150,7 @@
"apps/bot": { "apps/bot": {
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@grammyjs/conversations": "^2.1.0",
"@grammyjs/i18n": "^1.1.2", "@grammyjs/i18n": "^1.1.2",
"@hono/node-server": "^1.15.0", "@hono/node-server": "^1.15.0",
"@repo/config": "*", "@repo/config": "*",
@@ -894,6 +896,18 @@
"npm": ">=7.0.0" "npm": ">=7.0.0"
} }
}, },
"node_modules/@grammyjs/conversations": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@grammyjs/conversations/-/conversations-2.1.0.tgz",
"integrity": "sha512-nYoHwFyXcQ2lkQejKReoINknkgFjeIZhyJNMBwHng5UHO/ewHKs1dV2XVaLrKkHTZ1gCZ+dKkjEp26wwHyQeGg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || >=14.13.1"
},
"peerDependencies": {
"grammy": "^1.20.1"
}
},
"node_modules/@grammyjs/i18n": { "node_modules/@grammyjs/i18n": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@grammyjs/i18n/-/i18n-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@grammyjs/i18n/-/i18n-1.1.2.tgz",