feat(src/hh/scraper.ts): добавляет функцию для построения URL поиска вакансий
All checks were successful
Deploy / deploy (push) Successful in 3m49s

 feat(src/hh/ui.ts): добавляет кнопку для переключения режима поиска

 feat(src/hh/bot-commands.ts): обновляет обработчики команд для использования новых функций

 feat(src/hh/types.ts): обновляет интерфейс ApplyOptions для поддержки режима поиска

 feat(src/hh/state.ts): добавляет новое состояние для режима поиска в onboarding

 feat(src/hh/handlers/settings.ts): добавляет обработчик для переключения режима поиска

 feat(src/hh/handlers/onboarding.ts): добавляет шаг для выбора режима поиска по резюме

 feat(src/hh/handlers/apply.ts): обновляет логику применения с учетом режима поиска

 feat(prisma/schema.prisma): добавляет поле для хранения режима поиска в схему базы данных
This commit is contained in:
Oscar
2026-06-09 16:42:19 +03:00
parent 162abdfc0a
commit e32c827d19
13 changed files with 228 additions and 40 deletions

View File

@@ -0,0 +1,16 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Settings" (
"telegramId" BIGINT NOT NULL PRIMARY KEY,
"searchQuery" TEXT NOT NULL DEFAULT 'Vue',
"maxApplies" INTEGER NOT NULL DEFAULT 1,
"selectedResumeId" TEXT,
"searchMode" TEXT NOT NULL DEFAULT 'text',
CONSTRAINT "Settings_telegramId_fkey" FOREIGN KEY ("telegramId") REFERENCES "User" ("telegramId") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Settings" ("maxApplies", "searchQuery", "selectedResumeId", "telegramId") SELECT "maxApplies", "searchQuery", "selectedResumeId", "telegramId" FROM "Settings";
DROP TABLE "Settings";
ALTER TABLE "new_Settings" RENAME TO "Settings";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Settings" ADD COLUMN "area" TEXT;

View File

@@ -43,6 +43,8 @@ model Settings {
searchQuery String @default("Vue")
maxApplies Int @default(1)
selectedResumeId String?
searchMode String @default("text")
area String?
}
model SkippedVacancy {

15
src/hh/areas.ts Normal file
View File

@@ -0,0 +1,15 @@
export interface HhArea {
code: string | null
label: string
}
export const HH_AREAS: HhArea[] = [
{ code: null, label: '🌍 Весь мир' },
{ code: '113', label: '🇷🇺 Россия' },
{ code: '1', label: '🏙 Москва' },
{ code: '2', label: '🌊 Санкт-Петербург' },
]
export function areaLabel(code: string | null | undefined): string {
return HH_AREAS.find(a => a.code === (code ?? null))?.label ?? '🌍 Весь мир'
}

View File

@@ -4,11 +4,12 @@ import { debugFunc } from '@/hh/handlers/debug'
import { handleApply } from './handlers/apply.js'
import { doLogin, doLoginByPhone, handleLogin, handleLoginByEmail, handleLoginByPhone } from './handlers/auth.js'
import { handleSkipped, handleStatus } from './handlers/info.js'
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo } from './handlers/onboarding.js'
import { finishOnboarding, showPromptStep, showQueryStep, showResumeInfo, showResumeModeStep } from './handlers/onboarding.js'
import { handleAreaPick, handleRegion } from './handlers/region.js'
import { handleMyResume, handleResumeList, handleResumePick } from './handlers/resume.js'
import { DEFAULT_PROMPT, handleAutoToggle, handleMax, handlePrompt, handleQuery } from './handlers/settings.js'
import { DEFAULT_PROMPT, handleAutoToggle, handleMax, handlePrompt, handleQuery, handleSearchModeToggle } from './handlers/settings.js'
import { getState } from './state.js'
import { BTN, FILTERS_REPLY_KEYBOARD, INFO_REPLY_KEYBOARD, LOGIN_REPLY_KEYBOARD, MAIN_REPLY_KEYBOARD, SETTINGS_REPLY_KEYBOARD } from './ui.js'
import { BTN, INFO_REPLY_KEYBOARD, LOGIN_REPLY_KEYBOARD, MAIN_REPLY_KEYBOARD, buildFiltersKeyboard, buildSettingsKeyboard } from './ui.js'
type MsgHandler = (chatId: number) => Promise<void>
type CallbackHandler = (chatId: number, messageId: number) => Promise<void>
@@ -19,16 +20,17 @@ const MESSAGE_HANDLERS: Partial<Record<string, MsgHandler>> = {
[BTN.QUERY]: handleQuery,
[BTN.MAX]: handleMax,
[BTN.AUTO_TOGGLE]: handleAutoToggle,
[BTN.SEARCH_MODE_TOGGLE]: handleSearchModeToggle,
[BTN.PROMPT]: handlePrompt,
[BTN.LOGIN]: handleLogin,
[BTN.RESUME_LIST]: handleResumeList,
[BTN.MY_RESUME]: handleMyResume,
[BTN.SKIPPED]: handleSkipped,
[BTN.SETTINGS]: async (chatId) => { await bot.sendMessage(chatId, '⚙️ Настройки:', { reply_markup: SETTINGS_REPLY_KEYBOARD }) },
[BTN.FILTERS]: async (chatId) => { await bot.sendMessage(chatId, '🔎 Фильтры:', { reply_markup: FILTERS_REPLY_KEYBOARD }) },
[BTN.SETTINGS]: async (chatId) => { await bot.sendMessage(chatId, '⚙️ Настройки:', { reply_markup: await buildSettingsKeyboard(chatId) }) },
[BTN.FILTERS]: async (chatId) => { await bot.sendMessage(chatId, '🔎 Фильтры:', { reply_markup: await buildFiltersKeyboard(chatId) }) },
[BTN.INFO]: async (chatId) => { await bot.sendMessage(chatId, ' Информация:', { reply_markup: INFO_REPLY_KEYBOARD }) },
[BTN.BACK]: async (chatId) => { await bot.sendMessage(chatId, '🤖 HH Auto-Apply', { reply_markup: MAIN_REPLY_KEYBOARD }) },
[BTN.REGION]: debugFunc,
[BTN.REGION]: handleRegion,
}
const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
@@ -83,6 +85,14 @@ const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
state.queryPromptMessageId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
},
hh_clear_query: async (chatId, messageId) => {
const state = getState(chatId)
state.awaitingQuery = false
state.queryPromptMessageId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
await prisma.settings.update({ where: { telegramId: chatId }, data: { searchQuery: '' } })
await bot.sendMessage(chatId, '🗑 Запрос очищен')
},
hh_keep_max: async (chatId, messageId) => {
const state = getState(chatId)
state.awaitingMax = false
@@ -111,6 +121,33 @@ const CALLBACK_HANDLERS: Record<string, CallbackHandler> = {
await showQueryStep(chatId)
},
ob_skip_query: async (chatId, messageId) => {
const state = getState(chatId)
state.onboardingStep = null
state.onboardingMsgId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
await showResumeModeStep(chatId)
},
ob_resume_mode_enable: async (chatId, messageId) => {
const state = getState(chatId)
state.onboardingStep = null
state.onboardingMsgId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
await prisma.settings.update({ where: { telegramId: chatId }, data: { searchMode: 'resume' } })
await bot.sendMessage(chatId, '✅ Поиск по резюме <b>включён</b>', { parse_mode: 'HTML' })
await showResumeInfo(chatId)
await showPromptStep(chatId)
},
ob_resume_mode_disable: async (chatId, messageId) => {
const state = getState(chatId)
state.onboardingStep = null
state.onboardingMsgId = null
await bot.deleteMessage(chatId, messageId).catch(() => {})
await prisma.settings.update({ where: { telegramId: chatId }, data: { searchMode: 'text' } })
await bot.sendMessage(chatId, '⛔ Поиск по резюме <b>выключен</b>', { parse_mode: 'HTML' })
await showResumeInfo(chatId)
await showPromptStep(chatId)
},
ob_skip_resume_mode: async (chatId, messageId) => {
const state = getState(chatId)
state.onboardingStep = null
state.onboardingMsgId = null
@@ -184,6 +221,10 @@ export function registerHHCommands() {
const idx = Number(query.data.replace('hh_resume_pick_', ''))
await handleResumePick(chatId, messageId, idx)
}
else if (query.data?.startsWith('hh_area_')) {
const code = query.data.replace('hh_area_', '')
await handleAreaPick(chatId, messageId, code)
}
})
bot.on('message', async (msg) => {
@@ -200,6 +241,8 @@ export function registerHHCommands() {
const isAwaiting = state.awaitingEmail || state.awaitingPhone || state.awaitingQuery || state.awaitingMax || state.awaitingPrompt || state.onboardingStep !== null
const isMenuButton = Object.values(BTN).includes(msg.text as typeof BTN[keyof typeof BTN])
|| msg.text.startsWith(BTN.SEARCH_MODE_TOGGLE)
|| msg.text.startsWith(BTN.REGION)
if (isMenuButton && isAwaiting) {
await clearAwaitingState(chatId)
@@ -232,8 +275,7 @@ export function registerHHCommands() {
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
const updated = await prisma.settings.update({ where: { telegramId: chatId }, data: { searchQuery: msg.text } })
await bot.sendMessage(chatId, `✅ Запрос: «${updated.searchQuery}»`)
await showResumeInfo(chatId)
await showPromptStep(chatId)
await showResumeModeStep(chatId)
return
}
@@ -321,6 +363,9 @@ export function registerHHCommands() {
return
}
await MESSAGE_HANDLERS[msg.text]?.(chatId)
const handler = MESSAGE_HANDLERS[msg.text]
?? (msg.text.startsWith(BTN.SEARCH_MODE_TOGGLE) ? MESSAGE_HANDLERS[BTN.SEARCH_MODE_TOGGLE] : undefined)
?? (msg.text.startsWith(BTN.REGION) ? MESSAGE_HANDLERS[BTN.REGION] : undefined)
await handler?.(chatId)
})
}

View File

@@ -13,9 +13,17 @@ export async function handleApply(chatId: number): Promise<void> {
state.isApplying = true
const reporter = createStatusReporter(chatId)
await bot.sendMessage(chatId, `🔄 Ищу вакансии по запросу "${settings.searchQuery}"...`, { reply_markup: APPLYING_REPLY_KEYBOARD })
const searchMode = (settings.searchMode ?? 'text') as 'text' | 'resume'
const hasQuery = !!settings.searchQuery
const searchLabel = searchMode === 'resume'
? (hasQuery ? `по запросу "${settings.searchQuery}" и резюме` : 'по резюме')
: `по запросу "${settings.searchQuery}"`
await bot.sendMessage(chatId, `🔄 Ищу вакансии ${searchLabel}...`, { reply_markup: APPLYING_REPLY_KEYBOARD })
applyToJobs({ query: settings.searchQuery, maxApplies: settings.maxApplies }, { chatId, reporter })
applyToJobs(
{ query: settings.searchQuery, maxApplies: settings.maxApplies, searchMode, resumeId: settings.selectedResumeId ?? undefined, area: settings.area ?? undefined },
{ chatId, reporter },
)
.then(async (result) => {
state.isApplying = false
await bot.sendMessage(chatId, '✅ Готово', { reply_markup: MAIN_REPLY_KEYBOARD })
@@ -25,7 +33,10 @@ export async function handleApply(chatId: number): Promise<void> {
}
const lines: string[] = []
lines.push(`📊 <b>Итого по запросу «${settings.searchQuery}»</b>`)
const summaryLabel = searchMode === 'resume'
? (settings.searchQuery ? `запросу «${settings.searchQuery}» + резюме` : 'резюме')
: `запросу «${settings.searchQuery}»`
lines.push(`📊 <b>Итого по ${summaryLabel}</b>`)
lines.push(`✅ Откликнулся: ${result.applied.length}`)
lines.push(`⏭ Пропущено: ${result.skipped.length}`)
if (result.errors.length)

View File

@@ -21,7 +21,7 @@ export async function showMaxStep(chatId: number): Promise<void> {
const msg = await bot.sendMessage(
chatId,
`🔢 <b>Шаг 1 из 3 — Максимум откликов</b>\n\n`
`🔢 <b>Шаг 1 из 4 — Максимум откликов</b>\n\n`
+ `Сколько вакансий бот обработает за один запуск. Рекомендуем начать с небольшого числа, чтобы проверить письма.\n\n`
+ `Можно оставить текущее значение: <b>${current}</b> или ввести число в чат от 1 до 50:`,
{
@@ -44,7 +44,7 @@ export async function showQueryStep(chatId: number): Promise<void> {
const msg = await bot.sendMessage(
chatId,
`🔍 <b>Шаг 2 из 3 — Поисковый запрос</b>\n\n`
`🔍 <b>Шаг 2 из 4 — Поисковый запрос</b>\n\n`
+ `По этому запросу бот ищет вакансии на hh.ru. Используй профессию или ключевые навыки.\n\n`
+ `Текущий запрос: <b>${current}</b>\n\n`
+ `Введи новый или оставь текущий:`,
@@ -60,6 +60,36 @@ export async function showQueryStep(chatId: number): Promise<void> {
state.onboardingMsgId = msg.message_id
}
export async function showResumeModeStep(chatId: number): Promise<void> {
const state = getState(chatId)
state.onboardingStep = 'resume_mode'
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
const enabled = settings?.searchMode === 'resume'
const msg = await bot.sendMessage(
chatId,
`🔄 <b>Шаг 3 из 4 — Режим поиска по резюме</b>\n\n`
+ `Можно комбинировать параметры поиска:\n\n`
+ `• <b>Выключен</b> — всегда ищет по ключевым словам (<code>?text=...</code>)\n`
+ `• <b>Включён + есть ключевые слова</b> — комбинирует оба параметра (<code>?text=...&resume=...</code>)\n`
+ `• <b>Включён + ключевые слова пусты</b> — ищет только по резюме (<code>?resume=...</code>)\n\n`
+ `Сейчас: <b>${enabled ? '✅ включён' : '⛔ выключен'}</b>`,
{
parse_mode: 'HTML',
reply_markup: {
inline_keyboard: [[
{
text: enabled ? '⛔ Выключить и продолжить' : '✅ Включить и продолжить',
callback_data: enabled ? 'ob_resume_mode_disable' : 'ob_resume_mode_enable',
},
{ text: 'Продолжить →', callback_data: 'ob_skip_resume_mode' },
]],
},
},
)
state.onboardingMsgId = msg.message_id
}
export async function showResumeInfo(chatId: number): Promise<void> {
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
const resume = settings?.selectedResumeId
@@ -90,7 +120,7 @@ export async function showPromptStep(chatId: number): Promise<void> {
const msg = await bot.sendMessage(
chatId,
`📝 <b>Шаг 3 из 3 — Промт для AI</b>\n\n`
`📝 <b>Шаг 4 из 4 — Промт для AI</b>\n\n`
+ `Инструкция, которую AI получает при написании сопроводительного письма. `
+ `Задаёт стиль, тон и то, что важно упомянуть.\n\n`
+ `<i>Текущий промт:</i>\n<pre>${escapeHtml(currentPrompt)}</pre>\n\n`

26
src/hh/handlers/region.ts Normal file
View File

@@ -0,0 +1,26 @@
import bot from '@bot'
import prisma from '@prisma'
import { HH_AREAS, areaLabel } from '../areas.js'
export async function handleRegion(chatId: number): Promise<void> {
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
const current = settings?.area ?? null
const buttons = HH_AREAS.map(({ code, label }) => [{
text: code === current ? `${label}` : label,
callback_data: `hh_area_${code ?? 'world'}`,
}])
await bot.sendMessage(
chatId,
`🌍 <b>Регион</b>\n\nТекущий: <b>${areaLabel(current)}</b>\n\nВыбери регион поиска:`,
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: buttons } },
)
}
export async function handleAreaPick(chatId: number, messageId: number, rawCode: string): Promise<void> {
const code = rawCode === 'world' ? null : rawCode
await prisma.settings.update({ where: { telegramId: chatId }, data: { area: code } })
await bot.deleteMessage(chatId, messageId).catch(() => {})
await bot.sendMessage(chatId, `✅ Регион: <b>${areaLabel(code)}</b>`, { parse_mode: 'HTML' })
}

View File

@@ -1,9 +1,21 @@
import bot from '@bot'
import prisma from '@prisma'
import cron from 'node-cron'
import { SETTINGS_REPLY_KEYBOARD, escapeHtml } from '../ui.js'
import { buildSettingsKeyboard, escapeHtml } from '../ui.js'
import { getState } from '../state.js'
export async function handleSearchModeToggle(chatId: number): Promise<void> {
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
const current = settings?.searchMode ?? 'text'
const next = current === 'text' ? 'resume' : 'text'
await prisma.settings.update({ where: { telegramId: chatId }, data: { searchMode: next } })
const text = next === 'resume'
? `✅ <b>Поиск по резюме включён</b>\n\n• Есть ключевые слова → <code>?text=...&resume=...</code>\n• Ключевые слова пусты → только <code>?resume=...</code>`
: `⛔ <b>Поиск по резюме выключен</b>\n\n• Всегда ищет по ключевым словам → <code>?text=...</code>`
await bot.sendMessage(chatId, text, { parse_mode: 'HTML', reply_markup: await buildSettingsKeyboard(chatId) })
}
export async function handleQuery(chatId: number): Promise<void> {
const state = getState(chatId)
state.awaitingQuery = true
@@ -17,6 +29,7 @@ export async function handleQuery(chatId: number): Promise<void> {
reply_markup: {
inline_keyboard: [[
{ text: `✅ Оставить «${currentQuery}»`, callback_data: 'hh_keep_query' },
{ text: '🗑 Очистить запрос', callback_data: 'hh_clear_query' },
]],
},
},
@@ -72,12 +85,12 @@ export async function handleAutoToggle(chatId: number): Promise<void> {
if (state.autoCron) {
state.autoCron.stop()
state.autoCron = null
await bot.sendMessage(chatId, '⛔ Авто остановлен', { reply_markup: SETTINGS_REPLY_KEYBOARD })
await bot.sendMessage(chatId, '⛔ Авто остановлен', { reply_markup: await buildSettingsKeyboard(chatId) })
}
else {
state.autoCron = cron.schedule('0 10 * * 1-5', async () => {
await bot.sendMessage(chatId, '⏰ Авто-отклик...')
})
await bot.sendMessage(chatId, '✅ Авто включён (пн-пт, 10:00)', { reply_markup: SETTINGS_REPLY_KEYBOARD })
await bot.sendMessage(chatId, '✅ Авто включён (пн-пт, 10:00)', { reply_markup: await buildSettingsKeyboard(chatId) })
}
}

View File

@@ -253,6 +253,18 @@ export async function saveResume(chatId: number, resumeItem: ResumeListItem): Pr
})
}
function buildVacancySearchUrl(query: string, searchMode: 'text' | 'resume', resumeId: string | undefined, area: string | undefined, page: number): string {
const parts: string[] = []
if (query && query !== '--')
parts.push(`text=${encodeURIComponent(query)}`)
if (searchMode === 'resume' && resumeId)
parts.push(`resume=${encodeURIComponent(resumeId)}`)
if (area)
parts.push(`area=${encodeURIComponent(area)}`)
parts.push('items_on_page=100', `page=${page}`)
return `https://hh.ru/search/vacancy?${parts.join('&')}`
}
async function collectPageVacancies(page: Page) {
return page.$$eval(
'[data-qa="vacancy-serp__vacancy"]',
@@ -272,7 +284,7 @@ async function collectPageVacancies(page: Page) {
}
export async function applyToJobs(
{ query, area = 1, maxApplies = 10 }: ApplyOptions,
{ query, area, maxApplies = 10, searchMode = 'text', resumeId }: ApplyOptions,
{ chatId, reporter }: { chatId: number, reporter: StatusReporter },
): Promise<ApplyResult> {
return withBrowser(async (browser) => {
@@ -283,7 +295,9 @@ export async function applyToJobs(
await loadSession(page, chatId)
const url = `https://hh.ru/search/vacancy?text=${encodeURIComponent(query)}&items_on_page=100&page=0` // &area=${area}
const url = buildVacancySearchUrl(query, searchMode, resumeId, area, 0)
log.info(url)
await page.goto(url, { waitUntil: 'domcontentloaded' })
await page.waitForSelector('[data-qa="serp-item__title"]', { timeout: 10000 }).catch(() => null)
await page.pause()
@@ -308,7 +322,7 @@ export async function applyToJobs(
log.info('URL:', page.url())
log.info('Max page:', maxPage)
for (let p = 1; p <= maxPage; p++) {
const pageUrl = `https://hh.ru/search/vacancy?text=${encodeURIComponent(query)}&items_on_page=100&page=${p}`
const pageUrl = buildVacancySearchUrl(query, searchMode, resumeId, area, p)
await page.goto(pageUrl, { waitUntil: 'domcontentloaded' })
await page.waitForSelector('[data-qa="vacancy-serp__vacancy"]', { timeout: 10000 }).catch(() => null)
const more = await collectPageVacancies(page)

View File

@@ -15,7 +15,7 @@ export interface UserState {
queryPromptMessageId: number | null
maxPromptMessageId: number | null
promptPromptMessageId: number | null
onboardingStep: 'max' | 'query' | 'prompt' | null
onboardingStep: 'max' | 'query' | 'resume_mode' | 'prompt' | null
onboardingMsgId: number | null
onboardingAfterResume: boolean
}

View File

@@ -1,7 +1,9 @@
export interface ApplyOptions {
query: string
area?: number
area?: string
maxApplies?: number
searchMode?: 'text' | 'resume'
resumeId?: string
}
export interface VacancyRef {

View File

@@ -1,4 +1,6 @@
import bot from '@bot'
import prisma from '@prisma'
import { areaLabel } from './areas.js'
export const BTN = {
APPLY: '🚀 Откликнуться',
@@ -8,6 +10,7 @@ export const BTN = {
REGION: '🌍 Регион',
MAX: '🔢 Макс. откликов',
AUTO_TOGGLE: '⏰ Авто',
SEARCH_MODE_TOGGLE: '🔄 Режим',
LOGIN: '🔑 Войти на hh.ru',
RESUME_LIST: '📄 Выбрать резюме',
MY_RESUME: '📋 Моё резюме',
@@ -34,26 +37,35 @@ export const MAIN_REPLY_KEYBOARD = {
persistent: true,
}
export const SETTINGS_REPLY_KEYBOARD = {
export async function buildSettingsKeyboard(chatId: number) {
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
const modeLabel = settings?.searchMode === 'resume' ? 'резюме' : 'текст'
return {
keyboard: [
[{ text: BTN.MAX }, { text: BTN.AUTO_TOGGLE }],
[{ text: BTN.RESUME_LIST }, { text: BTN.PROMPT }],
[{ text: BTN.LOGIN }],
[{ text: `${BTN.SEARCH_MODE_TOGGLE}: ${modeLabel}` }, { text: BTN.LOGIN }],
[{ text: BTN.BACK }],
],
resize_keyboard: true,
persistent: true,
}
}
export const FILTERS_REPLY_KEYBOARD = {
export async function buildFiltersKeyboard(chatId: number) {
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
const fullLabel = areaLabel(settings?.area)
const regionShort = fullLabel.split(' ').slice(1).join(' ')
return {
keyboard: [
[{ text: BTN.QUERY }],
[{ text: BTN.EXCLUSIONS }, { text: BTN.REGION }],
[{ text: BTN.EXCLUSIONS }, { text: `${BTN.REGION}: ${regionShort}` }],
[{ text: BTN.BACK }],
],
resize_keyboard: true,
persistent: true,
}
}
export const INFO_REPLY_KEYBOARD = {
keyboard: [