Last active
September 8, 2022 15:40
-
-
Save izelnakri/096c92e661a9f3dc69c99e24ed8747bf to your computer and use it in GitHub Desktop.
Running this as a local cronjob with 1h intervals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import process from "process"; | |
import chrome from 'chrome-cookies-secure'; | |
import Puppeteer from "puppeteer"; | |
const HOURLY_RATE = process.env.HOURLY_RATE || 320; | |
const DEBUG = process.env.DEBUG; | |
const CHROME_PROFILE = 'Profile 2'; // Change this to your chrome profile | |
const DEFAULT_TIMEOUT = 3000; | |
const BROWSER_HEIGHT = 800; | |
const BROWSER_WIDTH = 1200; | |
const BROWSER_DATA_PATH = '/home/izelnakri/.config/google-chrome'; | |
const BROWSER_BIN_PATH = '/usr/sbin/google-chrome-stable'; | |
const RECRUITER_TITLE_TOKENS = ['recruit', 'Sourcer', 'talent', 'human resource']; | |
const RECRUITER_MESSAGE_TOKENS = [ | |
'🍪', 'role', 'team', 'join', 'attrition', 'working hour', '100% remote', 'promising', 'freelance', 'scrum', | |
'requirement', 'agile', 'product manager', 'product owner', 'product design', 'product development', 'salary' | |
]; | |
(async () => { | |
console.log((new Date()).toString(), 'Running dear-recruiter.js:'); | |
const cookies = await chrome.getCookiesPromised('https://linkedin.com/messaging/', 'puppeteer', CHROME_PROFILE); | |
let browser = await Puppeteer.launch({ | |
executablePath: BROWSER_BIN_PATH, | |
userDataDir: BROWSER_DATA_PATH, | |
headless: !DEBUG, | |
args: [`--window-size=${BROWSER_WIDTH},${BROWSER_HEIGHT}`], | |
}); | |
const page = await browser.newPage() | |
await page.setCookie(...cookies); | |
await page.setViewport({ width: BROWSER_WIDTH, height: BROWSER_HEIGHT }); | |
await page.goto('https://www.linkedin.com/messaging', { timeout: 0 }); | |
await page.waitForTimeout(DEFAULT_TIMEOUT); | |
page.on('console', async (msg) => { | |
const args = await Promise.all(msg.args().map((arg) => turnToObjects(arg))); | |
console.log(...args); | |
}); | |
let sentMessages = await page.evaluate(async ([DEFAULT_TIMEOUT, RECRUITER_TITLE_TOKENS, RECRUITER_MESSAGE_TOKENS, HOURLY_RATE]) => { | |
const LAST_CONVERSATIONS_CARDS = Array.from(document.querySelectorAll('.msg-conversation-card__message-snippet')) | |
// .filter((conversation) => RECRUITER_MESSAGE_TOKENS.some((token) => conversation.textContent.includes(token))); | |
let result = []; | |
for (const [index, conversationCard] of LAST_CONVERSATIONS_CARDS.entries()) { | |
if (index <= 4) { | |
result.push(await checkConversation(conversationCard)); | |
} | |
} | |
return result; | |
async function checkConversation(conversationCard) { | |
conversationCard.click(); | |
await wait(DEFAULT_TIMEOUT); | |
window.scroll({ top: 220 }) | |
let userFullName = document.querySelector('#thread-detail-jump-target')?.textContent.trim(); | |
let userTitle = document.querySelector('.artdeco-entity-lockup__subtitle')?.textContent.trim(); | |
let messageHistory = Array.from(document.querySelectorAll('.msg-s-event-listitem__body')); | |
let firstMessage = messageHistory[0] && messageHistory[0].textContent.trim(); | |
if (messageHistory.length === 1 && firstMessage && isRecruiter(userTitle, firstMessage)) { | |
let message = letsCutTheBullshit(userFullName, HOURLY_RATE); | |
let messageForm = document.querySelector('.msg-form__contenteditable'); | |
if (await fillIn(messageForm, message)) { | |
await wait(DEFAULT_TIMEOUT); | |
let sendButton = document.querySelector('.msg-form__send-button'); | |
sendButton.disabled = false | |
sendButton.click(); | |
await wait(DEFAULT_TIMEOUT); | |
let contactSharePopup = document.querySelector('.artdeco-modal-is-open'); | |
if (contactSharePopup) { | |
let notNowButton = contactSharePopup.querySelector('.artdeco-button--muted'); | |
notNowButton.click(); | |
} | |
return [userFullName, userTitle, message, firstMessage]; | |
} | |
} | |
} | |
function wait(timeout) { | |
return new Promise((resolve) => setTimeout(resolve, timeout)); | |
} | |
function isRecruiter(userTitle, message) { | |
let sanitezedUserTitle = userTitle ? userTitle.toLowerCase() : ''; | |
return RECRUITER_TITLE_TOKENS.some((token) => sanitezedUserTitle.includes(token)) | |
|| RECRUITER_MESSAGE_TOKENS.some((token) => message.includes(token)); | |
} | |
function letsCutTheBullshit(recruiterName, hourlyRate) { | |
// NOTE: This is partially written by GitHub Copilot: | |
return 'Hi ' + recruiterName.split(' ')[0] + ',\n\nI\'m flattered by your interest.' | |
+ 'However I\'d be only interested in a job that pays at least $' + hourlyRate + '/hr, given my experience and ' | |
+ 'current demand in the market. I hope you understand it. Please only contact me if you think my rate is fair.' | |
+ '\n\nBest,\nIzel'; | |
} | |
function fillIn(element, text) { | |
return new Promise((resolve) => { | |
setTimeout(() => { | |
try { | |
element.blur() | |
element.focus(); | |
document.execCommand('insertText', false, text); | |
resolve(true); | |
} catch (error) { | |
resolve(false); | |
} | |
}, 1000); | |
}); | |
} | |
}, [DEFAULT_TIMEOUT, RECRUITER_TITLE_TOKENS, RECRUITER_MESSAGE_TOKENS, HOURLY_RATE]); | |
sentMessages.forEach((message) => { | |
if (Array.isArray(message)) { | |
console.log('===================================='); | |
console.log('Message sent to:', message[0]); | |
console.log('Title:', message[1]); | |
console.log('Message:', message[2]); | |
console.log('In response to:\n\n', message[3]); | |
console.log('===================================='); | |
} | |
}); | |
console.log((new Date()).toString(), 'Closing the browser on:'); | |
console.log(page.url()); | |
browser.close() | |
process.exit(0); | |
})(); | |
function turnToObjects(jsHandle) { | |
return jsHandle.jsonValue(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment