UNPKG

naukri-automation-tool

Version:

Naukri automation tool to fetch, filter, and apply for jobs automatically using gen ai.

783 lines (736 loc) 25.7 kB
const { applyJobsAPI, searchJobsAPI, getJobDetailsAPI, getSimJobsAPI, loginAPI, getRecommendedJobsAPI, getProfileDetailsAPI, matchScoreAPI, getResumeAPI, } = require("./api"); const { writeToFile, writeFileData, filterJobs, getDataFromFile, getFileData, getAnswerFromUser, compressProfile, getEmailsIds, } = require("./utils"); const { getGeminiUserConfiguration, answerQuestion } = require("./gemini"); const { localStorage } = require("./helper"); const prompts = require("@inquirer/prompts"); const login = async (profile) => { if (!profile?.creds) { const email = await prompts.input({ message: "Enter your email : " }); const password = await prompts.password({ message: "Enter your password : ", }); profile = { creds: { username: email, password: password } }; } const response = await loginAPI(profile.creds); if (!response.ok) { console.log("There was an error while logging in"); const data = await response.json(); const errors = [ ...(data?.validationErrors || []), ...(data?.fieldValidationErrors || []), ]; errors.forEach((error) => { console.log(`${error.field}: ${error.message} `); }); throw new Error(`HTTP error! status: ${response.status}`); } const cookies = {}; const setCookie = response.headers.get("set-cookie"); const cookie = setCookie.split(";"); cookie?.forEach((cookieStr) => { const [name, value] = cookieStr.split("="); cookies[name] = value; }); // writeToFile(cookies.nauk_at, "accessToken", profile.id); console.log("Logged in successfully"); const loginInfo = { creds: profile.creds, authorization: cookies.nauk_at, }; return loginInfo; }; // apply for jobs in a string array const applyForJobs = async (jobs, applyData) => { //change this code to apply maximum 5 jobs at a time const jobsArr = jobs.map((job) => job.jobId); let bodyStr; if (applyData) { bodyStr = `{"strJobsarr":${JSON.stringify( jobsArr )},"applyData":${JSON.stringify(applyData)}`; } else { bodyStr = `{"strJobsarr":${JSON.stringify(jobsArr)}`; } const response = await applyJobsAPI(bodyStr); const data = await response.json(); if (response.status == 401 || response.status == 403) { console.error(data?.message); throw new Error(response.status); } if (!data.jobs) { throw new Error(`Already applied for job`); } return data; }; const getJobInfo = async (jobIds, batchSize = 5) => { const profile = localStorage.getItem("profile"); const preferences = localStorage.getItem("preferences"); const jobInfo = []; try { for (let i = 0; i < jobIds.length; i += batchSize) { const batch = jobIds.slice(i, i + batchSize); // Process the current batch in parallel const batchPromises = batch.map(async (jobId) => { try { const matchScoreResponse = await matchScoreAPI(jobId); let matchScore = null; if (matchScoreResponse.status === 200) { matchScore = await matchScoreResponse.json(); if ( matchScore.Keyskills == 0 && preferences.matchStrategy === "naukriMatching" ) return null; } const jobDetailsResponse = await getJobDetailsAPI(jobId); if (jobDetailsResponse.status === 200) { const data = await jobDetailsResponse.json(); if (data.jobDetails) { const { salaryDetail, applyCount, minimumExperience, videoProfilePreferred, applyRedirectUrl, vacany, createdDate, jobId, jobRole, companyDetail, description, title, } = data.jobDetails; return { minimumSalary: salaryDetail.minimumSalary, maximumSalary: salaryDetail.maximumSalary, applyCount, minimumExperience, videoProfilePreferred, applyRedirectUrl, vacany, createdDate, jobId, jobRole, companyName: companyDetail.name, description, title, matchScore: matchScore?.Keyskills, }; } } else if (jobDetailsResponse.status === 403) { console.log("403 Forbidden:", jobDetailsResponse); throw new Error("403 Forbidden"); } else if (jobDetailsResponse.status === 303) { const data = await jobDetailsResponse.json(); if (data?.metaSearch?.isExpiredJob === "1") { process.stdout.write("Expired Job \r"); } } else { console.log( `Error fetching job details for Job ID: ${jobId} Status: ${jobDetailsResponse.status}` ); } return null; // Return null if no valid job data is found. } catch (error) { console.error( `Error fetching job details for Job ID: ${jobId}`, error.message ); return null; // Return null on error to avoid failing the entire batch. } }); const batchResults = await Promise.all(batchPromises); // Add the results of the current batch to the overall jobInfo array jobInfo.push(...batchResults.filter((job) => job !== null)); // Print progress after each batch process.stdout.write( `Completed ${jobInfo.length} jobs out of ${jobIds.length} \r` ); } // Write the results to a file writeToFile(jobInfo, "searchedJobs", profile.id); return jobInfo; } catch (error) { console.error("Unexpected error while fetching job info:", error.message); throw error; } }; //Search all the jobs const searchJobs = async (pageNo, keywords, repetitions) => { try { const data = await searchJobsAPI(pageNo, keywords).then(async (results) => { if (results.status == 200) return results.json(); else if (results.status == 403) { console.log("403 Forbidden : " + results.statusText); throw new Error("403 Forbidden"); } else { console.log(await results.json()); } }); if (!data?.jobDetails) { console.log(data); return []; } const jobIds = data.jobDetails.map((job) => job.jobId); // console.log(`Found ${jobIds.length} jobs on page ${pageNo}`); let simillarJobs = [...jobIds]; // console.log(`Searching for simillar jobs for ${repetitions} times`); for (let i = 1; i <= repetitions; i++) { simillarJobs = await searchSimillarJobs(simillarJobs); jobIds.push(...simillarJobs); } return jobIds; } catch (error) { console.error(error.message); if (error.message == "403 Forbidden") throw new Error("403 Forbidden"); return []; } }; const searchSimillarJobs = async (jobIds) => { let simillarJobIds = []; if (jobIds.length == 0) return simillarJobIds; const jobs = []; for (const jobId of jobIds) { const response = await getSimJobsAPI(jobId); if (response.status === 200) { const data = await response.json(); const jobIdsArr = data.simJobDetails.content.map((job) => job.jobId); jobs.push(...jobIdsArr); } else { console.error("Error in fetching similar jobs:"); // console.log(response); throw new Error("403 Forbidden"); } // Wait for 200ms before making the next request await new Promise((resolve) => setTimeout(resolve, 100)); } return jobs; }; const getRecommendedJobs = async () => { const response = await getRecommendedJobsAPI(null); if (response.status !== 200) { return []; } const data = await response.json(); const clusters = data.recommendedClusters?.map( (cluster) => cluster.clusterId ); //["profile", "apply", "preference", "similar_jobs"]; const jobPromises = clusters.map(async (cluster) => { try { const response = await getRecommendedJobsAPI(cluster); if (response.status !== 200) { console.error("Error in fetching recommended jobs:", response); return []; } const data = await response.json(); return data.jobDetails?.map((job) => job.jobId) || []; } catch (error) { console.error(`Error fetching jobs for cluster: ${cluster}`, error.message); return []; // Return an empty array on error to avoid failing all promises. } }); const allJobIds = await Promise.all(jobPromises); const jobIds = allJobIds.flat(); // Flatten the array of arrays into a single array. return jobIds; }; const handleQuestionnaire = async (data, enableGenAi) => { const applyData = {}; const profile = localStorage.getItem("profile"); const questions = (await getDataFromFile("questions", profile.id)) ?? {}; const updatedProfile = compressProfile(profile); for (const job of data.jobs) { const answers = {}; const questionsToBeAnswered = []; job.questionnaire.forEach((question) => { // Some questions have empty options const isEmptyOptions = Object.values(question.answerOption).every( (value) => value === "" ); if (isEmptyOptions) question.answerOption = {}; const uniqueQid = `${question.questionName}_${JSON.stringify( question.answerOption )}`; const que = questions ? questions[uniqueQid] : null; if (que) { answers[question.questionId] = que.answer; que.options = question.answerOption; } else { questionsToBeAnswered.push({ questionId: question.questionId, question: question.questionName, options: question.answerOption, }); } }); if (questionsToBeAnswered.length > 0 && enableGenAi) { const answeredQuestions = await answerQuestion( questionsToBeAnswered, updatedProfile ); answeredQuestions?.forEach((question) => { if ( (typeof question.answer === "string" || typeof question.answer === "number" || question.answer instanceof Array) && question.answer.length !== 0 && Number(question.confidence) > 40 ) { answers[question.questionId] = question.answer; } else { const index = job.questionnaire.findIndex( (que) => que.questionId == question.questionId ); job.questionnaire[index].answer = question.answer; } }); } //get all the questions job.questionnaire which are not present in answers using question id const remainingQuestions = job.questionnaire.filter( (question) => !answers[question.questionId] ); if (remainingQuestions.length > 0) { for (const question of remainingQuestions) { const answer = await getAnswerFromUser(question); answers[question.questionId] = answer; } } // Normalize answers based on question type for (const question of job.questionnaire) { const questionId = question.questionId; const answer = answers[questionId]; if (question.questionType === "Text Box") { answers[questionId] = Array.isArray(answer) ? answer.join(" ") : String(answer || ""); } else if ( ["Check Box", "Radio Button", "List Menu"].includes( question.questionType ) && !Array.isArray(answer) ) { answers[questionId] = [answer].filter(Boolean); // Wrap non-array answer and handle falsy values } } // Add normalized answers to applyData applyData[job.jobId] = { answers }; // Save the questions in the file if (!questions) questions = {}; job.questionnaire.forEach((question) => { const uniqueId = `${question.questionName}_${JSON.stringify( question.answerOption )}`; if (!questions[uniqueId]) { questions[uniqueId] = { questionId: question.questionId, questionName: question.questionName, answerOption: question.answerOption, questionType: question.questionType, answer: answers[question.questionId], }; } }); writeToFile(questions, "questions", profile.id); } return applyData; }; const clearJobs = async () => { const profile = await localStorage.getItem("profile"); console.debug("Clearing jobs"); writeToFile({}, "searchedJobs", profile.id); writeToFile({}, "filteredJobIds", profile.id); }; // main function90 const findNewJobs = async (noOfPages=5, repetitions=1) => { const preferences = await localStorage.getItem("preferences"); const profile = await localStorage.getItem("profile"); clearJobs(); const searchedJobIds = []; const recommendedJobs = getRecommendedJobs(); const promises = []; for (let i = 0; i < noOfPages; i++) { const jobs = searchJobs( i + 1, preferences.desiredRole?.map(encodeURIComponent).join("%2C%20"), repetitions ); promises.push(jobs); } const jobs = await Promise.all(promises); jobs.forEach((job) => { searchedJobIds.push(...job); }); const recommendedJobIds = await recommendedJobs; searchedJobIds.push(...recommendedJobIds); const uniqueJobIds = Array.from(new Set(searchedJobIds)); console.log( `Found total ${uniqueJobIds.length} jobs from ${noOfPages} pages.` ); const jobInfo = await getJobInfo(uniqueJobIds); const emailIds = getEmailsIds(jobInfo, profile.id); const filteredJobs = filterJobs(jobInfo); writeToFile(filteredJobs, "filteredJobIds", profile.id); return filteredJobs; }; const getExistingJobs = async () => { const jobsFromFile = await getDataFromFile("filteredJobIds"); console.log(`Jobs from file : ${jobsFromFile?.length}`); if ( !jobsFromFile || jobsFromFile.length === 0 || Object.keys(jobsFromFile).length === 0 ) return []; const filteredJobs = jobsFromFile?.filter( (job) => !job.isSuitable || job.isApplied ); if (filteredJobs?.length > 0) { console.log("Found jobs from file " + filteredJobs.length); return filteredJobs; } else { console.log("No jobs found in file"); return []; } }; const getUserProfile = async () => { const response = await getProfileDetailsAPI(); const userData = await response.json(); return constructUser(userData); }; const getPreferences = async (user) => { let preferences = await getDataFromFile("preferences", user.id); let doConfiguration = preferences ? false : true; if (preferences) doConfiguration = await prompts.select({ message: "Do you want to configure your preferences ?", choices: [ { name: "No", value: false, description: "Use default preferences", }, { name: "Yes", value: true, description: "Configure your preferences", }, ], }); if (!preferences) { preferences = {}; } let matchStrategy = "naukriMatching"; //doConfiguration ? null : preferences.matchStrategy; if (!matchStrategy) { matchStrategy = await prompts.select({ message: "Select a strategy to match the jobs", choices: [ { name: "Naukri Matching", value: "naukriMatching", description: "Use Naukri Matching strategy to match the jobs", }, { name: "Keywords Matching", value: "keywords", description: "Match the jobs with keywords provided by you and title of the job", }, { name: "AI Matching", value: "ai", description: "Use Gen AI model to match the jobs", }, { name: "Manual Matching", value: "manual", description: "Manually match the jobs with your confirmation", }, ], }); preferences.matchStrategy = matchStrategy; } let enableGenAi = doConfiguration ? null : preferences.enableGenAi; if (enableGenAi === null || enableGenAi === undefined) { let enableGenAi = await prompts.select({ message: "Would you like to enable Gen Ai based question answering ?", choices: [ { name: "Yes", value: true, description: "Use gen ai model to generate answers for questions asked in job application.", }, { name: "No", value: false, description: "Skip gen ai setup. This will skip the jobs which require question answering.", }, ], }); preferences.enableGenAi = false; if (enableGenAi || matchStrategy === "ai") { let res = "gemini"; // let res = await prompts.select({ // message: "Please select gen ai model to use", // choices: [ // { name: "Google Gemini Model", value: "gemini" }, // { name: "ChatGPT", value: "chatgpt" }, // ], // }); // if (res !== "gemini") { // console.log( // "There is only gemini model implementation available currently, selecting gemini as default" // ); // res = "gemini"; // } if (res === "gemini") { const { config, enableGenAi } = await getGeminiUserConfiguration( preferences ); preferences.genAiConfig = config; preferences.enableGenAi = enableGenAi; } preferences.genAiModel = res; preferences.matchStrategy = matchStrategy; } else { const enableManualAnswering = await prompts.select({ message: "Would you like to manually answer the questions?", choices: [ { name: "No", value: false, description: "Jobs which require question answering will be skipped.", }, { name: "Yes", value: true, description: "You will have to enter the answers manually. (Not recommended, defeats the purpose of automation :) )", }, ], description: "Selecting no will result skipping the jobs which require question answering.", }); preferences.enableManualAnswering = enableManualAnswering; } } if (!preferences?.desiredRole) preferences.desiredRole = user.profile.desiredRole; const desiredRoles = preferences.desiredRole ? preferences.desiredRole.join(", ") : "None"; if (!preferences?.keywords) { preferences.keywords = user.profile.keySkills .split(",") .map((skill) => skill.trim()); } const keywords = preferences.keywords ? preferences.keywords.join(", ") : "None"; if (doConfiguration || desiredRoles === "None" || keywords === "None") { let res = await prompts.input({ message: `Here are current desired roles: ${desiredRoles} Please enter more desired roles in comma separated format (for example: "Software Engineer, Developer, Analyst") Hit enter to skip\n`, }); // ; if (res !== "") { res.split(",").forEach((role) => { preferences.desiredRole.push(role.trim()); }); } res = await prompts.input({ message: `Current keywords to match the jobs: ${keywords} Please enter more keywords to match the jobs in comma separated format (Java, React, HTML, CSS, etc.) Hit enter to skip Note: Include variation of the keywords as well to match correctly.(for example: use reactjs instead of react.js)\n`, }); if (res !== "") { res.split(",").forEach((keyword) => { preferences.keywords.push(keyword.trim().toLowerCase()); }); } } if (doConfiguration || !preferences.noOfPages || !preferences.dailyQuota) { // let res = await prompts.number({ // message: "Enter the number of pages to search for jobs", // default: 5, // min: 1, // max: 10, // }); preferences.noOfPages = 5; res = await prompts.number({ message: "Enter the number of jobs to apply for on daily basis", default: 40, min: 1, max: 50, description: "This is the number of jobs you want to apply for on daily basis, Maximum quota is 50", }); preferences.dailyQuota = res; } return preferences; }; const getLinkedInProfile = async () => { const linkedInProfile = await prompts.input({ message: "Please enter your LinkedIn profile URL.", }); return linkedInProfile; }; const getResume = async () => { try { debugger; const profile = await localStorage.getItem("profile"); ; const response = await getResumeAPI(profile.profile.profileId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const filename = "Resume.pdf"; writeToFile(buffer, filename, null, (isBuffer = true)); return filename; } catch (error) { console.error("Error fetching resume : ", error.message); return null; } }; const constructUser = async (apiData) => { ; const user = { id: apiData.profile[0].name, userDetails: { email: apiData.user.email, mobile: apiData.user.mobile, name: apiData.profile[0].name, }, skills: apiData.itskills.map((skill) => ({ skillName: skill.skill, experienceYears: skill.experienceTime.year, experienceMonths: skill.experienceTime.month, })), education: apiData.educations.map((edu) => ({ degree: edu.course.value, specialization: edu.specialisation.value, institute: edu.institute, marks: edu.marks, startYear: edu.yearOfStart, completionYear: edu.yearOfCompletion, })), employmentHistory: apiData.employments.map((emp) => ({ designation: emp.designation, organization: emp.organization, startDate: emp.startDate, endDate: emp.endDate || "Present", jobDescription: emp.jobDescription, })), schools: apiData.schools.map((school) => ({ board: school.schoolBoard.value, completionYear: school.schoolCompletionYear, percentage: school.schoolPercentage.value, educationType: school.educationType.value, })), languages: apiData.languages.map((lang) => ({ language: lang.lang, proficiency: lang.proficiency.value, abilities: lang.ability, })), profile: { profileId: apiData.profileAdditional.profileId, keySkills: apiData.profile[0].keySkills, birthDate: apiData.profile[0].birthDate, gender: apiData.profile[0].gender == "M" ? "Male" : "Female", maritalStatus: apiData.profile[0].maritalStatus.value, pinCode: apiData.profile[0].pincode, desiredRole: apiData.profile[0].desiredRole.map((role) => role.value), locationPreference: apiData.profile[0].locationPrefId.map( (loc) => loc.value ), expectedCtc: Number(apiData.profile[0].absoluteExpectedCtc), disability: apiData.profile[0].disability.isDisabled == "N" ? "No" : "Yes", noticePeriod: apiData.profile[0]?.noticePeriod?.value, noticeEndDate: apiData.noticePeriod[0]?.noticeEndDate, currentCtc: Number(apiData.profile[0].absoluteCtc), totalExperience: { year: apiData.profile[0].experience.year, month: apiData.profile[0].experience.month, }, desiredRole: apiData.profile[0].desiredRole.map((role) => role.value), currentLocation: ` ${apiData.profile[0].city.value}, ${apiData.profile[0].country.value}`, }, onlineProfile: apiData.onlineProfile.map((profile) => ({ type: profile.profile, url: profile.url, })), workSamples: apiData.workSample }; if (!user.onlineProfile.find((profile) => profile.type === "LinkedIn")) { const linkedInProfile = await getLinkedInProfile(); user.onlineProfile.push({ type: "LinkedIn", url: linkedInProfile, }); } ; const preferences = await getPreferences(user); return { user, preferences }; }; const manageProfiles = async (profile, loginInfo) => { const profiles = await getFileData("profiles"); const data = { id: profile.id, creds: loginInfo.creds, }; if (!profiles) { writeFileData([data], "profiles"); return [data]; } const profileIndex = profiles.findIndex((p) => p.id === profile.id); if (profileIndex !== -1) { profiles[profileIndex] = { ...profiles[profileIndex], ...data }; } else { profiles.push(data); } return profiles; }; module.exports = { login, applyForJobs, getJobInfo, searchJobs, searchSimillarJobs, getRecommendedJobs, handleQuestionnaire, clearJobs, findNewJobs, getExistingJobs, getUserProfile, constructUser, manageProfiles, getResume, };