mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-07-24 21:04:58 +00:00
✨ feat(src/hh/scraper.ts): добавляет функцию для построения URL поиска вакансий
All checks were successful
Deploy / deploy (push) Successful in 3m49s
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:
@@ -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;
|
||||
2
prisma/migrations/20260609132601_add_area/migration.sql
Normal file
2
prisma/migrations/20260609132601_add_area/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Settings" ADD COLUMN "area" TEXT;
|
||||
@@ -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
15
src/hh/areas.ts
Normal 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 ?? '🌍 Весь мир'
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
26
src/hh/handlers/region.ts
Normal 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' })
|
||||
}
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export interface ApplyOptions {
|
||||
query: string
|
||||
area?: number
|
||||
area?: string
|
||||
maxApplies?: number
|
||||
searchMode?: 'text' | 'resume'
|
||||
resumeId?: string
|
||||
}
|
||||
|
||||
export interface VacancyRef {
|
||||
|
||||
20
src/hh/ui.ts
20
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 = {
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user