mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-06-09 02:15:34 +00:00
✨ feat(file/topic): добавлено меню в главном окне бота. UI обновлен в соответствии с новыми требованиями.
All checks were successful
Deploy / deploy (push) Successful in 46s
All checks were successful
Deploy / deploy (push) Successful in 46s
This commit is contained in:
49
src/hh/handlers/apply.ts
Normal file
49
src/hh/handlers/apply.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import { applyToJobs } from '../scraper.js'
|
||||
import { createStatusReporter, escapeHtml } from '../ui.js'
|
||||
|
||||
export async function handleApply(chatId: number): Promise<void> {
|
||||
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
|
||||
if (!settings)
|
||||
return
|
||||
|
||||
const reporter = createStatusReporter(chatId)
|
||||
await reporter.keep(`🔄 Ищу вакансии по запросу "${settings.searchQuery}"...`)
|
||||
|
||||
applyToJobs({ query: settings.searchQuery, maxApplies: settings.maxApplies }, { chatId, reporter })
|
||||
.then(async (result) => {
|
||||
if (result.error) {
|
||||
await bot.sendMessage(chatId, `❌ ${result.error}`)
|
||||
return
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push(`📊 <b>Итого по запросу «${settings.searchQuery}»</b>`)
|
||||
lines.push(`✅ Откликнулся: ${result.applied.length}`)
|
||||
lines.push(`⏭ Пропущено: ${result.skipped.length}`)
|
||||
if (result.errors.length)
|
||||
lines.push(`❌ Ошибок: ${result.errors.length}`)
|
||||
|
||||
if (result.skipped.length) {
|
||||
lines.push('')
|
||||
lines.push('⏭ <b>Пропущенные:</b>')
|
||||
result.skipped.forEach(v => lines.push(`• <a href="${v.href}">${v.title}</a>`))
|
||||
}
|
||||
|
||||
if (result.errors.length) {
|
||||
lines.push('')
|
||||
lines.push('❌ <b>Ошибки:</b>')
|
||||
result.errors.forEach(v => lines.push(`• <a href="${v.href}">${escapeHtml(v.title)}</a> — ${escapeHtml(v.message ?? '')}`))
|
||||
}
|
||||
|
||||
const fullText = lines.join('\n')
|
||||
const LIMIT = 4000
|
||||
for (let i = 0; i < fullText.length; i += LIMIT) {
|
||||
await bot.sendMessage(chatId, fullText.slice(i, i + LIMIT), {
|
||||
parse_mode: 'HTML',
|
||||
disable_web_page_preview: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
80
src/hh/handlers/auth.ts
Normal file
80
src/hh/handlers/auth.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import { listResumes, login, saveResume } from '../scraper.js'
|
||||
import { MAIN_REPLY_KEYBOARD } from '../ui.js'
|
||||
import { getState } from '../state.js'
|
||||
import type { ResumeListItem } from '../types.js'
|
||||
|
||||
export async function doLogin(chatId: number, email: string): Promise<void> {
|
||||
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
||||
try {
|
||||
await login(email, chatId)
|
||||
await prisma.user.upsert({
|
||||
where: { telegramId: chatId },
|
||||
update: { hhEmail: email },
|
||||
create: { telegramId: chatId, hhEmail: email, Settings: { create: {} } },
|
||||
})
|
||||
|
||||
const state = getState(chatId)
|
||||
|
||||
let resumes: ResumeListItem[] | null = null
|
||||
try {
|
||||
resumes = await listResumes(chatId)
|
||||
}
|
||||
catch {
|
||||
await bot.sendMessage(chatId, '⚠️ Не удалось загрузить резюме — выбери вручную через меню')
|
||||
}
|
||||
|
||||
await bot.sendMessage(chatId, '✅ Вход выполнен!', { reply_markup: MAIN_REPLY_KEYBOARD })
|
||||
|
||||
if (resumes === null) {
|
||||
// таймаут при загрузке резюме
|
||||
}
|
||||
else if (resumes.length === 0) {
|
||||
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
||||
}
|
||||
else if (resumes.length === 1) {
|
||||
await saveResume(chatId, resumes[0])
|
||||
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
||||
}
|
||||
else {
|
||||
state.pendingResumes = resumes
|
||||
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]),
|
||||
[{ text: '◀️ Закрыть', callback_data: 'hh_back' }],
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLogin(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
|
||||
state.awaitingEmail = true
|
||||
|
||||
if (!user?.hhEmail) {
|
||||
await bot.sendMessage(chatId, '📧 Введи email от hh.ru:')
|
||||
}
|
||||
else {
|
||||
const prompt = await bot.sendMessage(
|
||||
chatId,
|
||||
`📧 Текущий email: <b>${user.hhEmail}</b>\n\nИспользовать его или введи другой:`,
|
||||
{
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: `✅ Войти как ${user.hhEmail}`, callback_data: 'hh_login_use_current' },
|
||||
]],
|
||||
},
|
||||
},
|
||||
)
|
||||
state.loginPromptMessageId = prompt.message_id
|
||||
}
|
||||
}
|
||||
37
src/hh/handlers/info.ts
Normal file
37
src/hh/handlers/info.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import { checkIsAuth } from '../scraper.js'
|
||||
import { getState } from '../state.js'
|
||||
import { escapeHtml } from '../ui.js'
|
||||
|
||||
export async function handleStatus(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
|
||||
const isAuth = await checkIsAuth(chatId)
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
`⚙️ Настройки:\n\nЗапрос: ${settings?.searchQuery ?? '--'}\nМакс откликов: ${settings?.maxApplies ?? '--'}\nАвто: ${state.autoCron ? '✅ включено' : '❌ выключено'}\nАвторизован: ${isAuth ? '✅' : '❌'}`,
|
||||
{ reply_markup: { inline_keyboard: [] } },
|
||||
)
|
||||
}
|
||||
|
||||
export async function handleSkipped(chatId: number): Promise<void> {
|
||||
const skipped = await prisma.skippedVacancy.findMany({
|
||||
where: { telegramId: chatId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
})
|
||||
|
||||
if (!skipped.length) {
|
||||
await bot.sendMessage(chatId, '✅ Проблемных вакансий нет')
|
||||
return
|
||||
}
|
||||
|
||||
const lines = ['🚫 <b>Вакансии с опросником (бот не может откликнуться):</b>', '']
|
||||
skipped.forEach(v => lines.push(`• <a href="${escapeHtml(v.href)}">${escapeHtml(v.title)}</a>`))
|
||||
await bot.sendMessage(chatId, lines.join('\n'), {
|
||||
parse_mode: 'HTML',
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: { inline_keyboard: [] },
|
||||
})
|
||||
}
|
||||
100
src/hh/handlers/resume.ts
Normal file
100
src/hh/handlers/resume.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import { listResumes, NoResumeError, saveResume } from '../scraper.js'
|
||||
import { getState } from '../state.js'
|
||||
import { escapeHtml, NO_RESUME_MARKUP, safeEdit } from '../ui.js'
|
||||
|
||||
export async function handleResumeList(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
const loadingMsg = await bot.sendMessage(chatId, '🔄 Загружаю список резюме...')
|
||||
|
||||
let resumes
|
||||
try {
|
||||
resumes = await listResumes(chatId)
|
||||
console.log(`[handleResumeList ${chatId}]: ${resumes}`)
|
||||
}
|
||||
catch (e) {
|
||||
await bot.deleteMessage(chatId, loadingMsg.message_id).catch(() => {})
|
||||
if (e instanceof NoResumeError) {
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
'📝 Резюме не найдено.\n\nСоздайте резюме на <a href="https://hh.ru/applicant/resumes/new">hh.ru</a>, затем нажмите <b>Повторить</b>.',
|
||||
{ parse_mode: 'HTML', reply_markup: NO_RESUME_MARKUP },
|
||||
)
|
||||
}
|
||||
else {
|
||||
await bot.sendMessage(chatId, '❌ Не удалось загрузить резюме. Попробуйте войти заново через «Войти на hh.ru».')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
await bot.deleteMessage(chatId, loadingMsg.message_id).catch(() => {})
|
||||
|
||||
if (resumes.length === 0) {
|
||||
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
||||
}
|
||||
else if (resumes.length === 1) {
|
||||
await saveResume(chatId, resumes[0])
|
||||
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
||||
}
|
||||
else {
|
||||
state.pendingResumes = resumes
|
||||
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]),
|
||||
[{ text: '◀️ Закрыть', callback_data: 'hh_back' }],
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleMyResume(chatId: number): Promise<void> {
|
||||
const settings = await prisma.settings.findUnique({ where: { telegramId: chatId } })
|
||||
const resume = settings?.selectedResumeId
|
||||
? await prisma.resume.findUnique({ where: { id: settings.selectedResumeId } })
|
||||
: await prisma.resume.findFirst({ where: { telegramId: chatId } })
|
||||
|
||||
if (!resume) {
|
||||
await bot.sendMessage(chatId, '📋 Резюме не найдено.\n\nВыбери резюме через кнопку 📄 Выбрать резюме.')
|
||||
return
|
||||
}
|
||||
|
||||
const MAX = 3500
|
||||
const text = resume.data.length > MAX
|
||||
? `${resume.data.slice(0, MAX)}\n\n… (текст обрезан)`
|
||||
: resume.data
|
||||
|
||||
await bot.sendMessage(
|
||||
chatId,
|
||||
`📋 <b>Твоё резюме:</b>\n<b>${resume.title}</b>\n<pre>${escapeHtml(text)}</pre>`,
|
||||
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|
||||
)
|
||||
}
|
||||
|
||||
export async function handleResumePick(chatId: number, messageId: number, idx: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
const resume = state.pendingResumes[idx]
|
||||
if (!resume) {
|
||||
await safeEdit('❌ Резюме не найдено, попробуйте снова', {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: { inline_keyboard: [] },
|
||||
})
|
||||
return
|
||||
}
|
||||
await safeEdit('🔄 Сохраняю резюме...', {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: { inline_keyboard: [] },
|
||||
})
|
||||
await saveResume(chatId, resume)
|
||||
state.pendingResumes = []
|
||||
await safeEdit(`✅ Резюме выбрано: ${resume.title}`, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
reply_markup: { inline_keyboard: [] },
|
||||
// reply_markup: BACK_MARKUP,
|
||||
})
|
||||
}
|
||||
78
src/hh/handlers/settings.ts
Normal file
78
src/hh/handlers/settings.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import bot from '@bot'
|
||||
import prisma from '@prisma'
|
||||
import cron from 'node-cron'
|
||||
import { SETTINGS_REPLY_KEYBOARD, escapeHtml } from '../ui.js'
|
||||
import { getState } from '../state.js'
|
||||
|
||||
export async function handleQuery(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
state.awaitingQuery = true
|
||||
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
|
||||
const currentQuery = settings?.searchQuery || '--'
|
||||
const msg = await bot.sendMessage(
|
||||
chatId,
|
||||
`🔍 Текущий запрос: <b>${escapeHtml(currentQuery)}</b>\n\nВведи новый или оставь текущий:`,
|
||||
{
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: `✅ Оставить «${currentQuery}»`, callback_data: 'hh_keep_query' },
|
||||
]],
|
||||
},
|
||||
},
|
||||
)
|
||||
state.queryPromptMessageId = msg.message_id
|
||||
}
|
||||
|
||||
export async function handleMax(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
state.awaitingMax = true
|
||||
const settings = await prisma.settings.findFirst({ where: { telegramId: chatId } })
|
||||
const currentMax = settings?.maxApplies ?? '--'
|
||||
const msg = await bot.sendMessage(
|
||||
chatId,
|
||||
`🔢 Текущее значение: <b>${currentMax}</b>\n\nВведи новое количество откликов (1–50) или оставь текущее:`,
|
||||
{
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: `✅ Оставить ${currentMax}`, callback_data: 'hh_keep_max' },
|
||||
]],
|
||||
},
|
||||
},
|
||||
)
|
||||
state.maxPromptMessageId = msg.message_id
|
||||
}
|
||||
|
||||
export async function handlePrompt(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
state.awaitingPrompt = true
|
||||
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
|
||||
const currentPrompt = user?.prompt
|
||||
const text = currentPrompt
|
||||
? `📝 Текущий промт:\n<pre>${escapeHtml(currentPrompt)}</pre>\n\nВведи новый или оставь текущий:`
|
||||
: '📝 Введи промт для AI (пока не задан):'
|
||||
const keepButton = currentPrompt
|
||||
? [[{ text: '✅ Оставить текущий промт', callback_data: 'hh_keep_prompt' }]]
|
||||
: []
|
||||
const msg = await bot.sendMessage(chatId, text, {
|
||||
parse_mode: 'HTML',
|
||||
reply_markup: { inline_keyboard: keepButton },
|
||||
})
|
||||
state.promptPromptMessageId = msg.message_id
|
||||
}
|
||||
|
||||
export async function handleAutoToggle(chatId: number): Promise<void> {
|
||||
const state = getState(chatId)
|
||||
if (state.autoCron) {
|
||||
state.autoCron.stop()
|
||||
state.autoCron = null
|
||||
await bot.sendMessage(chatId, '⛔ Авто остановлен', { reply_markup: SETTINGS_REPLY_KEYBOARD })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user