mirror of
https://github.com/hempyhemp/hh-auto-reply.git
synced 2026-06-08 18:04:57 +00:00
🛠️ refactor(file/topic): Рефакторинг кода для показа меню сообщений.
This commit is contained in:
@@ -1,61 +1,11 @@
|
|||||||
import bot from '@bot'
|
import bot from '@bot'
|
||||||
import { triggerHHStart } from '@/hh/bot-commands'
|
import { triggerHHStart } from '@/hh/bot-commands'
|
||||||
import { askLLM } from '@/openai'
|
|
||||||
|
|
||||||
export function sendMenu(chatId: number, isFirstTime: boolean) {
|
|
||||||
const text = isFirstTime
|
|
||||||
? '👋 Добро пожаловать! Это твой первый вход'
|
|
||||||
: '💼 Главное меню'
|
|
||||||
|
|
||||||
return bot.sendMessage(chatId, text, {
|
|
||||||
reply_markup: {
|
|
||||||
keyboard: [
|
|
||||||
[{ text: '💼 Меню' }],
|
|
||||||
[{ text: '👤 Debug' }],
|
|
||||||
],
|
|
||||||
resize_keyboard: true,
|
|
||||||
one_time_keyboard: false,
|
|
||||||
is_persistent: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.on('message', async (msg) => {
|
bot.on('message', async (msg) => {
|
||||||
const chatId = msg.chat.id
|
if (!msg.text || msg.text.startsWith('/'))
|
||||||
const text = msg.text
|
|
||||||
|
|
||||||
if (!text || text.startsWith('/'))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
switch (text) {
|
if (msg.text === '💼 Меню') {
|
||||||
case '💼 Меню':
|
await triggerHHStart(msg.chat.id)
|
||||||
|
|
||||||
triggerHHStart(chatId)
|
|
||||||
break
|
|
||||||
|
|
||||||
case '👤 Debug': {
|
|
||||||
askLLM('Привет, как дела?')
|
|
||||||
// const resume = await getResume(chatId)
|
|
||||||
//
|
|
||||||
// await bot.sendMessage(chatId, resume || '--')
|
|
||||||
|
|
||||||
// const letter = await askGPT('test')
|
|
||||||
//
|
|
||||||
// const user = await prisma.user.findUnique({
|
|
||||||
// where: { telegramId: BigInt(chatId) },
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// await bot.sendMessage(
|
|
||||||
// chatId,
|
|
||||||
// `👤:\n\n
|
|
||||||
// Username: @${user?.username ?? 'нет'}\n
|
|
||||||
// Письмо: ${letter}\n
|
|
||||||
// HH Email: ${user?.hhEmail}\n
|
|
||||||
// Создан: ${user?.createdAt}
|
|
||||||
//
|
|
||||||
// `,
|
|
||||||
// )
|
|
||||||
// break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import prisma from '@prisma'
|
|||||||
import cron, { type ScheduledTask } from 'node-cron'
|
import cron, { type ScheduledTask } from 'node-cron'
|
||||||
import { applyToJobs, checkIsAuth, listResumes, login, type ResumeListItem, saveResume } from './scraper.js'
|
import { applyToJobs, checkIsAuth, listResumes, login, type ResumeListItem, saveResume } from './scraper.js'
|
||||||
|
|
||||||
|
function escapeHtml(text: string): string {
|
||||||
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
}
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
autoCron: ScheduledTask | null
|
autoCron: ScheduledTask | null
|
||||||
awaitingEmail: boolean
|
awaitingEmail: boolean
|
||||||
@@ -10,6 +14,8 @@ interface UserState {
|
|||||||
awaitingMax: boolean
|
awaitingMax: boolean
|
||||||
tempEmail: string
|
tempEmail: string
|
||||||
pendingResumes: ResumeListItem[]
|
pendingResumes: ResumeListItem[]
|
||||||
|
menuMessageId: number | null
|
||||||
|
loginPromptMessageId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUserState(): UserState {
|
function makeUserState(): UserState {
|
||||||
@@ -20,6 +26,8 @@ function makeUserState(): UserState {
|
|||||||
awaitingMax: false,
|
awaitingMax: false,
|
||||||
tempEmail: '',
|
tempEmail: '',
|
||||||
pendingResumes: [],
|
pendingResumes: [],
|
||||||
|
menuMessageId: null,
|
||||||
|
loginPromptMessageId: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,24 +39,9 @@ function getState(chatId: number): UserState {
|
|||||||
return states.get(chatId)!
|
return states.get(chatId)!
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendResumeSelector(chatId: number, resumes: ResumeListItem[]) {
|
const MAIN_MARKUP = {
|
||||||
getState(chatId).pendingResumes = resumes
|
|
||||||
await bot.sendMessage(chatId, '📄 Выбери резюме:', {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: resumes.map((r, i) => [
|
|
||||||
{ text: r.title, callback_data: `hh_resume_pick_${i}` },
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function triggerHHStart(chatId: number): void {
|
|
||||||
bot.sendMessage(chatId, '🤖 HH Auto-Apply', {
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[
|
[{ text: '🚀 Откликнуться сейчас', callback_data: 'hh_apply' }],
|
||||||
{ text: '🚀 Откликнуться сейчас', callback_data: 'hh_apply' },
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
{ text: '🔍 Изменить запрос', callback_data: 'hh_query' },
|
{ text: '🔍 Изменить запрос', callback_data: 'hh_query' },
|
||||||
{ text: '🔢 Макс откликов', callback_data: 'hh_max' },
|
{ text: '🔢 Макс откликов', callback_data: 'hh_max' },
|
||||||
@@ -63,12 +56,99 @@ export function triggerHHStart(chatId: number): void {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ text: '📄 Выбрать резюме', callback_data: 'hh_resume_list' },
|
{ text: '📄 Выбрать резюме', callback_data: 'hh_resume_list' },
|
||||||
|
{ text: '📋 Моё резюме', callback_data: 'hh_my_resume' },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACK_MARKUP = {
|
||||||
|
inline_keyboard: [[{ text: '◀️ Назад', callback_data: 'hh_back' }]],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редактирует существующее сообщение-меню или отправляет новое
|
||||||
|
async function showMenu(chatId: number, messageId?: number | null): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
const targetId = messageId ?? state.menuMessageId
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
try {
|
||||||
|
await bot.editMessageText('🤖 HH Auto-Apply', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: targetId,
|
||||||
|
reply_markup: MAIN_MARKUP,
|
||||||
|
})
|
||||||
|
state.menuMessageId = targetId
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Сообщение устарело или недоступно — отправим новое
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = await bot.sendMessage(chatId, '🤖 HH Auto-Apply', {
|
||||||
|
reply_markup: MAIN_MARKUP,
|
||||||
|
})
|
||||||
|
state.menuMessageId = msg.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Редактирует сообщение с меню: показывает текст + кнопку "Назад"
|
||||||
|
async function showResult(chatId: number, messageId: number, text: string): Promise<void> {
|
||||||
|
await bot.editMessageText(text, {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: BACK_MARKUP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendResumeSelector(chatId: number, resumes: ResumeListItem[], messageId: number): Promise<void> {
|
||||||
|
const state = getState(chatId)
|
||||||
|
state.pendingResumes = resumes
|
||||||
|
await bot.editMessageText('📄 Выбери резюме:', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
...resumes.map((r, i) => [{ text: r.title, callback_data: `hh_resume_pick_${i}` }]),
|
||||||
|
[{ text: '◀️ Назад', callback_data: 'hh_back' }],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doLogin(chatId: number, email: string): Promise<void> {
|
||||||
|
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
||||||
|
try {
|
||||||
|
await login(email, chatId)
|
||||||
|
await prisma.user.update({ where: { telegramId: chatId }, data: { hhEmail: email } })
|
||||||
|
|
||||||
|
const resumes = await listResumes(chatId)
|
||||||
|
const state = getState(chatId)
|
||||||
|
|
||||||
|
if (resumes.length === 0) {
|
||||||
|
await bot.sendMessage(chatId, '⚠️ Резюме не найдены. Создайте резюме на hh.ru')
|
||||||
|
}
|
||||||
|
else if (resumes.length === 1) {
|
||||||
|
await saveResume(chatId, resumes[0].href)
|
||||||
|
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
||||||
|
}
|
||||||
|
else if (state.menuMessageId) {
|
||||||
|
await sendResumeSelector(chatId, resumes, state.menuMessageId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.sendMessage(chatId, '✅ Авторизован! Куки сохранены.')
|
||||||
|
await showMenu(chatId)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
await bot.sendMessage(chatId, `❌ Ошибка: ${(e as Error).message}`)
|
||||||
|
await showMenu(chatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function triggerHHStart(chatId: number): Promise<void> {
|
||||||
|
await showMenu(chatId)
|
||||||
|
}
|
||||||
|
|
||||||
export function registerHHCommands() {
|
export function registerHHCommands() {
|
||||||
bot.onText(/\/hhstart/, (msg) => {
|
bot.onText(/\/hhstart/, (msg) => {
|
||||||
triggerHHStart(msg.chat.id)
|
triggerHHStart(msg.chat.id)
|
||||||
@@ -77,91 +157,194 @@ export function registerHHCommands() {
|
|||||||
bot.on('callback_query', async (query) => {
|
bot.on('callback_query', async (query) => {
|
||||||
if (!query.message)
|
if (!query.message)
|
||||||
return
|
return
|
||||||
|
|
||||||
const chatId = query.message.chat.id
|
const chatId = query.message.chat.id
|
||||||
|
const messageId = query.message.message_id
|
||||||
const state = getState(chatId)
|
const state = getState(chatId)
|
||||||
|
|
||||||
bot.answerCallbackQuery(query.id)
|
await bot.answerCallbackQuery(query.id)
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { telegramId: chatId },
|
where: { telegramId: chatId },
|
||||||
include: { Settings: true },
|
include: { Settings: true },
|
||||||
})
|
})
|
||||||
|
|
||||||
const settings = user!.Settings!
|
const settings = user!.Settings!
|
||||||
|
|
||||||
switch (query.data) {
|
switch (query.data) {
|
||||||
case 'hh_apply':
|
case 'hh_back':
|
||||||
await bot.sendMessage(chatId, `🚀 Ищу: "${settings.searchQuery}"...`)
|
await showMenu(chatId, messageId)
|
||||||
applyToJobs({ query: settings.searchQuery, maxApplies: settings.maxApplies }, {
|
break
|
||||||
chatId,
|
|
||||||
}).then((result) => {
|
case 'hh_apply': {
|
||||||
if (result.error)
|
await bot.editMessageText(`🔄 Ищу вакансии по запросу "${settings.searchQuery}"...`, {
|
||||||
return bot.sendMessage(chatId, `❌ ${result.error}`)
|
chat_id: chatId,
|
||||||
const lines = result.applied.map((v, i) => `${i + 1}. ${v}`).join('\n')
|
message_id: messageId,
|
||||||
bot.sendMessage(chatId, `✅ Откликнулся: ${result.applied.length}\n${lines}\n\n`
|
reply_markup: { inline_keyboard: [] },
|
||||||
+ `⏭ Пропущено: ${result.skipped.length}\n${
|
})
|
||||||
result.errors.length ? `❌ Ошибок: ${result.errors.length}` : ''}`)
|
state.menuMessageId = null
|
||||||
|
|
||||||
|
applyToJobs({ query: settings.searchQuery, maxApplies: settings.maxApplies }, { chatId })
|
||||||
|
.then(async (result) => {
|
||||||
|
if (result.error) {
|
||||||
|
await bot.sendMessage(chatId, `❌ ${result.error}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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}">${v.title}</a> — ${v.message}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
await bot.sendMessage(chatId, lines.join('\n'), {
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
disable_web_page_preview: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await showMenu(chatId)
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'hh_status':
|
case 'hh_status': {
|
||||||
await bot.sendMessage(chatId, `⚙️ Настройки:\n
|
const isAuth = await checkIsAuth(chatId)
|
||||||
Запрос: ${settings.searchQuery}\n
|
await showResult(
|
||||||
Макс откликов: ${settings.maxApplies}\n
|
chatId,
|
||||||
Авто: ${state.autoCron ? '✅ включено' : '❌ выключено'}\n
|
messageId,
|
||||||
Авторизован: ${await checkIsAuth(chatId)}`)
|
`⚙️ Настройки:\n\nЗапрос: ${settings.searchQuery}\nМакс откликов: ${settings.maxApplies}\nАвто: ${state.autoCron ? '✅ включено' : '😬 выключено'}\nАвторизован: ${isAuth}`,
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'hh_my_resume': {
|
||||||
|
const resume = await prisma.resume.findFirst({
|
||||||
|
where: { telegramId: chatId },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
})
|
||||||
|
if (!resume) {
|
||||||
|
await showResult(chatId, messageId, '📋 Резюме не найдено.\n\nВыбери резюме через кнопку 📄 Выбрать резюме.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX = 3800
|
||||||
|
const text = resume.data.length > MAX
|
||||||
|
? `${resume.data.slice(0, MAX)}\n\n… (текст обрезан)`
|
||||||
|
: resume.data
|
||||||
|
|
||||||
|
await bot.editMessageText(
|
||||||
|
`📋 <b>Твоё резюме</b>\n<pre>${escapeHtml(text)}</pre>`,
|
||||||
|
{
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: BACK_MARKUP,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'hh_login':
|
case 'hh_login':
|
||||||
state.awaitingEmail = true
|
state.menuMessageId = messageId
|
||||||
if (!user?.hhEmail) {
|
if (!user?.hhEmail) {
|
||||||
|
state.awaitingEmail = true
|
||||||
await bot.sendMessage(chatId, '📧 Введи email от hh.ru:')
|
await bot.sendMessage(chatId, '📧 Введи email от hh.ru:')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await bot.sendMessage(chatId, `📧 Email от hh.ru: ${user?.hhEmail}`)
|
state.awaitingEmail = true
|
||||||
|
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
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'hh_login_use_current': {
|
||||||
|
state.awaitingEmail = false
|
||||||
|
await bot.deleteMessage(chatId, messageId).catch(() => {})
|
||||||
|
state.loginPromptMessageId = null
|
||||||
|
const email = user?.hhEmail
|
||||||
|
if (!email) {
|
||||||
|
await bot.sendMessage(chatId, '❌ Email не найден, введи вручную')
|
||||||
|
state.awaitingEmail = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
await doLogin(chatId, email)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'hh_query':
|
case 'hh_query':
|
||||||
state.awaitingQuery = true
|
state.awaitingQuery = true
|
||||||
|
state.menuMessageId = messageId
|
||||||
await bot.sendMessage(chatId, '🔍 Введи поисковый запрос:')
|
await bot.sendMessage(chatId, '🔍 Введи поисковый запрос:')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'hh_max':
|
case 'hh_max':
|
||||||
state.awaitingMax = true
|
state.awaitingMax = true
|
||||||
|
state.menuMessageId = messageId
|
||||||
await bot.sendMessage(chatId, '🔢 Введи максимальное количество откликов (1-50):')
|
await bot.sendMessage(chatId, '🔢 Введи максимальное количество откликов (1-50):')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'hh_auto_start':
|
case 'hh_auto_start':
|
||||||
if (state.autoCron) {
|
if (state.autoCron) {
|
||||||
await bot.sendMessage(chatId, 'Уже запущено!')
|
await showResult(chatId, messageId, '⚠️ Авто уже запущено')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
state.autoCron = cron.schedule('0 10 * * 1-5', async () => {
|
state.autoCron = cron.schedule('0 10 * * 1-5', async () => {
|
||||||
await bot.sendMessage(chatId, '⏰ Авто-отклик...')
|
await bot.sendMessage(chatId, '⏰ Авто-отклик...')
|
||||||
})
|
})
|
||||||
await bot.sendMessage(chatId, '✅ Авто включён (пн-пт, 10:00)')
|
await showResult(chatId, messageId, '✅ Авто включён (пн-пт, 10:00)')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'hh_auto_stop':
|
case 'hh_auto_stop':
|
||||||
state.autoCron?.stop()
|
state.autoCron?.stop()
|
||||||
state.autoCron = null
|
state.autoCron = null
|
||||||
await bot.sendMessage(chatId, '⛔ Авто остановлен')
|
await showResult(chatId, messageId, '⛔ Авто остановлен')
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'hh_resume_list': {
|
case 'hh_resume_list': {
|
||||||
await bot.sendMessage(chatId, '🔄 Загружаю список резюме...')
|
await bot.editMessageText('🔄 Загружаю список резюме...', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: { inline_keyboard: [] },
|
||||||
|
})
|
||||||
const resumes = await listResumes(chatId)
|
const resumes = await listResumes(chatId)
|
||||||
if (resumes.length === 0) {
|
if (resumes.length === 0) {
|
||||||
await bot.sendMessage(chatId, '❌ Резюме не найдены. Создайте резюме на hh.ru')
|
await showResult(chatId, messageId, '😬 Резюме не найдены. Создайте резюме на hh.ru')
|
||||||
}
|
}
|
||||||
else if (resumes.length === 1) {
|
else if (resumes.length === 1) {
|
||||||
await bot.sendMessage(chatId, '🔄 Сохраняю резюме...')
|
await bot.editMessageText('🔄 Сохраняю резюме...', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: { inline_keyboard: [] },
|
||||||
|
})
|
||||||
await saveResume(chatId, resumes[0].href)
|
await saveResume(chatId, resumes[0].href)
|
||||||
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
await showResult(chatId, messageId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await sendResumeSelector(chatId, resumes)
|
state.menuMessageId = messageId
|
||||||
|
await sendResumeSelector(chatId, resumes, messageId)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -171,13 +354,17 @@ export function registerHHCommands() {
|
|||||||
const idx = Number(query.data.replace('hh_resume_pick_', ''))
|
const idx = Number(query.data.replace('hh_resume_pick_', ''))
|
||||||
const resume = state.pendingResumes[idx]
|
const resume = state.pendingResumes[idx]
|
||||||
if (!resume) {
|
if (!resume) {
|
||||||
await bot.sendMessage(chatId, '❌ Резюме не найдено, попробуйте снова')
|
await showResult(chatId, messageId, '😬 Резюме не найдено, попробуйте снова')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
await bot.sendMessage(chatId, '🔄 Сохраняю резюме...')
|
await bot.editMessageText('🔄 Сохраняю резюме...', {
|
||||||
|
chat_id: chatId,
|
||||||
|
message_id: messageId,
|
||||||
|
reply_markup: { inline_keyboard: [] },
|
||||||
|
})
|
||||||
await saveResume(chatId, resume.href)
|
await saveResume(chatId, resume.href)
|
||||||
await bot.sendMessage(chatId, `✅ Резюме выбрано: ${resume.title}`)
|
|
||||||
state.pendingResumes = []
|
state.pendingResumes = []
|
||||||
|
await showResult(chatId, messageId, `✅ Резюме выбрано: ${resume.title}`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -198,62 +385,42 @@ export function registerHHCommands() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (state.awaitingEmail) {
|
if (state.awaitingEmail) {
|
||||||
state.tempEmail = user?.hhEmail || msg.text
|
|
||||||
state.awaitingEmail = false
|
state.awaitingEmail = false
|
||||||
|
|
||||||
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
await bot.sendMessage(chatId, '🔄 Логинюсь...')
|
if (state.loginPromptMessageId) {
|
||||||
try {
|
await bot.deleteMessage(chatId, state.loginPromptMessageId).catch(() => {})
|
||||||
await login(state.tempEmail, chatId)
|
state.loginPromptMessageId = null
|
||||||
await bot.sendMessage(chatId, '✅ Авторизован! Куки сохранены.')
|
|
||||||
|
|
||||||
await prisma.user.update({
|
|
||||||
where: { telegramId: chatId },
|
|
||||||
data: { hhEmail: state.tempEmail },
|
|
||||||
})
|
|
||||||
|
|
||||||
const resumes = await listResumes(chatId)
|
|
||||||
if (resumes.length === 0) {
|
|
||||||
await bot.sendMessage(chatId, '❌ Резюме не найдены. Создайте резюме на hh.ru')
|
|
||||||
}
|
|
||||||
else if (resumes.length === 1) {
|
|
||||||
await saveResume(chatId, resumes[0].href)
|
|
||||||
await bot.sendMessage(chatId, `✅ Резюме сохранено: ${resumes[0].title}`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await sendResumeSelector(chatId, resumes)
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerHHStart(chatId)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
await bot.sendMessage(chatId, `😬 Ошибка: ${(e as Error).message}`)
|
|
||||||
}
|
}
|
||||||
|
await doLogin(chatId, msg.text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.awaitingQuery) {
|
if (state.awaitingQuery) {
|
||||||
state.awaitingQuery = false
|
state.awaitingQuery = false
|
||||||
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
const updated = await prisma.settings.update({
|
const updated = await prisma.settings.update({
|
||||||
where: { telegramId: chatId },
|
where: { telegramId: chatId },
|
||||||
data: { searchQuery: msg.text },
|
data: { searchQuery: msg.text },
|
||||||
})
|
})
|
||||||
await bot.sendMessage(chatId, `✅ Запрос: "${updated.searchQuery}"`)
|
await bot.sendMessage(chatId, `✅ Запрос: "${updated.searchQuery}"`)
|
||||||
|
await showMenu(chatId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.awaitingMax) {
|
if (state.awaitingMax) {
|
||||||
state.awaitingMax = false
|
|
||||||
const num = Number(msg.text)
|
const num = Number(msg.text)
|
||||||
if (num < 1 || num > 50) {
|
if (Number.isNaN(num) || num < 1 || num > 50) {
|
||||||
await bot.sendMessage(chatId, '❌ Число от 1 до 50')
|
await bot.sendMessage(chatId, '😬 Введи число от 1 до 50:')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
state.awaitingMax = false
|
||||||
|
await bot.deleteMessage(chatId, msg.message_id).catch(() => {})
|
||||||
const updated = await prisma.settings.update({
|
const updated = await prisma.settings.update({
|
||||||
where: { telegramId: chatId },
|
where: { telegramId: chatId },
|
||||||
data: { maxApplies: num },
|
data: { maxApplies: num },
|
||||||
})
|
})
|
||||||
await bot.sendMessage(chatId, `✅ Макс откликов: ${updated.maxApplies}`)
|
await bot.sendMessage(chatId, `✅ Макс откликов: ${updated.maxApplies}`)
|
||||||
|
await showMenu(chatId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ interface ApplyOptions {
|
|||||||
maxApplies?: number
|
maxApplies?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VacancyRef {
|
||||||
|
title: string
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ApplyResult {
|
interface ApplyResult {
|
||||||
applied: string[]
|
applied: VacancyRef[]
|
||||||
skipped: string[]
|
skipped: VacancyRef[]
|
||||||
errors: string[]
|
errors: Array<VacancyRef & { message: string }>
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +81,7 @@ export async function login(
|
|||||||
await bot.sendMessage(chatId, `Browser is connected: ${browser.isConnected()}`)
|
await bot.sendMessage(chatId, `Browser is connected: ${browser.isConnected()}`)
|
||||||
if (!browser.version())
|
if (!browser.version())
|
||||||
return
|
return
|
||||||
await bot.sendMessage(chatId, `Browser version: ${browser.version()}`)
|
// await bot.sendMessage(chatId, `Browser version: ${browser.version()}`)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
@@ -136,7 +141,12 @@ export async function login(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await bot.sendMessage(chatId, `cookies: ${cookies.length}`)
|
if (cookies.length > 0) {
|
||||||
|
await bot.sendMessage(chatId, `✅ Авторизация выполнена`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await bot.sendMessage(chatId, `😬 Произошла ошибка`)
|
||||||
|
}
|
||||||
|
|
||||||
await browser.close()
|
await browser.close()
|
||||||
}
|
}
|
||||||
@@ -230,6 +240,25 @@ export async function applyToJobs({
|
|||||||
const page = await context.newPage()
|
const page = await context.newPage()
|
||||||
const results: ApplyResult = { applied: [], skipped: [], errors: [] }
|
const results: ApplyResult = { applied: [], skipped: [], errors: [] }
|
||||||
|
|
||||||
|
let statusMsgId: number | null = null
|
||||||
|
|
||||||
|
async function status(text: string): Promise<void> {
|
||||||
|
if (statusMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, statusMsgId).catch(() => {})
|
||||||
|
statusMsgId = null
|
||||||
|
}
|
||||||
|
const msg = await bot.sendMessage(chatId, text)
|
||||||
|
statusMsgId = msg.message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function keep(text: string): Promise<void> {
|
||||||
|
if (statusMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, statusMsgId).catch(() => {})
|
||||||
|
statusMsgId = null
|
||||||
|
}
|
||||||
|
await bot.sendMessage(chatId, text, { parse_mode: 'HTML' })
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadSession(page, chatId)
|
await loadSession(page, chatId)
|
||||||
|
|
||||||
@@ -237,12 +266,11 @@ export async function applyToJobs({
|
|||||||
await page.goto(url, { waitUntil: 'networkidle' })
|
await page.goto(url, { waitUntil: 'networkidle' })
|
||||||
|
|
||||||
const isLoggedIn = await page.$('[data-qa="profileAndResumes-button"]')
|
const isLoggedIn = await page.$('[data-qa="profileAndResumes-button"]')
|
||||||
// await page.$('[data-qa="mainmenu_myResumes"]')
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return { ...results, error: 'Не авторизован. Выполните login' }
|
return { ...results, error: 'Не авторизован. Выполните login' }
|
||||||
}
|
}
|
||||||
|
|
||||||
await bot.sendMessage(chatId, `✅ Авторизация выполнена`)
|
await status(`✅ Авторизация выполнена`)
|
||||||
|
|
||||||
const vacancies = await page.$$eval(
|
const vacancies = await page.$$eval(
|
||||||
'[data-qa="serp-item__title"]',
|
'[data-qa="serp-item__title"]',
|
||||||
@@ -252,65 +280,49 @@ export async function applyToJobs({
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
await bot.sendMessage(chatId, `✅ Вакансий найдено: ${vacancies.length}`)
|
await status(`✅ Вакансий найдено: ${vacancies.length}`)
|
||||||
|
|
||||||
|
const resume = await prisma.resume.findFirst({ where: { telegramId: chatId } })
|
||||||
|
const user = await prisma.user.findUnique({ where: { telegramId: chatId } })
|
||||||
|
|
||||||
|
if (!resume?.data) {
|
||||||
|
await keep('❌ Резюме не выбрано — выбери резюме через меню')
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
for (const vacancy of vacancies.slice(0, maxApplies)) {
|
for (const vacancy of vacancies.slice(0, maxApplies)) {
|
||||||
|
const ref: VacancyRef = { title: vacancy.title, href: vacancy.href }
|
||||||
try {
|
try {
|
||||||
await bot.sendMessage(chatId, `🔄 Обрабатывается вакансия: ${vacancy.title}`)
|
await status(`🔄 Обрабатывается: ${vacancy.title}`)
|
||||||
await page.goto(vacancy.href, { waitUntil: 'networkidle' })
|
await page.goto(vacancy.href, { waitUntil: 'networkidle' })
|
||||||
|
|
||||||
const description = await page
|
const description = await page
|
||||||
.locator('[data-qa="vacancy-description"]')
|
.locator('[data-qa="vacancy-description"]')
|
||||||
.innerText()
|
.innerText()
|
||||||
|
.catch(() => '')
|
||||||
|
|
||||||
if (!description) {
|
if (!description) {
|
||||||
await bot.sendMessage(chatId, `😬 Ошибка с получением описания`)
|
results.skipped.push(ref)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await bot.sendMessage(chatId, `✅ Описание получено`)
|
await status(`✍️ Генерирую письмо: ${vacancy.title}`)
|
||||||
|
|
||||||
const resume = await prisma.resume.findFirst({
|
const letter = await createMessage(resume.data, description, user!.prompt)
|
||||||
where: { telegramId: chatId },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!resume?.data) {
|
await keep(`✅ <b>${vacancy.title}</b>\n\n${letter}`)
|
||||||
results.errors.push(`${vacancy.title}: резюме не выбрано — выберите резюме через меню`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
results.applied.push(ref)
|
||||||
where: { telegramId: chatId },
|
|
||||||
})
|
|
||||||
|
|
||||||
const letter = await createMessage(resume!.data, description, user!.prompt)
|
|
||||||
|
|
||||||
await bot.sendMessage(chatId, `✅ Сопроводительное письмо отправлено: ${letter}`)
|
|
||||||
|
|
||||||
// const applyBtn = await page.$('[data-qa="vacancy-response-link-top"]')
|
|
||||||
// if (!applyBtn) {
|
|
||||||
// results.skipped.push(vacancy.title)
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await randomScroll(page)
|
|
||||||
//
|
|
||||||
// await applyBtn.click()
|
|
||||||
// await page.waitForTimeout(randomDelay())
|
|
||||||
//
|
|
||||||
// const submitBtn = await page.$('[data-qa="vacancy-response-popup-submit"]')
|
|
||||||
// if (submitBtn) {
|
|
||||||
// await submitBtn.click()
|
|
||||||
// await page.waitForTimeout(randomDelay())
|
|
||||||
// }
|
|
||||||
|
|
||||||
results.applied.push(vacancy.title)
|
|
||||||
// await page.waitForTimeout(3000 + Math.random() * 2000)
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
results.errors.push(`${vacancy.title}: ${(err as Error).message}`)
|
results.errors.push({ ...ref, message: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (statusMsgId) {
|
||||||
|
await bot.deleteMessage(chatId, statusMsgId).catch(() => {})
|
||||||
|
statusMsgId = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
await browser.close()
|
await browser.close()
|
||||||
|
|||||||
21
src/index.ts
21
src/index.ts
@@ -1,7 +1,7 @@
|
|||||||
import bot from '@bot'
|
import bot from '@bot'
|
||||||
import prisma from '@prisma'
|
import prisma from '@prisma'
|
||||||
|
|
||||||
import { sendMenu } from './bot-menu'
|
import { triggerHHStart } from './hh/bot-commands.js'
|
||||||
import { registerHHCommands } from './hh/bot-commands.js'
|
import { registerHHCommands } from './hh/bot-commands.js'
|
||||||
|
|
||||||
registerHHCommands()
|
registerHHCommands()
|
||||||
@@ -10,17 +10,11 @@ bot.onText(/\/start/, async (msg) => {
|
|||||||
const chatId = msg.chat.id
|
const chatId = msg.chat.id
|
||||||
const telegramId = BigInt(chatId)
|
const telegramId = BigInt(chatId)
|
||||||
|
|
||||||
const existingUser = await prisma.user.findUnique({
|
const existingUser = await prisma.user.findUnique({ where: { telegramId } })
|
||||||
where: { telegramId },
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFirstTime = !existingUser
|
|
||||||
|
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: { telegramId },
|
where: { telegramId },
|
||||||
update: {
|
update: { username: msg.from?.username ?? null },
|
||||||
username: msg.from?.username ?? null,
|
|
||||||
},
|
|
||||||
create: {
|
create: {
|
||||||
telegramId,
|
telegramId,
|
||||||
username: msg.from?.username ?? null,
|
username: msg.from?.username ?? null,
|
||||||
@@ -29,7 +23,14 @@ bot.onText(/\/start/, async (msg) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await sendMenu(chatId, isFirstTime)
|
if (!existingUser) {
|
||||||
|
await bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
`👋 Привет, ${msg.from?.first_name ?? 'друг'}!\n\nЭто бот для авто-откликов на hh.ru.\nНачни с логина — нажми 🔑 Логин.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await triggerHHStart(chatId)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Bot started 🚀')
|
console.log('Bot started 🚀')
|
||||||
|
|||||||
@@ -81,25 +81,26 @@ export async function askLLM(userMessage: string) {
|
|||||||
return textPart?.text ?? ''
|
return textPart?.text ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMessage(resume: string, message: string, prompt: string) {
|
export async function createMessage(resume: string, message: string, prompt?: string) {
|
||||||
const client = await getClient()
|
const client = await getClient()
|
||||||
|
|
||||||
const session = await client.session.create({ body: { title: 'Cover letter' } })
|
const session = await client.session.create({ body: { title: 'Cover letter' } })
|
||||||
const sessionId = session.data!.id
|
const sessionId = session.data!.id
|
||||||
|
|
||||||
|
const finalPromt = 'Ты — помощник по написанию сопроводительных писем. Отвечай только текстом самого письма, без вступлений, ремарок и пояснений. Пиши по короче и простыми словами. В конце письма оставляй все контакты для связи.'
|
||||||
// Задаём роль без ответа
|
// Задаём роль без ответа
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
noReply: true,
|
noReply: true,
|
||||||
parts: [{ type: 'text', text: 'Ты — помощник по написанию сопроводительных писем. Отвечай только текстом самого письма, без вступлений, ремарок и пояснений.' }],
|
parts: [{ type: 'text', text: finalPromt }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
// ${prompt}\n\n
|
||||||
const result = await client.session.prompt({
|
const result = await client.session.prompt({
|
||||||
path: { id: sessionId },
|
path: { id: sessionId },
|
||||||
body: {
|
body: {
|
||||||
parts: [{ type: 'text', text: `${prompt}\n\nРезюме:\n${resume}\n\nВакансия:\n${message}` }],
|
parts: [{ type: 'text', text: `Резюме:\n${resume}\n\nВакансия:\n${message}` }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user