Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 44 additions & 46 deletions linkedin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Linkedin:
def __init__(self) -> None:
utils.prYellow("🤖 Thanks for using Easy Apply Jobs bot, for more information you can visit our site - www.automated-bots.com")
utils.prYellow("🌐 Bot will run in Chrome browser and log in Linkedin for you.")

# Fix for WinError 193: Explicitly construct chromedriver path
try:
chrome_install = ChromeDriverManager().install()
Expand All @@ -42,7 +42,7 @@ def __init__(self) -> None:
if config.displayWarnings:
utils.prYellow(f"⚠️ Warning: Could not use explicit chromedriver path, using default: {str(e)[0:50]}")
self.driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=utils.chromeBrowserOptions())

# Apply stealth mode if available
if STEALTH_AVAILABLE:
try:
Expand All @@ -55,15 +55,15 @@ def __init__(self) -> None:
fix_hairline=True)
except Exception as e:
utils.prYellow(f"⚠️ Warning: Could not apply stealth mode: {str(e)}")

self.cookies_path = f"{os.path.join(os.getcwd(),'cookies')}/{self.getHash(config.email)}.pkl"
self.driver.get('https://www.linkedin.com')
self.loadCookies()

if not self.isLoggedIn():
self.driver.get("https://www.linkedin.com/login?trk=guest_homepage-basic_nav-header-signin")
utils.prYellow("🔄 Trying to log in Linkedin...")
try:
try:
self.driver.find_element("id","username").send_keys(config.email)
time.sleep(2)
self.driver.find_element("id","password").send_keys(config.password)
Expand All @@ -90,32 +90,32 @@ def saveCookies(self) -> None:
try:
# Get the directory path for cookies
cookies_dir = os.path.dirname(self.cookies_path)

# Create cookies directory if it doesn't exist
if cookies_dir and not os.path.exists(cookies_dir):
os.makedirs(cookies_dir, exist_ok=True)

# Save cookies to file
with open(self.cookies_path, "wb") as f:
pickle.dump(self.driver.get_cookies(), f)
except Exception as e:
if config.displayWarnings:
utils.prYellow(f"⚠️ Warning: Could not save cookies: {str(e)[0:100]}")
# Don't raise the exception - cookie saving is not critical for bot operation

def isLoggedIn(self) -> bool:
self.driver.get('https://www.linkedin.com/feed')
try:
self.driver.find_element(By.XPATH,'//*[@id="ember14"]')
return True
except Exception:
pass
return False
return False

def generateUrls(self) -> None:
if not os.path.exists('data'):
os.makedirs('data')
try:
try:
with open('data/urlData.txt', 'w',encoding="utf-8" ) as file:
linkedinJobLinks = utils.LinkedinUrlGenerate().generateUrlLinks()
for url in linkedinJobLinks:
Expand All @@ -136,13 +136,13 @@ def linkJobApply(self) -> None:

urlData = utils.getUrlDataFile()

for url in urlData:
for url in urlData:
self.driver.get(url)
time.sleep(random.uniform(1, constants.botSpeed))

# Handle case where no jobs are found (//small element doesn't exist)
try:
totalJobs = self.driver.find_element(By.XPATH,'//small').text
totalJobs = self.driver.find_element(By.XPATH,'//small').text
except Exception as e:
urlWords = utils.urlToKeywords(url)
lineToWrite = "\n Category: " + urlWords[0] + ", Location: " + urlWords[1] + ", No jobs found for this search criteria. Skipping..."
Expand All @@ -165,7 +165,7 @@ def linkJobApply(self) -> None:

offersPerPage = self.driver.find_elements(By.XPATH, '//li[@data-occludable-job-id]')
offerIds = []

# Extract all offer IDs immediately to avoid stale element references
for offer in offersPerPage:
try:
Expand All @@ -176,9 +176,9 @@ def linkJobApply(self) -> None:
if config.displayWarnings:
utils.prYellow(f"⚠️ Warning: Could not get offer ID: {str(e)[0:50]}")
continue

