From e32c827d19bbc89515018415d08d10d60579ddb6 Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 9 Jun 2026 16:42:19 +0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(src/hh/scraper.ts):=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=B8=D1=8F=20URL=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B2=D0=B0=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D1=81=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 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): добавляет поле для хранения режима поиска в схему базы данных --- .../migration.sql | 16 +++++ .../20260609132601_add_area/migration.sql | 2 + prisma/schema.prisma | 2 + src/hh/areas.ts | 15 +++++ src/hh/bot-commands.ts | 63 ++++++++++++++++--- src/hh/handlers/apply.ts | 17 ++++- src/hh/handlers/onboarding.ts | 36 ++++++++++- src/hh/handlers/region.ts | 26 ++++++++ src/hh/handlers/settings.ts | 19 +++++- src/hh/scraper.ts | 20 +++++- src/hh/state.ts | 2 +- src/hh/types.ts | 4 +- src/hh/ui.ts | 46 +++++++++----- 13 files changed, 228 insertions(+), 40 deletions(-) create mode 100644 prisma/migrations/20260609131406_add_search_mode/migration.sql create mode 100644 prisma/migrations/20260609132601_add_area/migration.sql create mode 100644 src/hh/areas.ts create mode 100644 src/hh/handlers/region.ts diff --git a/prisma/migrations/20260609131406_add_search_mode/migration.sql b/prisma/migrations/20260609131406_add_search_mode/migration.sql new file mode 100644 index 0000000..90e4d85 --- /dev/null +++ b/prisma/migrations/20260609131406_add_search_mode/migration.sql @@ -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; diff --git a/prisma/migrations/20260609132601_add_area/migration.sql b/prisma/migrations/20260609132601_add_area/migration.sql new file mode 100644 index 0000000..4abc4c7 --- /dev/null +++ b/prisma/migrations/20260609132601_add_area/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Settings" ADD COLUMN "area" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cb279c9..0f84aa9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,6 +43,8 @@ model Settings { searchQuery String @default("Vue") maxApplies Int @default(1) selectedResumeId String? + searchMode String @default("text") + area String? } model SkippedVacancy { diff --git a/src/hh/areas.ts b/src/hh/areas.ts new file mode 100644 index 0000000..1606a8b --- /dev/null +++ b/src/hh/areas.ts @@ -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 ?? '🌍 Весь мир' +} diff --git a/src/hh/bot-commands.ts b/src/hh/bot-commands.ts index d65048d..9d79bc2 100644 --- a/src/hh/bot-commands.ts +++ b/src/hh/bot-commands.ts @@ -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 type CallbackHandler = (chatId: number, messageId: number) => Promise @@ -19,16 +20,17 @@ const MESSAGE_HANDLERS: Partial> = { [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 = { @@ -83,6 +85,14 @@ const CALLBACK_HANDLERS: Record = { 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 = { 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, '✅ Поиск по резюме включён', { 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, '⛔ Поиск по резюме выключен', { 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) }) } diff --git a/src/hh/handlers/apply.ts b/src/hh/handlers/apply.ts index 3873b8a..108f13e 100644 --- a/src/hh/handlers/apply.ts +++ b/src/hh/handlers/apply.ts @@ -13,9 +13,17 @@ export async function handleApply(chatId: number): Promise { 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 { } const lines: string[] = [] - lines.push(`📊 Итого по запросу «${settings.searchQuery}»`) + const summaryLabel = searchMode === 'resume' + ? (settings.searchQuery ? `запросу «${settings.searchQuery}» + резюме` : 'резюме') + : `запросу «${settings.searchQuery}»` + lines.push(`📊 Итого по ${summaryLabel}`) lines.push(`✅ Откликнулся: ${result.applied.length}`) lines.push(`⏭ Пропущено: ${result.skipped.length}`) if (result.errors.length) diff --git a/src/hh/handlers/onboarding.ts b/src/hh/handlers/onboarding.ts index 900f850..2781be6 100644 --- a/src/hh/handlers/onboarding.ts +++ b/src/hh/handlers/onboarding.ts @@ -21,7 +21,7 @@ export async function showMaxStep(chatId: number): Promise { const msg = await bot.sendMessage( chatId, - `🔢 Шаг 1 из 3 — Максимум откликов\n\n` + `🔢 Шаг 1 из 4 — Максимум откликов\n\n` + `Сколько вакансий бот обработает за один запуск. Рекомендуем начать с небольшого числа, чтобы проверить письма.\n\n` + `Можно оставить текущее значение: ${current} или ввести число в чат от 1 до 50:`, { @@ -44,7 +44,7 @@ export async function showQueryStep(chatId: number): Promise { const msg = await bot.sendMessage( chatId, - `🔍 Шаг 2 из 3 — Поисковый запрос\n\n` + `🔍 Шаг 2 из 4 — Поисковый запрос\n\n` + `По этому запросу бот ищет вакансии на hh.ru. Используй профессию или ключевые навыки.\n\n` + `Текущий запрос: ${current}\n\n` + `Введи новый или оставь текущий:`, @@ -60,6 +60,36 @@ export async function showQueryStep(chatId: number): Promise { state.onboardingMsgId = msg.message_id } +export async function showResumeModeStep(chatId: number): Promise { + 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, + `🔄 Шаг 3 из 4 — Режим поиска по резюме\n\n` + + `Можно комбинировать параметры поиска:\n\n` + + `• Выключен — всегда ищет по ключевым словам (?text=...)\n` + + `• Включён + есть ключевые слова — комбинирует оба параметра (?text=...&resume=...)\n` + + `• Включён + ключевые слова пусты — ищет только по резюме (?resume=...)\n\n` + + `Сейчас: ${enabled ? '✅ включён' : '⛔ выключен'}`, + { + 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 { const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } }) const resume = settings?.selectedResumeId @@ -90,7 +120,7 @@ export async function showPromptStep(chatId: number): Promise { const msg = await bot.sendMessage( chatId, - `📝 Шаг 3 из 3 — Промт для AI\n\n` + `📝 Шаг 4 из 4 — Промт для AI\n\n` + `Инструкция, которую AI получает при написании сопроводительного письма. ` + `Задаёт стиль, тон и то, что важно упомянуть.\n\n` + `Текущий промт:\n
${escapeHtml(currentPrompt)}
\n\n` diff --git a/src/hh/handlers/region.ts b/src/hh/handlers/region.ts new file mode 100644 index 0000000..809ac2b --- /dev/null +++ b/src/hh/handlers/region.ts @@ -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 { + 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, + `🌍 Регион\n\nТекущий: ${areaLabel(current)}\n\nВыбери регион поиска:`, + { parse_mode: 'HTML', reply_markup: { inline_keyboard: buttons } }, + ) +} + +export async function handleAreaPick(chatId: number, messageId: number, rawCode: string): Promise { + 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, `✅ Регион: ${areaLabel(code)}`, { parse_mode: 'HTML' }) +} diff --git a/src/hh/handlers/settings.ts b/src/hh/handlers/settings.ts index 49676cf..c8c05f7 100644 --- a/src/hh/handlers/settings.ts +++ b/src/hh/handlers/settings.ts @@ -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 { + 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' + ? `✅ Поиск по резюме включён\n\n• Есть ключевые слова → ?text=...&resume=...\n• Ключевые слова пусты → только ?resume=...` + : `⛔ Поиск по резюме выключен\n\n• Всегда ищет по ключевым словам → ?text=...` + await bot.sendMessage(chatId, text, { parse_mode: 'HTML', reply_markup: await buildSettingsKeyboard(chatId) }) +} + export async function handleQuery(chatId: number): Promise { const state = getState(chatId) state.awaitingQuery = true @@ -17,6 +29,7 @@ export async function handleQuery(chatId: number): Promise { 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 { 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) }) } } diff --git a/src/hh/scraper.ts b/src/hh/scraper.ts index f90837f..18923a2 100644 --- a/src/hh/scraper.ts +++ b/src/hh/scraper.ts @@ -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 { 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) diff --git a/src/hh/state.ts b/src/hh/state.ts index 72f4e0c..252ac15 100644 --- a/src/hh/state.ts +++ b/src/hh/state.ts @@ -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 } diff --git a/src/hh/types.ts b/src/hh/types.ts index 52b087d..a461592 100644 --- a/src/hh/types.ts +++ b/src/hh/types.ts @@ -1,7 +1,9 @@ export interface ApplyOptions { query: string - area?: number + area?: string maxApplies?: number + searchMode?: 'text' | 'resume' + resumeId?: string } export interface VacancyRef { diff --git a/src/hh/ui.ts b/src/hh/ui.ts index 1f09797..6268199 100644 --- a/src/hh/ui.ts +++ b/src/hh/ui.ts @@ -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,25 +37,34 @@ export const MAIN_REPLY_KEYBOARD = { persistent: true, } -export const SETTINGS_REPLY_KEYBOARD = { - keyboard: [ - [{ text: BTN.MAX }, { text: BTN.AUTO_TOGGLE }], - [{ text: BTN.RESUME_LIST }, { text: BTN.PROMPT }], - [{ text: BTN.LOGIN }], - [{ text: BTN.BACK }], - ], - resize_keyboard: true, - persistent: true, +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.SEARCH_MODE_TOGGLE}: ${modeLabel}` }, { text: BTN.LOGIN }], + [{ text: BTN.BACK }], + ], + resize_keyboard: true, + persistent: true, + } } -export const FILTERS_REPLY_KEYBOARD = { - keyboard: [ - [{ text: BTN.QUERY }], - [{ text: BTN.EXCLUSIONS }, { text: BTN.REGION }], - [{ text: BTN.BACK }], - ], - resize_keyboard: true, - persistent: true, +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}: ${regionShort}` }], + [{ text: BTN.BACK }], + ], + resize_keyboard: true, + persistent: true, + } } export const INFO_REPLY_KEYBOARD = {