time.sleep(random.uniform(1, constants.botSpeed))

# Check for "Applied" status by re-finding elements to avoid stale references
try:
offersPerPage = self.driver.find_elements(By.XPATH, '//li[@data-occludable-job-id]')
Expand All @@ -205,18 +205,18 @@ def linkJobApply(self) -> None:
countJobs += 1

jobProperties = self.getJobProperties(countJobs)
if "blacklisted" in jobProperties:
if "blacklisted" in jobProperties:
countBlacklisted += 1
lineToWrite = jobProperties + " | " + "* 🤬 Blacklisted Job, skipped!: " +str(offerPage)
self.displayWriteResults(lineToWrite)
else :

else:
easyApplybutton = self.easyApplyButton()

if easyApplybutton is not None:
easyApplybutton.click()
time.sleep(random.uniform(1, constants.botSpeed))

# Fix for issue #72: LinkedIn added an extra "Continue to next step" button after Easy Apply
try:
continue_button = self.driver.find_element(By.CSS_SELECTOR, "button[aria-label='Continue to next step']")
Expand All @@ -226,7 +226,7 @@ def linkJobApply(self) -> None:
except Exception:
# If button doesn't exist, continue normally
pass

try:
self.chooseResume()
# Fill phone number before submitting
Expand Down Expand Up @@ -256,7 +256,7 @@ def linkJobApply(self) -> None:
self.chooseResume()
comPercentage = self.driver.find_element(By.XPATH,'html/body/div[3]/div/div/div[2]/div/div/span').text
percenNumber = int(comPercentage[0:comPercentage.index("%")])

# For multi-step forms, respect dry-run as well.
if config.dryRun:
result = "* 🧪 DRY RUN - Would go through multi-step application: " + str(offerPage)
Expand All @@ -269,8 +269,8 @@ def linkJobApply(self) -> None:
countApplied += 1
if config.maxApplicationsPerRun and countApplied >= config.maxApplicationsPerRun:
reachedCap = True
except Exception:

except Exception:
countCannotApply += 1
self.chooseResume()
lineToWrite = jobProperties + " | " + "* 🥵 Cannot apply to this Job! " +str(offerPage)
Expand All @@ -289,7 +289,7 @@ def linkJobApply(self) -> None:

utils.prYellow("Category: " + urlWords[0] + "," +urlWords[1]+ " applied: " + str(countApplied) +
" jobs out of " + str(countJobs) + ".")

if reachedCap:
utils.prYellow("🛑 Reached max applications per run limit (" + str(config.maxApplicationsPerRun) + "). Stopping.")
durationSec = time.time() - startTime
Expand All @@ -304,11 +304,11 @@ def chooseResume(self) -> None:
By.CLASS_NAME, "jobs-document-upload__title--is-required")
resumes = self.driver.find_elements(
By.XPATH, "//div[contains(@class, 'ui-attachment--pdf')]")
if (len(resumes) == 1 and resumes[0].get_attribute("aria-label") == "Select this resume"):
if len(resumes) == 1 and resumes[0].get_attribute("aria-label") == "Select this resume":
resumes[0].click()
elif (len(resumes) > 1 and resumes[config.preferredCv-1].get_attribute("aria-label") == "Select this resume"):
elif len(resumes) > 1 and resumes[config.preferredCv-1].get_attribute("aria-label") == "Select this resume":
resumes[config.preferredCv-1].click()
elif (type(len(resumes)) != int):
elif len(resumes) == 0:
utils.prRed(
"❌ No resume has been selected please add at least one resume to your Linkedin account.")
except Exception:
Expand All @@ -321,23 +321,22 @@ def getJobProperties(self, count: int) -> str:

try:
jobTitle = self.driver.find_element(By.XPATH, "//h1[contains(@class, 'job-title')]").get_attribute("innerHTML").strip()
res = [blItem for blItem in config.blackListTitles if (blItem.lower() in jobTitle.lower())]
if (len(res) > 0):
res = [blItem for blItem in config.blackListTitles if blItem.lower() in jobTitle.lower()]
if len(res) > 0:
jobTitle += "(blacklisted title: " + ' '.join(res) + ")"
except Exception as e:
if (config.displayWarnings):
if config.displayWarnings:
utils.prYellow("⚠️ Warning in getting jobTitle: " + str(e)[0:50])
jobTitle = ""

try:
time.sleep(5)
jobDetail = self.driver.find_element(By.XPATH, "//div[contains(@class, 'job-details-jobs')]//div").text.replace("·", "|")
res = [blItem for blItem in config.blacklistCompanies if (blItem.lower() in jobTitle.lower())]
if (len(res) > 0):
res = [blItem for blItem in config.blacklistCompanies if blItem.lower() in jobTitle.lower()]
if len(res) > 0:
jobDetail += "(blacklisted company: " + ' '.join(res) + ")"
except Exception as e:
if (config.displayWarnings):
print(e)
if config.displayWarnings:
utils.prYellow("⚠️ Warning in getting jobDetail: " + str(e)[0:100])
jobDetail = ""

Expand All @@ -347,8 +346,7 @@ def getJobProperties(self, count: int) -> str:
jobLocation = jobLocation + " | " + span.text

except Exception as e:
if (config.displayWarnings):
print(e)
if config.displayWarnings:
utils.prYellow("⚠️ Warning in getting jobLocation: " + str(e)[0:100])
jobLocation = ""

Expand All @@ -370,7 +368,7 @@ def fillPhoneNumber(self) -> None:
try:
# Get phone number from config or additionalQuestions.yaml
phone_number = ""

# Try to get from config.Phone first
if hasattr(config, 'Phone') and config.Phone and config.Phone.strip():
phone_number = config.Phone.strip()
Expand All @@ -385,10 +383,10 @@ def fillPhoneNumber(self) -> None:
phone_number = questions['inputField'].get('Phone Number', '').strip()
except Exception:
pass

if not phone_number:
return # No phone number configured, skip filling

# Try multiple selectors to find phone number input fields
phone_selectors = [
"input[type='tel']",
Expand All @@ -399,17 +397,17 @@ def fillPhoneNumber(self) -> None:
"input[data-test-single-line-text-input]",
"input[class*='phone']"
]

phone_filled = False

# Also try XPath selectors for case-insensitive matching
xpath_selectors = [
"//input[contains(translate(@name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'phone')]",
"//input[contains(translate(@id, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'phone')]",
"//input[contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'phone')]",
"//input[contains(translate(@placeholder, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'phone')]"
]

# Try CSS selectors first
for selector in phone_selectors:
try:
Expand All @@ -433,7 +431,7 @@ def fillPhoneNumber(self) -> None:
break
except Exception:
continue

# Try XPath selectors if CSS didn't work
if not phone_filled:
for xpath in xpath_selectors:
Expand All @@ -457,13 +455,13 @@ def fillPhoneNumber(self) -> None:
break
except Exception:
continue

except Exception as e:
if config.displayWarnings:
utils.prYellow(f"⚠️ Warning: Error in fillPhoneNumber: {str(e)[0:50]}")

def applyProcess(self, percentage: int, offerPage: str) -> str:
applyPages = math.floor(100 / percentage) - 2
applyPages = math.floor(100 / percentage) - 2
result = ""
for pages in range(applyPages):
# Fill phone number before continuing to next step
Expand All @@ -479,7 +477,7 @@ def applyProcess(self, percentage: int, offerPage: str) -> str:
result = "* 🧪 DRY RUN - Would apply to this job: " + str(offerPage)
return result

self.driver.find_element( By.CSS_SELECTOR, "button[aria-label='Review your application']").click()
self.driver.find_element(By.CSS_SELECTOR, "button[aria-label='Review your application']").click()
time.sleep(random.uniform(1, constants.botSpeed))

if config.followCompanies is False:
Expand Down
Loading