import os import re import json import time import random import requests import csv # --- SSL/TLS MONKEYPATCH FOR HUGGING FACE TO TELEGRAM CONNECTION ISSUES --- import ssl from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager class TLSv1_2Adapter(HTTPAdapter): def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): # Force TLS 1.2 to prevent UNEXPECTED_EOF_WHILE_READING errors on Hugging Face context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) context.verify_mode = ssl.CERT_REQUIRED context.check_hostname = True context.load_default_certs() self.poolmanager = PoolManager( num_pools=connections, maxsize=maxsize, block=block, ssl_context=context, **pool_kwargs ) # Apply monkeypatch to all requests HTTPS sessions original_init = requests.sessions.Session.__init__ def patched_init(self, *args, **kwargs): original_init(self, *args, **kwargs) self.mount("https://", TLSv1_2Adapter()) requests.sessions.Session.__init__ = patched_init # -------------------------------------------------------------------------- from datetime import datetime from bs4 import BeautifulSoup from flask import Flask from threading import Thread import telebot from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ForceReply from playwright.sync_api import sync_playwright # pyrefly: ignore [missing-import] from playwright_stealth import stealth_sync from google import genai from fpdf import FPDF import groq import base64 from dotenv import load_dotenv # Load local environment variables if testing locally (do not override system env vars/secrets) load_dotenv(override=False) # --- CUSTOM LOG CAPTURE (Bypass HuggingFace Log Glitch) --- import sys from bot_features import generate_dynamic_cover_letter, generate_interview_prep, send_cold_email_if_found, check_for_interviews, sync_to_notion, wait_for_otp from enterprise_adapters import execute_workday_adapter, execute_lever_adapter, execute_greenhouse_adapter from instahyre_engine import run_instahyre_mass_apply from bot_optimizer import minify_form_html, apply_regex_fallback, apply_rag_memory_fallback class LoggerWriter: def __init__(self, filename): self.filename = filename self.original_stdout = sys.__stdout__ # Use the real original stdout def write(self, message): try: # Encode to ASCII-safe for Windows console, then decode back safe_msg = message.encode('utf-8', errors='replace').decode('utf-8', errors='replace') self.original_stdout.write(safe_msg) self.original_stdout.flush() except Exception: pass try: with open(self.filename, "a", encoding="utf-8", errors="replace") as f: f.write(message) except Exception: pass def flush(self): try: self.original_stdout.flush() except Exception: pass sys.stdout = LoggerWriter("debug.log") sys.stderr = sys.stdout print("--- NEW SERVER BOOT ---") # --- 1. CONFIGURATION & ENVIRONMENT VARIABLES --- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "8697043742:AAHU5HAJ0cit6ctZ-GqWZdvOW490K60Cky4") TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "7607565831") # Hardcoded fallback so HF never forgets GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "AIzaSyBP67pZqV9YruexvY7eRPu515AxuAkgt7A,AIzaSyClfxaBYwPTw_Qq54zJwe0XxT4GYlgZuE4,AQ.Ab8RN6LRR8fRVg8D3WcnKpLzyb__MVWOPuXFLBo_iIP5wr8G8w") TARGET_CHANNEL = os.getenv("TARGET_CHANNEL", "JobSkull") # Primary channel (kept for backward compat) # All channels to monitor (from env as comma-separated list, or use defaults) _channels_env = os.getenv("TARGET_CHANNELS", "") TARGET_CHANNELS = [c.strip().lstrip("@") for c in _channels_env.split(",") if c.strip()] if _channels_env else [ "JobSkull", "KickCharm", "OffCampusJobs4u", "offcampusjobss", "Freshershunt", "job4freshers", "placementjobs", "jobsinternshipplacement", "fresheroffcampus", "workfromhomejobs1", "offcampusphodenge", "veagance", # Remote Jobs [PAN INDIA] "DailyJobs4You", # DailyJobs4You "Foundthejob", # Work From Home JOBS with Foundthejob ] # Files STATE_FILE = "applied_jobs.json" RESUME_FILE = "resume.pdf" PROFILE_FILE = "profile.json" CHAT_ID_FILE = "chat_id.json" QA_MEMORY_FILE = "qa_memory.json" PENDING_QA_FILE = "pending_qa.json" STATS_FILE = "stats_daily.json" RETRY_FILE = "retry_queue.json" LAST_JOB_FILE = "last_job.json" # Stores details of the last application CHANNEL_STATUS_FILE = "channel_status.json" # Stores per-channel scan results WEEKLY_STATS_FILE = "weekly_stats.json" # Stores weekly cumulative stats # Live Handoff Protocol HANDOFF_ACTIVE = False HANDOFF_PAGE = None HANDOFF_URL = "" last_briefing_date = None last_notion_digest_date = None # Unsupported/Social media domains that the bot should skip applying to UNSUPPORTED_DOMAINS = [ # Job boards (apply manually or not supported) "naukri.com", "linkedin.com", "internshala.com", "foundit.in", "indeed.com", "glassdoor.com", "shine.com", "monster.com", "timesjobs.com", "hirist.com", # Google forms / docs (no auto-fill support) "docs.google.com", "google.com/forms", "forms.gle", # Social / media "youtube.com", "youtu.be", "t.me", "telegram.org", "facebook.com", "instagram.com", "twitter.com", "x.com", "whatsapp.com", "pinterest.com", "linktr.ee", "bit.ly", "shorturl" ] # Helper to load and save chat ID dynamically def load_chat_id(): global TELEGRAM_CHAT_ID if TELEGRAM_CHAT_ID: return TELEGRAM_CHAT_ID data = safe_load_json(CHAT_ID_FILE, {}) return data.get("chat_id", "7607565831") def save_chat_id(chat_id): global TELEGRAM_CHAT_ID TELEGRAM_CHAT_ID = str(chat_id) safe_save_json(CHAT_ID_FILE, {"chat_id": TELEGRAM_CHAT_ID}) print(f"[Auth] Saved Telegram chat ID dynamically: {TELEGRAM_CHAT_ID}") # Initialize APIs if TELEGRAM_TOKEN: from telebot import apihelper from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, ForceReply apihelper.CONNECT_TIMEOUT = 60 apihelper.READ_TIMEOUT = 60 # Enable automatic session refreshment & retry mechanism to survive Hugging Face SSL/TLS drops apihelper.SESSION_TIME_TO_LIVE = 60 apihelper.RETRY_ON_ERROR = True apihelper.MAX_RETRIES = 5 apihelper.RETRY_TIMEOUT = 2 bot = telebot.TeleBot(TELEGRAM_TOKEN, threaded=True, num_threads=30) else: bot = None # Handle multiple Gemini API keys GEMINI_API_KEYS = [k.strip() for k in str(os.getenv("GEMINI_API_KEY", "")).split(",") if k.strip()] current_gemini_key_index = 0 def get_gemini_client(): global current_gemini_key_index if not GEMINI_API_KEYS: return None try: return genai.Client(api_key=GEMINI_API_KEYS[current_gemini_key_index], http_options={'timeout': 120.0}) except Exception: return None def rotate_gemini_key(): global current_gemini_key_index, gemini_client if len(GEMINI_API_KEYS) > 1: current_gemini_key_index = (current_gemini_key_index + 1) % len(GEMINI_API_KEYS) gemini_client = get_gemini_client() print(f"[Gemini] Rotated to API key #{current_gemini_key_index + 1}/{len(GEMINI_API_KEYS)}") chat_id = load_chat_id() if bot and chat_id: try: bot.send_message(chat_id, f"🔄 Gemini API Timeout/Error. Rotated to API key #{current_gemini_key_index + 1}") except: pass return True return False gemini_client = get_gemini_client() GROQ_API_KEYS = [k.strip() for k in str(os.getenv("GROQ_API_KEY", "")).split(",") if k.strip()] current_groq_key_index = 0 GROQ_MODEL = os.getenv("GROQ_MODEL", "openai/gpt-oss-120b") def get_groq_client(): global current_groq_key_index if not GROQ_API_KEYS: return None try: return groq.Groq(api_key=GROQ_API_KEYS[current_groq_key_index], timeout=120.0) except Exception: return None def rotate_groq_key(): global current_groq_key_index, groq_client if len(GROQ_API_KEYS) > 1: current_groq_key_index = (current_groq_key_index + 1) % len(GROQ_API_KEYS) groq_client = get_groq_client() print(f"[Groq] Rotated to API key #{current_groq_key_index + 1}/{len(GROQ_API_KEYS)}") chat_id = load_chat_id() if bot and chat_id: try: bot.send_message(chat_id, f"🔄 Groq Rate Limit hit. Rotated to API key #{current_groq_key_index + 1}") except: pass return True return False groq_client = get_groq_client() # Global pause flag BOT_PAUSED = False ghost_mode_chats = set() playwright_active = False # --- 1.5 THREAD-SAFE STORAGE UTILITIES --- import threading import queue file_lock = threading.Lock() application_queue = queue.Queue() def auto_bug_fixer(error: Exception, context: str = ""): """Feed error to Gemini and send the AI fix directly to Telegram.""" import traceback tb = traceback.format_exc() try: gc = get_gemini_client() prompt = f"""You are a Python debugging expert. The following error occurred in a job application bot: ERROR: {str(error)} TRACEBACK: {tb} CONTEXT: {context} In 2-3 sentences max: 1. Explain what caused the bug (plain English, no jargon) 2. Give the exact fix (code snippet if needed) Reply in this format: 🐛 *Cause:* [explanation] 🔧 *Fix:* [exact fix]""" resp = gc.models.generate_content(model="gemini-2.5-flash", contents=prompt) fix_msg = resp.text.strip() except Exception as gem_err: fix_msg = f"🐛 *Error:* `{str(error)[:200]}`\n\n_(Gemini unavailable to suggest fix: {gem_err})_" # Send to Telegram chat_id = load_chat_id() if bot and chat_id: try: full_msg = f"🚨 *Bot Error Detected!*\n\n{fix_msg}\n\n_Context: {context[:100]}_" bot.send_message(chat_id, full_msg[:4096], parse_mode=None) except Exception as tg_err: print(f"[AutoBugFixer] Telegram send failed: {tg_err}") def application_worker(): while True: job_link = application_queue.get() if job_link is None: break try: print(f"[Queue Worker] Processing job: {job_link}") run_playwright_apply(job_link) except Exception as e: print(f"[Queue Worker] Error: {e}") # Auto Bug Fixer: Feed error to Gemini and alert user in Telegram try: auto_bug_fixer(e, context=f"Auto-apply for: {job_link}") except Exception: pass finally: application_queue.task_done() threading.Thread(target=application_worker, daemon=True).start() def safe_load_json(filepath, default_val=None): if default_val is None: default_val = {} with file_lock: if os.path.exists(filepath): try: with open(filepath, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: print(f"[I/O Error] Failed to read {filepath}: {e}") return default_val def safe_save_json(filepath, data): with file_lock: try: temp_path = filepath + ".tmp" with open(temp_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) os.replace(temp_path, filepath) except Exception as e: print(f"[I/O Error] Failed to write {filepath}: {e}") # Load state def load_applied_jobs(): data = safe_load_json(STATE_FILE, []) return set(data) def save_applied_job(job_url): applied = load_applied_jobs() applied.add(job_url) safe_save_json(STATE_FILE, list(applied)) # --- QA Memory: remember answers to custom job questions --- def load_qa_memory(): return safe_load_json(QA_MEMORY_FILE, {}) def save_qa_memory(qa_memory): safe_save_json(QA_MEMORY_FILE, qa_memory) def load_pending_qa(): return safe_load_json(PENDING_QA_FILE, {}) def save_pending_qa(pending): safe_save_json(PENDING_QA_FILE, pending) def load_stats(): today = datetime.now().strftime("%Y-%m-%d") default_stats = {"date": today, "applied": 0, "skipped": 0, "failed": 0, "current_streak": 0, "last_apply_date": ""} stats = safe_load_json(STATS_FILE, default_stats) if stats.get("date") != today: stats["date"] = today stats["applied"] = 0 stats["skipped"] = 0 stats["failed"] = 0 return stats def save_stats(stats): safe_save_json(STATS_FILE, stats) def load_retry_queue(): return safe_load_json(RETRY_FILE, {}) def save_retry_queue(queue): safe_save_json(RETRY_FILE, queue) # --- Last Job Storage --- def save_last_job(data: dict): """Saves details of the most recent application attempt.""" safe_save_json(LAST_JOB_FILE, data) def load_last_job() -> dict: return safe_load_json(LAST_JOB_FILE, {}) # --- Channel Status Storage --- def save_channel_status(status: dict): safe_save_json(CHANNEL_STATUS_FILE, status) def load_channel_status() -> dict: return safe_load_json(CHANNEL_STATUS_FILE, {}) # --- Weekly Stats Storage --- def load_weekly_stats() -> dict: week = datetime.now().strftime("%Y-W%W") default = {"week": week, "applied": 0, "skipped": 0, "failed": 0, "channels_scanned": 0} data = safe_load_json(WEEKLY_STATS_FILE, default) if data.get("week") != week: return default return data def save_weekly_stats(stats: dict): safe_save_json(WEEKLY_STATS_FILE, stats) # --- Smart Sleep Hours Guard --- def is_sleep_time() -> bool: """Returns True between 11 PM and 6 AM IST — bot rests to avoid bot-detection.""" # Convert UTC to IST (+5:30) utc_now = datetime.utcnow() ist_hour = (utc_now.hour + 5 + (utc_now.minute + 30) // 60) % 24 return ist_hour >= 23 or ist_hour < 6 # --- Duplicate URL hash set (cross-channel dedup within one cycle) --- _seen_this_cycle: set = set() # Load profile data def load_profile(): default_profile = { "full_name": "Manoj Kumar", "email": "manoj.kumar@example.com", "phone": "+91 9876543210", "experience_years": "2 years", "github": "https://github.com/manoj", "linkedin": "https://linkedin.com/in/manoj", "portfolio": "https://manoj.dev", "skills": "Python, JavaScript, Playwright, React, SQL, Automation", "about": "Self-motivated software developer with a passion for web scraping, browser automation, and AI integrations.", "cover_letters": { "default": "I am a passionate software developer with 2 years of experience building automation tools and scalable web applications. I am excited about the opportunity to contribute my skills to your innovative team.", "startup": "As a self-motivated developer, I thrive in fast-paced startup environments. I have strong experience in Python and full-stack development and can quickly adapt to new challenges to drive your product forward.", "corporate": "With a solid foundation in software engineering principles and a track record of reliable delivery, I am eager to bring my technical expertise to your established organization and contribute to long-term success." } } return safe_load_json(PROFILE_FILE, default_profile) # --- 2. WEBPAGE REDIRECT BYPASSER --- def bypass_blog_redirect(blog_url): """ Intelligently finds the real company application link inside ad-heavy blogger/shortener pages. Step 1: Follow HTTP redirects (handles URL shorteners like pdlink.in, bit.ly, etc.) Step 2: Deep HTML scraping for the actual apply link Step 3: Playwright JS-rendering fallback for dynamic pages """ from urllib.parse import urlparse, parse_qs, unquote # If the URL is already a direct job board/form, return it immediately without fetching direct_domains = [ "docs.google.com", "greenhouse.io", "lever.co", "workdayjobs.com", "smartrecruiters.com", "joinsuperset.com", "myworkdayjobs.com", "oraclecloud.com", "successfactors", "icims.com", "ashbyhq.com", "bamboohr.com", "jobs.lever.co", "taleo.net", "breezy.hr", "recruitee.com", "freshteam.com", "zohorecruit.com", "wellfound.com", "angel.co", "workingnomads.com", "weworkremotely.com", "hired.com", "triplebyte.com", "ycombinator.com/companies", ] if any(domain in blog_url.lower() for domain in direct_domains): print(f"[Bypasser] URL is already a direct job page: {blog_url}") return blog_url headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", } try: # Step 1: Follow all HTTP redirects to get the FINAL URL response = requests.get(blog_url, headers=headers, timeout=15, allow_redirects=True) final_url_after_redirect = response.url print(f"[Bypasser] Redirect chain resolved: {blog_url} → {final_url_after_redirect}") # If the redirect itself landed on a known job platform, return immediately if any(domain in final_url_after_redirect.lower() for domain in direct_domains): print(f"[Bypasser] Redirect resolved to direct job page: {final_url_after_redirect}") return final_url_after_redirect # If redirect changed the URL significantly (e.g. shortener resolved), use the final URL parsed_original = urlparse(blog_url) parsed_final = urlparse(final_url_after_redirect) if parsed_original.netloc != parsed_final.netloc: print(f"[Bypasser] URL shortener resolved to new domain: {final_url_after_redirect}") blog_url = final_url_after_redirect parsed_blog = urlparse(blog_url) blog_domain = parsed_blog.netloc soup = BeautifulSoup(response.text, "html.parser") # Priority 0: Check redirect tags meta_refresh = soup.find("meta", attrs={"http-equiv": re.compile(r"refresh", re.I)}) if meta_refresh: content = meta_refresh.get("content", "") url_match = re.search(r'url\s*=\s*["\']?([^"\';\s>]+)', content, re.I) if url_match: meta_url = url_match.group(1) if meta_url.startswith("http") and blog_domain not in meta_url: print(f"[Bypasser] Found meta refresh redirect: {meta_url}") return meta_url # Priority 1: Look for links with "Apply" keywords in their text apply_keywords = [ "apply", "register", "fill", "submit", "application", "apply now", "click here to apply", "apply online", "apply for this job", "start application", "apply here", "external apply", "apply on company", "apply directly", "official link", "career page", "company website", ] skip_domains = [ "newsletter", "instagram", "youtube", "whatsapp", "telegram", "facebook", "twitter", "x.com", "pinterest", "reddit", "play.google.com", "apps.apple.com", ] for link in soup.find_all("a", href=True): href = link["href"].strip() link_text = link.get_text(separator=" ").strip().lower() if not href.startswith("http"): continue if any(word in link_text for word in apply_keywords): if blog_domain not in href and not any(x in href.lower() for x in skip_domains): print(f"[Bypasser] Found apply link by text keyword: {href}") return href # Priority 2: Look for direct external job platforms in any link href job_platform_domains = [ "docs.google.com/forms", "greenhouse.io", "lever.co", "instahyre.com", "linkedin.com/jobs", "workday.com", "smartrecruiters.com", "joinsuperset.com", "ashbyhq.com", "bamboohr.com", "breezy.hr", "recruitee.com", "freshteam.com", "zohorecruit.com", "wellfound.com", "taleo.net", "icims.com", "successfactors", # Career page patterns "careers.", "jobs.", "apply.", "hire.", "recruit.", "/careers/", "/jobs/", "/apply/", "/hiring/", "/openings/", "/job-opening/", "/vacancies/", "/positions/", ] for link in soup.find_all("a", href=True): href = link["href"].strip() if not href.startswith("http"): continue if any(domain in href.lower() for domain in job_platform_domains): if blog_domain not in href and not any(x in href.lower() for x in skip_domains): print(f"[Bypasser] Found job platform link: {href}") return href # Priority 3: Check query parameters in links for redirect targets for link in soup.find_all("a", href=True): href = link["href"] if any(param in href for param in ["target=", "url=", "redirect=", "goto=", "link=", "dest=", "next=", "redir="]): try: parsed = urlparse(href) qs = parse_qs(parsed.query) for key in ["target", "url", "redirect", "goto", "link", "dest", "next", "redir"]: if key in qs: real_url = unquote(qs[key][0]) if real_url.startswith("http"): print(f"[Bypasser] Found redirect param link: {real_url}") return real_url except Exception: pass # Priority 4: Look for JavaScript window.location or window.open redirects in scripts for script in soup.find_all("script"): script_text = script.string or "" js_patterns = [ r'window\.location\s*=\s*["\']([^"\']+)', r'window\.location\.href\s*=\s*["\']([^"\']+)', r'window\.open\s*\(\s*["\']([^"\']+)', r'location\.replace\s*\(\s*["\']([^"\']+)', ] for pattern in js_patterns: match = re.search(pattern, script_text) if match: js_url = match.group(1) if js_url.startswith("http") and blog_domain not in js_url: print(f"[Bypasser] Found JS redirect link: {js_url}") return js_url # Priority 5: Look for any external link that looks like a company website # (not blog/social media) — using heuristics all_external_links = [] for link in soup.find_all("a", href=True): href = link["href"].strip() if href.startswith("http") and blog_domain not in href: if not any(x in href.lower() for x in skip_domains): all_external_links.append(href) # Prefer links with career/jobs keywords in the URL itself for ext_link in all_external_links: if any(kw in ext_link.lower() for kw in ["/career", "/job", "/apply", "/opening", "/hiring", "/recruit"]): print(f"[Bypasser] Found career page URL pattern: {ext_link}") return ext_link # Priority 6: Check for button onclick handlers for btn in soup.find_all(["button", "a", "div"], onclick=True): onclick = btn.get("onclick", "") url_match = re.search(r'["\']?(https?://[^"\';\s]+)', onclick) if url_match: btn_url = url_match.group(1) if blog_domain not in btn_url and not any(x in btn_url.lower() for x in skip_domains): print(f"[Bypasser] Found onclick URL: {btn_url}") return btn_url # If we have external links but none matched career patterns, return the first non-social one if all_external_links: print(f"[Bypasser] Using first external link: {all_external_links[0]}") return all_external_links[0] # Fallback: return the final URL after redirect resolution (better than original shortener) print(f"[Bypasser] No apply link found in HTML. Using final resolved URL: {blog_url}") return blog_url except Exception as e: print(f"[Bypasser] Error resolving redirect for {blog_url}: {e}") return blog_url # --- 2.9 AI PAGE VERIFICATION HELPER --- def verify_page_with_ai(page, screenshot_bytes, context_msg=""): """ Uses Gemini Vision to analyze the current page state. Returns: 'success', 'error', 'form', 'captcha', or 'unknown' """ if not gemini_client: return 'unknown' try: from google.genai import types as gtypes prompt = f""" You are a job application bot verifying the state of a web page. Context: {context_msg} Look at this screenshot and classify what the page shows into EXACTLY ONE of these categories: - 'success' → Thank you, application submitted, confirmation email sent, application received, success! - 'error' → Error message, validation failed, required field missing, invalid input, please fix - 'form' → There is still a form or fields to fill in (next step, multi-step form continues) - 'captcha' → A CAPTCHA, reCAPTCHA, or security check is blocking - 'login' → Requires login or account creation to continue - 'unknown' → None of the above Reply ONLY with one word from the list above. No other text. """ contents = [prompt] if screenshot_bytes: contents.append(gtypes.Part.from_bytes(data=screenshot_bytes, mime_type='image/png')) response = gemini_client.models.generate_content(model="gemini-2.5-flash", contents=contents) result = response.text.strip().lower() for state in ['success', 'error', 'form', 'captcha', 'login', 'unknown']: if state in result: return state return 'unknown' except Exception as e: print(f"[AI Verify] Failed: {e}") return 'unknown' def ai_fix_selector(page, failed_selector, expected_value, screenshot_bytes, gemini_client, groq_client): """ When a selector fails, asks Gemini/Groq to suggest an alternative selector by analyzing the current page HTML and screenshot. Returns a new selector string or None. """ try: body_html = page.evaluate("() => (document.querySelector('main') || document.body).innerHTML")[:15000] prompt = f""" A CSS selector failed to locate a field on this job application form. Failed selector: "{failed_selector}" Expected value to fill: "{expected_value}" Here is the current page HTML: {body_html} Look for any visible input/textarea/select element that would logically accept this value. Reply ONLY with the single best CSS selector string. No explanation. No quotes. No markdown. If you cannot find any suitable element, reply with: NONE """ # Try Gemini first if gemini_client: try: from google.genai import types as gtypes contents = [prompt] if screenshot_bytes: contents.append(gtypes.Part.from_bytes(data=screenshot_bytes, mime_type='image/png')) resp = gemini_client.models.generate_content(model="gemini-2.5-flash", contents=contents) sel = resp.text.strip().strip('`').strip('"').strip("'") if sel and sel.upper() != 'NONE': return sel except Exception: pass # Fallback to Groq if groq_client: try: completion = groq_client.chat.completions.create( model=GROQ_MODEL, messages=[ {"role": "system", "content": "You are a CSS selector expert. Reply only with a single CSS selector."}, {"role": "user", "content": prompt} ], temperature=0, ) sel = completion.choices[0].message.content.strip().strip('`').strip('"').strip("'") if sel and sel.upper() != 'NONE': return sel except Exception: pass except Exception as e: print(f"[AI Fix Selector] Error: {e}") return None # --- 3. DYNAMIC FORM FILLING WITH GEMINI --- def analyze_form_with_gemini(form_html, profile_data, screenshot_bytes=None, job_context="", job_description=""): """ Uses Gemini API to map form fields to the user's profile data, leveraging both HTML and Vision. It uses the job_description to write customized Cover Letters. """ if not gemini_client: print("[Warning] Gemini client not initialized. Skipping AI form mapping.") return None qa_memory = load_qa_memory() qa_memory_text = "" if qa_memory: qa_memory_text = "\n\n Previously Answered Questions (USE THESE EXACT ANSWERS for matching questions):\n" for q, a in qa_memory.items(): qa_memory_text += f" Q: {q}\n A: {a}\n" prompt = f""" You are an expert job application automation bot. Your ONLY job is to analyze the form HTML and screenshot below and output JSON field mappings so the bot can fill the form. Job URL: {job_context} Specific Job Description: {job_description} User Profile: {json.dumps(profile_data, indent=2)}{qa_memory_text} Form HTML: {form_html} CRITICAL RULES - READ CAREFULLY: 1. ALWAYS output real CSS selectors. Prefer: input[name='xxx'], textarea[name='xxx'], select[name='xxx'], input[placeholder='xxx'], input[type='email'], input[id='xxx'], [data-field='xxx']. 2. NEVER output a selector like 'input' or 'textarea' alone — always include attribute or id to make it unique. 3. For EVERY visible text input/textarea/select in the HTML, output a mapping entry using profile data. Map: - Name fields → full_name - Email fields → email - Phone/Mobile fields → phone - LinkedIn URL fields → linkedin - GitHub/Portfolio fields → github or portfolio - Years of experience fields → experience_years - Skills/Summary/About fields → skills or about - Current/Expected salary → output '0' or 'As per industry standard' - Notice period → output 'Immediate' - City/Location → 'Bengaluru' (or best guess from profile) 4. DROPDOWN FIELDS (select elements): When you see a

Click directly on the image below to interact with the webpage.

""" @app.route("/api/status") def api_status(): """Live stats for dashboard widgets.""" import time as _time wk = load_weekly_stats() retry_q = load_retry_queue() uptime_secs = int(_time.time() - _server_start_time) if '_server_start_time' in globals() else 0 hours, rem = divmod(uptime_secs, 3600) mins = rem // 60 return jsonify({ "queue_size": application_queue.qsize(), "weekly_applied": wk.get("applied", 0), "retry_count": len(retry_q), "uptime": f"{hours}h {mins}m" if hours > 0 else f"{mins}m", "bot_paused": BOT_PAUSED, "instahyre_running": playwright_active, }) @app.route("/api/live_screenshot") def live_screenshot(): if not HANDOFF_ACTIVE or not HANDOFF_PAGE: return "No active handoff", 404 try: ss_bytes = HANDOFF_PAGE.screenshot() return send_file(io.BytesIO(ss_bytes), mimetype='image/png') except Exception as e: return str(e), 500 @app.route("/api/live_action", methods=["POST"]) def live_action(): if not HANDOFF_ACTIVE or not HANDOFF_PAGE: return "No active handoff", 400 try: data = request.json if data.get('action') == 'click': HANDOFF_PAGE.mouse.click(data['x'], data['y']) elif data.get('action') == 'type': HANDOFF_PAGE.keyboard.type(data['text']) HANDOFF_PAGE.keyboard.press('Enter') return jsonify({"status": "ok"}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api/live_resume", methods=["POST"]) def live_resume(): global HANDOFF_ACTIVE HANDOFF_ACTIVE = False return jsonify({"status": "resumed"}) @app.route("/logs") def view_logs(): try: with open("debug.log", "r", encoding="utf-8") as f: lines = f.readlines() return "
" + "".join(lines[-100:]) + "
" # Show last 100 lines except Exception as e: return f"Log file not found or error: {e}" # ─── JOB RADAR API ROUTES ───────────────────────────────── @app.route("/api/radar") def api_radar(): """Returns the last radar scan results from radar_results.json.""" try: with open("radar_results.json", "r", encoding="utf-8") as f: data = json.load(f) return jsonify(data) except Exception: return jsonify({"last_scan": "Never", "total": 0, "jobs": []}) @app.route("/api/run_radar", methods=["POST"]) def api_run_radar(): """Triggers a fresh Job Radar scan in a background thread.""" def _run(): try: from job_radar import run_radar run_radar() except Exception as e: print(f"[RadarAPI] Error: {e}") Thread(target=_run, daemon=True).start() return jsonify({"status": "started", "message": "Radar scan started! Results will appear in ~60-90 seconds. Check your Telegram too!"}) @app.route("/screenshots/") def serve_screenshot(filename): from flask import send_from_directory return send_from_directory("screenshots", filename) @app.route("/api/debug_bot", methods=["GET"]) def api_debug_bot(): token_val = str(TELEGRAM_TOKEN) if TELEGRAM_TOKEN else "None" masked = token_val[:5] + "..." + token_val[-5:] if len(token_val) > 10 else token_val # Test connection to Telegram API telegram_ok = False telegram_err = None try: r = requests.get("https://api.telegram.org", timeout=5) telegram_ok = True except Exception as e: telegram_err = str(e) return jsonify({ "bot_exists": bot is not None, "token_loaded": masked, "chat_id_loaded": load_chat_id(), "telegram_api_reachable": telegram_ok, "telegram_api_error": telegram_err }) @app.route("/api/telegram_test", methods=["POST"]) def api_telegram_test(): """Sends a test message to Telegram to verify bot connectivity.""" chat_id = load_chat_id() if not bot or not chat_id: return jsonify({"status": "error", "message": "Bot or chat ID not configured."}) try: bot.send_message(chat_id, "✅ *Elite Job Bot Dashboard — Connection Test Successful!*\n\n🤖 Your bot is fully online and connected.\n📡 Radar is scanning for your Unicorn Developer jobs.", parse_mode=None) return jsonify({"status": "ok", "message": "Test message sent to Telegram!"}) except Exception as e: return jsonify({"status": "error", "message": str(e)}) @app.route("/api/force_scan", methods=["POST"]) def api_force_scan(): """Triggers an immediate Telegram channel scan.""" def _run(): try: scrape_telegram_channel() except Exception as e: print(f"[ForceScan] Error: {e}") Thread(target=_run, daemon=True).start() return jsonify({"status": "started", "message": "Channel scan triggered! Check Telegram for updates."}) @app.route("/api/instahyre", methods=["POST"]) def api_instahyre(): """Triggers the Instahyre mass-applier engine from the dashboard.""" chat_id = load_chat_id() data = request.get_json(silent=True) or {} email = data.get('email') password = data.get('password') if bot and chat_id: try: bot.send_message(chat_id, "🚀 Instahyre Campaign Launched!\n\nThe stealth engine has started in the background. It will automatically apply to 20 Fresher Software Engineer roles. Please wait 5-10 minutes for the final report.", parse_mode=None) except: pass def _run(): global playwright_active playwright_active = True try: success, msg, _ = run_instahyre_mass_apply(email=email, password=password, skills="Software Engineer Fresher", max_applications=20) print(f"[Instahyre] {msg}") if bot and chat_id: status_icon = "✅" if success else "❌" bot.send_message(chat_id, f"{status_icon} Instahyre Campaign Finished\n\n{msg}", parse_mode=None) except Exception as e: err_str = str(e) print(f"[Instahyre] Engine Error: {err_str}") if bot and chat_id: try: bot.send_message(chat_id, f"❌ Instahyre Campaign Error\n\n{err_str[:300]}\n\nThe engine encountered an error. Check dashboard logs for details.") except: pass finally: playwright_active = False Thread(target=_run, daemon=True).start() return jsonify({"status": "started", "message": "Instahyre Engine launched! It will take 5-10 mins. Results will be sent to Telegram."}) @app.route("/") def home(): active_chat_id = load_chat_id() stats = load_stats() applied = load_applied_jobs() qa_memory = load_qa_memory() qa_size = len(qa_memory) if qa_memory else 0 imap_status = "CONNECTED" if os.environ.get("BOT_EMAIL") and os.environ.get("BOT_EMAIL_PASSWORD") else "OFFLINE" ghost_mode_active = len(ghost_mode_chats) > 0 # AI Engine Status gemini_status = "ONLINE" if gemini_client else "OFFLINE" groq_status = "ONLINE" if groq_client else "OFFLINE" gemini_key_count = len(GEMINI_API_KEYS) active_key_num = current_gemini_key_index + 1 # Success Rate total_applied = stats.get('applied', 0) total_skipped = stats.get('skipped', 0) total = total_applied + total_skipped success_pct = round((total_applied / total) * 100) if total > 0 else 0 stroke_offset = round(339.3 * (1 - success_pct / 100), 1) # Recent jobs from CSV recent_rows = [] csv_file = "applied_jobs_log.csv" best_job = None best_score = 0 if os.path.exists(csv_file): try: with open(csv_file, "r", encoding="utf-8") as f: rows = list(csv.reader(f)) if len(rows) > 1: recent_rows = list(reversed(rows[-10:])) except Exception: pass recent_jobs_html = '
' if recent_rows: recent_jobs_html += '' for row in recent_rows: if len(row) >= 4: date, title, url, status = row[0], row[1][:50], row[2], row[3] # Check if it was marked as interview previously in notes or status notes = row[4] if len(row) > 4 else "" if "Interview" in status or "Interview" in notes: badge = '📞 INTERVIEW' elif "Rejected" in status or "Rejected" in notes: badge = '❌ REJECTED' elif "Auto applied" in status: badge = '✅ APPLIED' elif "Failed" in status or "failed" in status: badge = '❌ FAILED' else: badge = '⏭ SKIPPED' recent_jobs_html += f''' ''' recent_jobs_html += '
StatusJob URL / TitleDateCRM Actions
{badge} {title} {date}
' else: recent_jobs_html = '

No recent activity found. Jobs will appear here as a CRM table once the bot runs.

' # Channel grid channels_html = "" for ch in TARGET_CHANNELS: channels_html += f'
@{ch}
' from flask import render_template_string try: base_dir = os.path.dirname(os.path.abspath(__file__)) template_path = os.path.join(base_dir, "templates", "dashboard.html") if not os.path.exists(template_path): # Fallback if the user uploaded it directly to the root on Hugging Face template_path = os.path.join(base_dir, "dashboard.html") with open(template_path, "r", encoding="utf-8") as f: html_template = f.read() rendered = render_template_string(html_template, gemini_status=gemini_status, groq_status=groq_status, active_key_num=active_key_num, gemini_key_count=gemini_key_count, success_pct=success_pct, stroke_offset=stroke_offset, total_applied=total_applied, total_skipped=total_skipped, qa_size=qa_size, active_chat_id=active_chat_id, imap_status=imap_status, ghost_mode_active=ghost_mode_active, channels_html=channels_html, recent_jobs_html=recent_jobs_html, HANDOFF_URL=HANDOFF_URL, notion_url=f"https://notion.so/{re.search(r'([a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12})', os.getenv('NOTION_DATABASE_ID', '')).group(1).replace('-', '') if re.search(r'([a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12})', os.getenv('NOTION_DATABASE_ID', '')) else ''}" if os.getenv('NOTION_DATABASE_ID') else "https://notion.so", stats=stats, applied=applied, os=__import__('os'), TARGET_CHANNEL=TARGET_CHANNEL, TARGET_CHANNELS=TARGET_CHANNELS, len=len, RESUME_FILE=RESUME_FILE, BOT_PAUSED=BOT_PAUSED ) from flask import make_response resp = make_response(rendered) resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' resp.headers['Pragma'] = 'no-cache' resp.headers['Expires'] = '0' return resp except Exception as e: import traceback traceback.print_exc() return f"Error loading dashboard template: {e}" # (Radar routes defined earlier in file) @app.route("/api/pause", methods=["POST"]) def api_pause(): global BOT_PAUSED BOT_PAUSED = True print("[Dashboard] Bot PAUSED via web dashboard.") return {"status": "paused", "message": "Bot is now paused."} @app.route("/api/resume", methods=["POST"]) def api_resume(): global BOT_PAUSED BOT_PAUSED = False print("[Dashboard] Bot RESUMED via web dashboard.") return {"status": "running", "message": "Bot is now running."} # (force_scan route defined earlier in file) @app.route("/api/rotate_key", methods=["POST"]) def api_rotate_key(): success = rotate_gemini_key() if success: return {"status": "success", "message": f"Switched to key #{current_gemini_key_index + 1}"} return {"status": "error", "message": "Failed to rotate key"} @app.route("/api/download_log") def api_download_log(): csv_file = "applied_jobs_log.csv" if os.path.exists(csv_file): return send_file(csv_file, as_attachment=True) return {"status": "error", "message": "No log file found! The bot needs to apply to at least one job first."} from flask import request, send_file, jsonify import subprocess @app.route("/api/manual_apply", methods=["POST"]) def api_manual_apply(): data = request.json url = data.get("url") if url: if not url.startswith("http"): url = "https://" + url final_url = bypass_blog_redirect(url) print(f"[Dashboard] Manual apply initiated for {final_url}") def run(): if any(domain in final_url.lower() for domain in UNSUPPORTED_DOMAINS): log_job(final_url, "Web Dashboard /apply", False, "Unsupported platform or social media") return success = run_playwright_apply(final_url, "Manual application from Web Dashboard") log_job(final_url, "Web Dashboard /apply", success, "Manual apply") Thread(target=run, daemon=True).start() return {"status": "success", "message": "Application started in background."} return {"status": "error", "message": "Invalid URL"} @app.route("/api/update_profile", methods=["POST"]) def api_update_profile(): data = request.json field = data.get("field") value = data.get("value") if field and value: profile = load_profile() profile[field] = value try: with open(PROFILE_FILE, "w", encoding="utf-8") as f: json.dump(profile, f, indent=4) print(f"[Dashboard] Profile updated: {field} = {value}") return {"status": "success", "message": f"{field} updated."} except Exception as e: return {"status": "error", "message": str(e)} return {"status": "error", "message": "Missing data"} @app.route("/api/regenerate_resume", methods=["POST"]) def api_regenerate_resume(): try: subprocess.run(["python", "generate_resume.py"], check=True) return {"status": "success", "message": "Resume regenerated."} except Exception as e: return {"status": "error", "message": str(e)} @app.route("/api/reset_history", methods=["POST"]) def api_reset_history(): try: if os.path.exists("applied_jobs_log.csv"): os.remove("applied_jobs_log.csv") if os.path.exists(STATS_FILE): os.remove(STATS_FILE) return {"status": "success", "message": "History and stats wiped."} except Exception as e: return {"status": "error", "message": str(e)} @app.route("/api/health_check", methods=["GET"]) def api_health_check(): gemini_status = "Offline" groq_status = "Offline" try: if gemini_client: st = time.time() gemini_client.models.generate_content(model="gemini-2.5-flash", contents="reply ok") gemini_status = f"Online ({int((time.time()-st)*1000)}ms)" except Exception: pass try: if groq_client: st = time.time() groq_client.chat.completions.create(model=GROQ_MODEL, messages=[{"role": "user", "content": "ok"}]) groq_status = f"Online ({int((time.time()-st)*1000)}ms)" except Exception: pass return {"gemini": gemini_status, "groq": groq_status} @app.route("/api/download_qa") def api_download_qa(): qa_file = "qa_memory.json" if os.path.exists(qa_file): return send_file(qa_file, as_attachment=True) return {"status": "error", "message": "No Q&A Brain found! Use the /answer command in Telegram to create one."} @app.route("/api/download_profile") def api_download_profile(): if os.path.exists(PROFILE_FILE): return send_file(PROFILE_FILE, as_attachment=True) return {"status": "error", "message": "No profile found"} @app.route("/api/add_channel", methods=["POST"]) def api_add_channel(): global TARGET_CHANNELS data = request.json channel = data.get("channel", "").replace("@", "").strip() if channel: if channel not in TARGET_CHANNELS: TARGET_CHANNELS.append(channel) print(f"[Dashboard] Added channel @{channel}") return {"status": "success", "message": f"Added @{channel}"} return {"status": "error", "message": "Channel already exists"} return {"status": "error", "message": "Invalid channel"} @app.route("/api/upload_resume", methods=["POST"]) def api_upload_resume(): if 'resume' not in request.files: return {"status": "error", "message": "No file part"} file = request.files['resume'] if file.filename == '': return {"status": "error", "message": "No selected file"} if file and file.filename.endswith('.pdf'): file.save(RESUME_FILE) return {"status": "success", "message": "Resume uploaded successfully!"} return {"status": "error", "message": "Invalid file type. Must be PDF."} @app.route("/api/upload_auth", methods=["POST"]) def api_upload_auth(): if 'auth' not in request.files: return {"status": "error", "message": "No file part"} file = request.files['auth'] if file.filename == '': return {"status": "error", "message": "No selected file"} if file and file.filename.endswith('.json'): file.save("instahyre_auth.json") return {"status": "success", "message": "Session auth state uploaded successfully!"} return {"status": "error", "message": "Invalid file type. Must be a .json file containing Playwright storage state."} @app.route("/api/scan_inbox", methods=["POST"]) def api_scan_inbox(): try: from imap_handler import scan_for_interview_invites results = scan_for_interview_invites() if results: msg = f"🎉 FOUND {len(results)} INTERVIEW EMAILS! Check Telegram." else: msg = "No new interview emails found in the last 7 days." return {"status": "success", "message": msg} except Exception as e: return {"status": "error", "message": f"IMAP Error: {str(e)}"} @app.route("/api/mark_crm", methods=["POST"]) def api_mark_crm(): data = request.json url = data.get("url") new_status = data.get("status") csv_file = "applied_jobs_log.csv" if os.path.exists(csv_file): try: with open(csv_file, "r", encoding="utf-8") as f: rows = list(csv.reader(f)) for i in range(1, len(rows)): if len(rows[i]) >= 4 and rows[i][2] == url: rows[i][3] = f"{new_status} (Manually Updated)" if len(rows[i]) > 4: rows[i][4] = new_status else: rows[i].append(new_status) with open(csv_file, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerows(rows) return {"status": "success", "message": f"Marked as {new_status}"} except Exception as e: return {"status": "error", "message": str(e)} return {"status": "error", "message": "No log file found"} def manual_radar_scan(chat_id): """ Dedicated Radar Scan: 1. Scrapes the configured Telegram channel. 2. Fetches fresh Software Engineering jobs specifically for Tamil Nadu, Bangalore, and Kerala. """ import urllib.request import json if bot: bot.send_message(chat_id, "📡 *South India Job Radar Active!*\n\nScanning portals for Software Engineering jobs in:\n📍 `Tamil Nadu (Chennai, Coimbatore, Madurai)`\n📍 `Bangalore`\n📍 `Kerala`...", parse_mode=None) applied = load_applied_jobs() found_jobs = 0 # 1. Standard Telegram scan try: if 'TARGET_CHANNEL' in globals() and TARGET_CHANNEL: f, _ = scrape_single_channel(TARGET_CHANNEL, applied, chat_id) found_jobs += f except Exception as e: print(f"[Radar] Telegram scan error: {e}") # 2. Multi-API Tech Job Aggregator (Arbeitnow, Remotive, Hasjob concepts) south_india_keywords = ['bangalore', 'bengaluru', 'chennai', 'tamil nadu', 'coimbatore', 'madurai', 'kerala', 'kochi', 'trivandrum', 'india', 'remote'] api_endpoints = [ {"url": "https://remotive.com/api/remote-jobs?category=software-dev&search=India", "type": "remotive"}, {"url": "https://www.arbeitnow.com/api/job-board-api", "type": "arbeitnow"} ] # Simple hash memory for current run to avoid duplicates across APIs seen_hashes = set() for api in api_endpoints: if found_jobs >= 5: # Hard limit to protect queue break try: req = urllib.request.Request(api["url"], headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) # Extract jobs based on API format if api["type"] == "remotive": jobs = data.get('jobs', [])[:20] for j in jobs: loc = str(j.get('candidate_required_location', '')).lower() title = str(j.get('title', '')).lower() company = str(j.get('company_name', '')).lower() url = j.get('url') fingerprint = f"{company}_{title}" if fingerprint in seen_hashes: continue if any(kw in loc for kw in south_india_keywords) or 'india' in loc: if url and url not in applied: seen_hashes.add(fingerprint) application_queue.put(url) found_jobs += 1 applied.add(url) save_applied_job(url) if bot: bot.send_message(chat_id, f"🎯 *Remotive Match!*\n\n💻 *Role:* {j.get('title')}\n🏢 *Company:* {j.get('company_name')}\n📍 *Location:* {j.get('candidate_required_location')}\n\n_Added to Queue!_", parse_mode=None) time.sleep(1) if found_jobs >= 5: break elif api["type"] == "arbeitnow": jobs = data.get('data', [])[:30] for j in jobs: loc = str(j.get('location', '')).lower() title = str(j.get('title', '')).lower() company = str(j.get('company_name', '')).lower() url = j.get('url') fingerprint = f"{company}_{title}" if fingerprint in seen_hashes: continue # Arbeitnow is global, we strictly filter for South India/Remote if any(kw in loc for kw in south_india_keywords): if url and url not in applied: seen_hashes.add(fingerprint) application_queue.put(url) found_jobs += 1 applied.add(url) save_applied_job(url) if bot: bot.send_message(chat_id, f"🎯 *Arbeitnow Match!*\n\n💻 *Role:* {j.get('title')}\n🏢 *Company:* {j.get('company_name')}\n📍 *Location:* {j.get('location')}\n\n_Added to Queue!_", parse_mode=None) time.sleep(1) if found_jobs >= 5: break except Exception as e: print(f"[Radar] Multi-API scan error on {api['url']}: {e}") if bot: bot.send_message(chat_id, f"✅ *Radar Scan Complete*\nFound {found_jobs} new tech jobs. They are now processing in the background queue.", parse_mode=None) if bot: @bot.message_handler(commands=['dashboard', 'menu']) def send_dashboard(message): """Renders the epic interactive control panel inside Telegram.""" save_chat_id(message.chat.id) markup = InlineKeyboardMarkup(row_width=2) # Row 1: Core Commands markup.add( InlineKeyboardButton("📊 Status & Stats", callback_data="status"), InlineKeyboardButton("📸 Last Screenshot", callback_data="last_job") ) # Row 2: Management markup.add( InlineKeyboardButton("📋 My Profile", callback_data="profile"), InlineKeyboardButton("🧠 AI Memory (QA)", callback_data="qa_memory") ) # Row 3: Actions markup.add( InlineKeyboardButton("🎯 Job Radar", callback_data="radar"), InlineKeyboardButton("👁️ Ghost Mode Toggle", callback_data="ghost") ) # Row 4: Control state_btn = InlineKeyboardButton("▶️ Resume Bot", callback_data="resume") if BOT_PAUSED else InlineKeyboardButton("⏸️ Pause Bot", callback_data="pause") markup.add( state_btn, InlineKeyboardButton("🕒 Download History", callback_data="history") ) bot.reply_to(message, "🎛️ *Elite Bot Command Center*\n\n" "Use the interactive buttons below to control your automation empire instantly.", parse_mode=None, reply_markup=markup) @bot.message_handler(commands=['start']) def send_welcome(message): save_chat_id(message.chat.id) bot.reply_to(message, "👋 Welcome to *Elite Job Auto-Apply Bot!*\n\n" "🤖 I am now locked to your Chat ID and scanning jobs 24/7!\n\n" "📱 Use `/dashboard` or `/menu` to open your Interactive Command Center.\n\n" "Type `/help` to see all available text commands.", parse_mode=None) @bot.message_handler(commands=['help']) def send_help(message): save_chat_id(message.chat.id) help_text = """ 🤖 *ELITE JOB AUTO-APPLY BOT | COMMAND CENTER* 🤖 _Welcome to your fully autonomous AI job-hunting engine! Here is your complete manual:_ ⚡️ *CORE OPERATIONS* 🔹 `/start` - Wake up the bot and lock your Chat ID for notifications. 🔹 `/help` - Show this detailed command manual. 🔹 `/status` - 🩺 Check the heartbeat of the bot, API keys, and total jobs processed. 🔹 `/pause` - 🛑 Temporarily stop the background 24/7 scanning. 🔹 `/resume` - 🟢 Turn the background scanning back on. 🧠 *AI MEMORY & PROFILE* 🔹 `/profile` - 📋 View all your current resume details the bot uses to fill forms. 🔹 `/setprofile | ` - ✍️ Update a specific field dynamically (e.g., `/setprofile phone | +19876543210`). 🔹 `/qa` - 🗂 View all the tricky company questions the bot has permanently memorized. 🔹 `/answer | ` - 💡 Teach the bot how to answer a specific question so it never asks you again! 🔹 `/clearqa ` - 🗑 Delete a saved answer from the bot's brain. 📊 *ANALYTICS & MANUAL ACTION* 🔹 `/apply ` - 🎯 Force the bot to immediately apply to a specific job link you found. 🔹 `/instahyre | ` - 🚀 Trigger the Instahyre Mass-Applier (Submits 20 jobs instantly). 🔹 `/notion` - 📋 Open your Notion Job Tracker with full CRM details. 🔹 `/history` - 🕒 View the last 10 jobs the bot attempted, including success/fail status. 🔹 `/download` - 📥 Export a massive CSV Excel file of every single job ever applied to. _Tip: The bot sends a daily summary at 7 AM, runs Instahyre at 11 PM, and tracks your streak!_ 🔥 """ bot.reply_to(message, help_text, parse_mode=None) @bot.message_handler(commands=['instahyre']) def trigger_instahyre(message): save_chat_id(message.chat.id) text = message.text.replace("/instahyre", "", 1).strip() if "|" not in text: bot.reply_to(message, "⚠️ *Invalid Format*\n\nPlease provide your credentials like this:\n`/instahyre your_email@gmail.com | your_password`\n\n_Note: We do not save your password. It is only used once in memory to run the engine._", parse_mode=None) return parts = text.split("|", 1) email = parts[0].strip() password = parts[1].strip() bot.reply_to(message, "🚀 *Instahyre Mass-Applier Activated!* 🚀\n\nBooting up the headless browser... This will take about 5-10 minutes to finish applying to 20 jobs. I'll notify you when it's done!", parse_mode=None) def run_engine(): success, result_msg, _ = run_instahyre_mass_apply(email=email, password=password, skills="Software Engineer Fresher", max_applications=20) if success: bot.send_message(message.chat.id, f"✅ *Instahyre Campaign Complete!*\n\n{result_msg}", parse_mode=None) else: bot.send_message(message.chat.id, f"❌ *Instahyre Campaign Failed*\n\n{result_msg}", parse_mode=None) Thread(target=run_engine, daemon=True).start() @bot.message_handler(commands=['notion']) def show_notion(message): save_chat_id(message.chat.id) raw_db_id = os.getenv('NOTION_DATABASE_ID', '') notion_match = re.search(r'([a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12})', raw_db_id) notion_id = notion_match.group(1).replace('-', '') if notion_match else '' notion_link = f"https://notion.so/{notion_id}" if notion_id else "https://notion.so" stats = load_stats() total_app = stats.get('applied', 0) msg = ( "📋 *Notion Job Tracker*\n" "━━━━━━━━━━━━━━━━━━━━━━\n\n" "Your CRM tracks every job automatically:\n\n" "• 🏢 *Company* — Extracted via AI\n" "• 💼 *Role* — With level & location\n" "• 📊 *Status* — Found → Applied → Interview\n" "• 🔗 *Direct Link* — Click to view posting\n" "• 📅 *Date* — When it was found/applied\n" "• 📡 *Source* — Which channel/radar found it\n\n" f"📊 Today's Applications: *{total_app}*\n\n" "_Tap the button below to open your tracker._" ) markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("📋 Open Notion CRM", url=notion_link)) markup.add(InlineKeyboardButton("📱 Open Dashboard", url="https://gokuuc-myjob-bot.hf.space")) bot.reply_to(message, msg, parse_mode=None, reply_markup=markup) @bot.message_handler(commands=['status']) def send_status(message): save_chat_id(message.chat.id) applied = load_applied_jobs() stats = load_stats() resume_ok = "✅" if os.path.exists(RESUME_FILE) else "❌" gemini_ok = "✅" if gemini_client else "❌" groq_ok = "✅" if groq_client else "❌" bot_state = "⏸️ PAUSED" if BOT_PAUSED else "▶️ RUNNING" markup = InlineKeyboardMarkup(row_width=2) if BOT_PAUSED: markup.add(InlineKeyboardButton("▶️ Resume Bot", callback_data="resume")) else: markup.add(InlineKeyboardButton("⏸️ Pause Bot", callback_data="pause")) markup.add( InlineKeyboardButton("🕒 History", callback_data="history"), InlineKeyboardButton("🔄 Refresh Status", callback_data="status") ) status_text = ( f"🤖 *Elite Job Bot — Live Status*\n\n" f"⚙️ Bot State: `{bot_state}`\n" f"🎯 Total Jobs Processed: `{len(applied)}`\n" f"🚀 Applied Today: `{stats.get('applied', 0)}`\n" f"⏭️ Skipped Today: `{stats.get('skipped', 0)}`\n" f"🔥 Day Streak: `{stats.get('current_streak', 0)}`\n\n" f"🧠 Gemini AI: {gemini_ok}\n" f"⚡ Groq AI: {groq_ok}\n" f"📄 Resume: {resume_ok}\n" f"📡 Channel: @{TARGET_CHANNEL}" ) bot.reply_to(message, status_text, parse_mode=None, reply_markup=markup) @bot.message_handler(commands=['profile']) def send_profile(message): save_chat_id(message.chat.id) profile = load_profile() bot.reply_to(message, f"📋 Profile details loaded:\n\n{json.dumps(profile, indent=2)}") @bot.message_handler(commands=['history']) def send_history(message): save_chat_id(message.chat.id) csv_file = "applied_jobs_log.csv" if not os.path.exists(csv_file): bot.reply_to(message, "📭 No applications logged yet. The bot hasn't processed any jobs.") return try: with open(csv_file, "r", encoding="utf-8") as f: reader = csv.reader(f) rows = list(reader) if len(rows) <= 1: bot.reply_to(message, "📭 No applications logged yet.") return # Show last 10 entries (skip header) recent = rows[-10:] msg = "📊 **Last 10 Applications:**\n\n" for row in recent: if len(row) >= 5: date, title, url, status, notes = row[0], row[1], row[2], row[3], row[4] emoji = "✅" if status == "Applied" else "⏭️" msg += f"{emoji} {date}\n{title}\n{status}: {notes}\n{url}\n\n" elif len(row) >= 4: msg += f"• {row[0]} | {row[1]} | {row[3]}\n" bot.reply_to(message, msg, disable_web_page_preview=True) except Exception as e: bot.reply_to(message, f"⚠️ Error reading log: {e}") @bot.message_handler(commands=['download']) def send_download(message): save_chat_id(message.chat.id) csv_file = "applied_jobs_log.csv" if not os.path.exists(csv_file): bot.reply_to(message, "📭 No CSV log file exists yet.") return try: with open(csv_file, "rb") as f: bot.send_document(message.chat.id, f, caption="📎 Full application log (CSV)") except Exception as e: bot.reply_to(message, f"⚠️ Error sending file: {e}") @bot.message_handler(commands=['pause']) def pause_bot(message): save_chat_id(message.chat.id) global BOT_PAUSED BOT_PAUSED = True markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("▶️ Resume Bot", callback_data="resume")) bot.reply_to(message, "⏸️ *Bot Paused!*\n\nAuto job scanning is stopped.\nTap the button below to resume.", parse_mode=None, reply_markup=markup) @bot.message_handler(commands=['resume']) def resume_bot(message): save_chat_id(message.chat.id) global BOT_PAUSED BOT_PAUSED = False markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("⏸️ Pause Bot", callback_data="pause")) bot.reply_to(message, "▶️ *Bot Resumed!*\n\nAuto job scanning is now active! The bot will find and apply to jobs automatically.", parse_mode=None, reply_markup=markup) @bot.message_handler(commands=['watch', 'ghost']) def toggle_ghost_mode(message): chat_id = message.chat.id save_chat_id(chat_id) if chat_id in ghost_mode_chats: ghost_mode_chats.remove(chat_id) bot.reply_to(message, "👻 *Ghost Mode Disabled!*\n\nYou will no longer receive live streaming screenshots.", parse_mode=None) else: ghost_mode_chats.add(chat_id) bot.reply_to(message, "👻 *Ghost Mode ENABLED!*\n\nYou will now receive live visual updates while the bot fills out applications in real-time.", parse_mode=None) @bot.message_handler(commands=['scan_inbox', 'sync']) def command_scan_inbox(message): bot.reply_to(message, "📧 *Scanning Inbox for CRM Updates...*\nThis might take a few seconds.", parse_mode=None) import threading def _bg_scan(): try: from imap_handler import scan_for_interview_invites results = scan_for_interview_invites() if results: msg = f"🎉 *FOUND {len(results)} UPDATES!* CRM Synced.\n\n" for r in results: emoji = "📅" if r['status'] == "Interview" else "❌" msg += f"{emoji} *{r['company']}* ({r['status']})\n" bot.send_message(message.chat.id, msg, parse_mode=None) else: bot.send_message(message.chat.id, "📭 No new interview or rejection emails found in the last 7 days.") except Exception as e: bot.send_message(message.chat.id, f"⚠️ *Error scanning inbox:* {e}", parse_mode=None) threading.Thread(target=_bg_scan).start() @bot.callback_query_handler(func=lambda call: call.data in ["pause", "resume", "status", "history", "profile", "help", "last_job", "qa_memory", "radar", "ghost"]) def handle_button(call): global BOT_PAUSED chat_id = call.message.chat.id save_chat_id(chat_id) try: bot.answer_callback_query(call.id) # Dismiss the loading spinner except Exception: pass if call.data == "pause": BOT_PAUSED = True markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("▶️ Resume Bot", callback_data="resume")) try: bot.edit_message_text("⏸️ *Bot Paused!*\n\nAuto job scanning is stopped.\nTap below to resume.", chat_id=chat_id, message_id=call.message.message_id, parse_mode=None, reply_markup=markup) except Exception: bot.send_message(chat_id, "⏸️ *Bot Paused!*", parse_mode=None, reply_markup=markup) elif call.data == "resume": BOT_PAUSED = False markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("⏸️ Pause Bot", callback_data="pause")) try: bot.edit_message_text("▶️ *Bot Resumed!*\n\nScanning jobs 24/7 again!", chat_id=chat_id, message_id=call.message.message_id, parse_mode=None, reply_markup=markup) except Exception: bot.send_message(chat_id, "▶️ *Bot Resumed!*", parse_mode=None, reply_markup=markup) elif call.data == "status": applied = load_applied_jobs() stats = load_stats() resume_ok = "✅" if os.path.exists(RESUME_FILE) else "❌" gemini_ok = "✅" if gemini_client else "❌" groq_ok = "✅" if groq_client else "❌" bot_state = "⏸️ PAUSED" if BOT_PAUSED else "▶️ RUNNING" markup = InlineKeyboardMarkup(row_width=2) if BOT_PAUSED: markup.add(InlineKeyboardButton("▶️ Resume Bot", callback_data="resume")) else: markup.add(InlineKeyboardButton("⏸️ Pause Bot", callback_data="pause")) markup.add( InlineKeyboardButton("🕒 History", callback_data="history"), InlineKeyboardButton("🔄 Refresh", callback_data="status") ) # Calculate Success Rate success_count = 0 total_logs = 0 try: import csv if os.path.exists("applied_jobs_log.csv"): with open("applied_jobs_log.csv", "r", encoding="utf-8") as f: reader = csv.reader(f) next(reader, None) # skip header for row in reader: if len(row) > 2: total_logs += 1 if row[2] == "Applied": success_count += 1 except: pass success_rate = "0%" if total_logs > 0: success_rate = f"{int((success_count/total_logs)*100)}%" status_text = ( f"🤖 *Elite Job Bot — Live Status*\n\n" f"⚙️ Bot State: `{bot_state}`\n" f"🎯 Total Processed: `{len(applied)}`\n" f"🚀 Applied Today: `{stats.get('applied', 0)}`\n" f"🔥 Day Streak: `{stats.get('current_streak', 0)}`\n" f"📈 *Success Rate:* `{success_rate}` _({success_count}/{total_logs})_\n\n" f"✨ Gemini AI: {gemini_ok} | ⚡ Groq: {groq_ok}\n" f"📄 Resume: {resume_ok} | 📡 @{TARGET_CHANNEL}" ) try: bot.edit_message_text(status_text, chat_id=chat_id, message_id=call.message.message_id, parse_mode=None, reply_markup=markup) except Exception: bot.send_message(chat_id, status_text, parse_mode=None, reply_markup=markup) elif call.data == "history": csv_file = "applied_jobs_log.csv" if not os.path.exists(csv_file): bot.send_message(chat_id, "📭 No applications logged yet.") return try: with open(csv_file, "rb") as f: bot.send_document(chat_id, f, caption="🕒 Here is your full application history.") except Exception as e: bot.send_message(chat_id, f"⚠️ Error sending history: {e}") elif call.data == "profile": profile = load_profile() if not profile: bot.send_message(chat_id, "⚠️ Profile is empty! Use `/setprofile` to add your details.") else: msg = "📋 *Your Current Profile:*\n\n" for k, v in profile.items(): msg += f"• *{k.replace('_', ' ').title()}:* `{v}`\n" bot.send_message(chat_id, msg, parse_mode=None) elif call.data == "last_job": if os.path.exists("after_submit.png"): try: with open("after_submit.png", "rb") as ph: bot.send_photo(chat_id, ph, caption="📸 *Last Job Application Result*", parse_mode=None) except Exception as e: bot.send_message(chat_id, f"⚠️ Could not load image: {e}") else: bot.send_message(chat_id, "📭 No screenshot available yet. The bot hasn't applied to anything recently.") elif call.data == "qa_memory": qa_memory = load_qa_memory() if not qa_memory: bot.send_message(chat_id, "🧠 AI Memory is empty. No custom questions answered yet.") else: msg = f"🧠 *Saved Q&A Memory ({len(qa_memory)} answers):*\n\n" for i, (q, a) in enumerate(list(qa_memory.items())[:10], 1): # show top 10 msg += f"*Q:* {q}\n💬 _{a}_\n\n" if len(qa_memory) > 10: msg += f"_...and {len(qa_memory)-10} more. Use `/qa` to see all._" bot.send_message(chat_id, msg, parse_mode=None) elif call.data == "radar": bot.send_message(chat_id, "📡 *Job Radar Triggered!*\nScanning all configured Telegram channels right now for fresh jobs...", parse_mode=None) # We trigger the manual radar scan thread import threading threading.Thread(target=manual_radar_scan, args=(chat_id,)).start() elif call.data == "ghost": if chat_id in ghost_mode_chats: ghost_mode_chats.remove(chat_id) bot.send_message(chat_id, "👻 *Ghost Mode Disabled.*\nThe bot will now send you screenshots of every successful application.") else: ghost_mode_chats.add(chat_id) bot.send_message(chat_id, "👻 *Ghost Mode Activated!*\nThe bot is now fully stealth. It will apply to jobs silently in the background and will NOT send you screenshots or alerts. Check `/status` anytime.") elif call.data == "history": csv_file = "applied_jobs_log.csv" if not os.path.exists(csv_file): bot.send_message(chat_id, "📭 No applications logged yet.") return try: with open(csv_file, "r", encoding="utf-8") as f: rows = list(csv.reader(f)) recent = rows[-5:] msg = "🕒 *Last 5 Applications:*\n\n" for row in reversed(recent): if len(row) >= 4: emoji = "✅" if "Applied" in row[3] else "❌" msg += f"{emoji} {row[0]}\n📌 {row[1][:40]}\n📋 {row[3]}\n\n" markup = InlineKeyboardMarkup() markup.add(InlineKeyboardButton("🔄 Refresh", callback_data="history")) bot.send_message(chat_id, msg, parse_mode=None, reply_markup=markup, disable_web_page_preview=True) except Exception as e: bot.send_message(chat_id, f"⚠️ Error: {e}") elif call.data == "help": help_text = ( "🤖 *Command Reference*\n\n" "📊 /status — Live bot status\n" "⏸️ /pause — Stop auto-scanning\n" "▶️ /resume — Start auto-scanning\n" "👻 /watch — Toggle Live Ghost Mode\n" "🕒 /history — Last 10 applications\n" "📋 /profile — View your profile\n" "🎯 /apply \ — Apply to specific job\n" "📥 /download — Export CSV log" ) bot.send_message(chat_id, help_text, parse_mode=None) @bot.message_handler(commands=['setprofile']) def set_profile_field(message): save_chat_id(message.chat.id) # Format: /setprofile field | value text = message.text.replace("/setprofile", "", 1).strip() if "|" not in text: profile = load_profile() fields = ", ".join(profile.keys()) bot.reply_to(message, f"📝 *Update your profile:*\n`/setprofile | `\n\nAvailable fields: `{fields}`\n\nExample:\n`/setprofile phone | +91 9876543210`", parse_mode=None) return parts = text.split("|", 1) field = parts[0].strip().lower() value = parts[1].strip() profile = load_profile() if field not in profile: bot.reply_to(message, f"❌ Unknown field: `{field}`\n\nValid fields: {', '.join(profile.keys())}", parse_mode=None) return profile[field] = value try: with open(PROFILE_FILE, "w", encoding="utf-8") as f: json.dump(profile, f, indent=2) bot.reply_to(message, f"✅ Profile updated!\n\n*{field}* → `{value}`", parse_mode=None) except Exception as e: bot.reply_to(message, f"⚠️ Error saving profile: {e}") @bot.message_handler(commands=['clearqa']) def clear_qa(message): save_chat_id(message.chat.id) text = message.text.replace("/clearqa", "", 1).strip() qa_memory = load_qa_memory() if not text: if not qa_memory: bot.reply_to(message, "📭 No saved answers to clear.") return msg = "🗑️ *Which answer to delete?*\nSend `/clearqa `\n\n" for i, q in enumerate(qa_memory.keys(), 1): msg += f"*{i}.* {q}\n" bot.reply_to(message, msg, parse_mode=None) return if text.isdigit(): keys = list(qa_memory.keys()) idx = int(text) - 1 if 0 <= idx < len(keys): deleted_q = keys[idx] del qa_memory[deleted_q] save_qa_memory(qa_memory) bot.reply_to(message, f"🗑️ Deleted answer for:\n_{deleted_q}_", parse_mode=None) else: bot.reply_to(message, "❌ Invalid number.") else: if text in qa_memory: del qa_memory[text] save_qa_memory(qa_memory) bot.reply_to(message, f"🗑️ Deleted answer for:\n_{text}_", parse_mode=None) else: bot.reply_to(message, "❌ Question not found in memory.") # --- Inline button callback handler --- @bot.callback_query_handler(func=lambda call: call.data.startswith("qa_answer:")) def handle_qa_button(call): num = call.data.split(":", 1)[1] pending = load_pending_qa() question = pending.get(num, f"Question {num}") # Send a ForceReply so user can type answer directly as a reply markup = ForceReply(selective=True) sent = bot.send_message( call.message.chat.id, f"✏️ *Answer for Q{num}:*\n_{question}_\n\nType your answer below 👇", parse_mode=None, reply_markup=markup ) bot.answer_callback_query(call.id) # Register a next-step handler to capture the reply bot.register_next_step_handler(sent, lambda msg, q=question: _save_inline_answer(msg, q)) def _save_inline_answer(message, question): answer = message.text.strip() if not answer: bot.reply_to(message, "❌ Answer cannot be empty!") return qa_memory = load_qa_memory() qa_memory[question] = answer save_qa_memory(qa_memory) bot.reply_to(message, f"✅ *Saved!*\n\n❓ {question}\n💬 *{answer}*\n\n_I'll use this automatically in all future applications!_", parse_mode=None) @bot.message_handler(commands=['answer']) def save_answer(message): save_chat_id(message.chat.id) text = message.text.replace("/answer", "", 1).strip() if "|" not in text: bot.reply_to(message, "❌ Format: `/answer | `\n\nExample:\n`/answer 1 | Yes`", parse_mode=None) return parts = text.split("|", 1) key = parts[0].strip() # Either a number like "1" or the full question text answer = parts[1].strip() if not answer: bot.reply_to(message, "❌ Answer cannot be empty!") return # Resolve question text from number pending = load_pending_qa() if key.isdigit() and key in pending: question = pending[key] else: # Fallback: treat the key as full question text question = key # Save to permanent QA memory qa_memory = load_qa_memory() qa_memory[question] = answer save_qa_memory(qa_memory) bot.reply_to(message, f"✅ *Saved!*\n\n❓ {question}\n💬 *{answer}*\n\n_I will use this answer automatically from now on!_", parse_mode=None) @bot.message_handler(commands=['qa']) def show_qa_memory(message): save_chat_id(message.chat.id) qa_memory = load_qa_memory() if not qa_memory: bot.reply_to(message, "📭 No answers saved yet.\n\nWhen the bot encounters unknown questions, it will ask you via Telegram. Use `/answer | ` to teach it!", parse_mode=None) return msg = f"🧠 *Saved Q&A Memory ({len(qa_memory)} answers):*\n\n" for i, (q, a) in enumerate(qa_memory.items(), 1): msg += f"*Q{i}.* {q}\n💬 _{a}_\n\n" msg += "To update an answer, just use `/answer` again with the same question." bot.reply_to(message, msg, parse_mode=None) @bot.message_handler(commands=['apply']) def manual_apply(message): save_chat_id(message.chat.id) # Robust URL extraction urls = re.findall(r'(https?://[^\s]+)', message.text) if not urls: args = message.text.split() if len(args) < 2: bot.reply_to(message, "Usage: /apply ") return url = args[1] if not url.startswith("http"): url = "https://" + url else: url = urls[0] bot.reply_to(message, f"⌛ Manually initiating application for:\n{url}") # Run in thread so bot doesn't freeze def run(): try: final_url = bypass_blog_redirect(url) if any(domain in final_url.lower() for domain in UNSUPPORTED_DOMAINS): bot.send_message(message.chat.id, f"⚠️ Skipped: The platform or link ({final_url}) is not supported for auto-filling (e.g. Naukri, LinkedIn, YouTube, Telegram).") log_job(final_url, "Manual /apply", False, "Unsupported platform or social media") return success = run_playwright_apply(final_url, "Manual application requested by user") log_job(final_url, "Manual /apply", success, "Manual apply") if success: bot.send_message(message.chat.id, f"✅ Manual application submitted for:\n{url}") else: bot.send_message(message.chat.id, f"❌ Manual application failed for:\n{url}") except Exception as e: print(f"[Manual Apply] Error in run thread: {e}") try: bot.send_message(message.chat.id, f"⚠️ Bot error: {e}") except: pass Thread(target=run).start() @bot.message_handler(commands=['channels']) def show_channels(message): save_chat_id(message.chat.id) ch_status = load_channel_status() sleep_note = "💤 _Bot is in sleep hours (11 PM–6 AM). Will resume at 6 AM._\n\n" if is_sleep_time() else "" msg = f"📡 *Monitored Channels ({len(TARGET_CHANNELS)} total)*\n\n{sleep_note}" for ch in TARGET_CHANNELS: info = ch_status.get(ch, {}) status = info.get("status", "Not scanned yet") last = info.get("last_scan", "Never") found = info.get("jobs_found", 0) msg += f"• *@{ch}*\n 🕒 Last scan: `{last}` | 🔗 Applied: `{found}` | {status}\n\n" bot.reply_to(message, msg, parse_mode=None) @bot.message_handler(commands=['lastjob']) def show_last_job(message): save_chat_id(message.chat.id) job = load_last_job() if not job: bot.reply_to(message, "💭 No applications have been made yet. The bot hasn't found a matching job.") return msg = ( f"💼 *Last Application Attempt*\n\n" f"📍 *Channel:* @{job.get('channel', 'N/A')}\n" f"⏰ *Time:* {job.get('timestamp', 'N/A')}\n" f"📊 *Result:* {job.get('status', 'N/A')}\n\n" f"{job.get('summary', 'No summary available.')}\n\n" f"🔗 [View Job]({job.get('url', '#')})" ) bot.reply_to(message, msg, parse_mode=None, disable_web_page_preview=True) @bot.message_handler(commands=['ping']) def ping_test(message): bot.reply_to(message, "🏓 Pong! The cloud bot is alive and listening!") @bot.message_handler(commands=['radar', 'jobs']) def show_radar(message): save_chat_id(message.chat.id) bot.reply_to(message, "⏳ Loading latest Job Radar results...", parse_mode=None) try: with open("radar_results.json", "r", encoding="utf-8") as f: data = json.load(f) jobs = data.get("jobs", []) if not jobs: bot.reply_to(message, "📡 *Job Radar* found no recent jobs. Check again later or run a scan from the dashboard.", parse_mode=None) return def clean_md(text): if not text: return "" return str(text).replace("*", "").replace("_", "").replace("`", "").replace("[", "").replace("]", "") # Group jobs by source grouped = {} for j in jobs: grouped.setdefault(j.get("source", "Other"), []).append(j) msg = f"📡 *JOB RADAR — MULTI-PLATFORM*\n_Last scan: {data.get('last_scan', 'Unknown')}_ | 🎯 {len(jobs)} Jobs\n\n" source_headers = { "linkedin": "🌐 LINKEDIN", "indeed": "🟢 INDEED", "adzuna": "🎯 ADZUNA", "internshala": "🎓 INTERNSHALA", "unstop": "🚀 UNSTOP", "jobicy": "💼 JOBICY", "arbeitnow": "🇩🇪 ARBEITNOW", "remoteok": "🌴 REMOTEOK" } messages = [] for source, src_jobs in grouped.items(): s_key = source.lower().replace(" ", "") header_text = source_headers.get(s_key, f"📡 {source.upper()}") block = f"*{header_text}*\n━━━━━━━━━━━━━━━━━━━━\n" for j in src_jobs[:8]: # Limit to 8 per source title = clean_md(j.get("title", "Unknown Role")) company = clean_md(j.get("company", "Unknown")) loc = clean_md(j.get("location", "Remote")) link = j.get("link", "#") desc = clean_md(j.get("description", "")) date_str = clean_md(j.get("date_posted", "")) t_title = title[:45] + "..." if len(title) > 45 else title t_company = company[:25] + "..." if len(company) > 25 else company t_desc = desc[:150] + "..." if len(desc) > 150 else desc from datetime import datetime time_display = date_str if date_str and len(date_str) >= 10: try: dt = datetime.strptime(date_str[:10], "%Y-%m-%d") days = (datetime.now() - dt).days if days == 0: time_display = "Today" elif days == 1: time_display = "Yesterday" else: time_display = f"{days} days ago" except Exception: pass import random, string if "id" not in j: j["id"] = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) job_id = j["id"] desc_line = f"📝 _{t_desc}_\n" if t_desc else "" s_key_check = source.lower() unsafe_platforms = ["linkedin", "indeed", "naukri", "foundit"] is_safe = not any(p in s_key_check for p in unsafe_platforms) action_line = f"🔗 [Apply Manually]({link})\n" if is_safe: action_line += f"⚡ *Auto-Apply:* `/apply_{job_id}`\n" line = ( f"💼 *{t_title}*\n" f"🏢 {t_company} • 📍 {loc} • 🗓️ {time_display}\n" f"{desc_line}" f"{action_line}\n" ) if len(msg) + len(block) + len(line) > 3800: messages.append(msg) msg = f"📡 *JOB RADAR (continued)*\n\n" msg += block + line block = "" # Reset header block after printing once messages.append(msg) # Save generated IDs back with open("radar_results.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) for m in messages: bot.reply_to(message, m, parse_mode=None, disable_web_page_preview=True) time.sleep(0.5) except Exception as e: bot.reply_to(message, f"❌ Error loading radar results: {e}") @bot.message_handler(regexp=r"^/apply_([a-zA-Z0-9]+)$") def handle_auto_apply(message): job_id = message.text.split("_")[1] # Look up job_id in radar_results.json link = None try: with open("radar_results.json", "r", encoding="utf-8") as f: data = json.load(f) for j in data.get("jobs", []): if j.get("id") == job_id: link = j.get("link") break except Exception: pass if not link: bot.reply_to(message, "❌ Job not found or expired from Radar. Please apply manually if possible.") return bot.reply_to(message, f"⚡ *Initiating Auto-Apply Sequence!*\n\nTarget: {link}\n\n_The Playwright engine is taking over... check the web dashboard for live status!_", parse_mode=None, disable_web_page_preview=True) # Add to the global application queue application_queue.put(link) try: bot.send_message(message.chat.id, f"✅ Job added to the background queue. (Queue size: {application_queue.qsize()})") except: pass def daily_report_loop(): """Sends a daily summary at 9 AM and a weekly report every Monday.""" while True: now = datetime.now() chat_id = load_chat_id() # Daily report at 9:00 AM if now.hour == 9 and now.minute == 0: stats = load_stats() if bot and chat_id: channels_count = len(TARGET_CHANNELS) sleep_status = "💤 Sleeping" if is_sleep_time() else "🟢 Active" msg = ( f"📊 *Daily Report — {stats.get('date', '')}*\n\n" f"✅ Applied: *{stats.get('applied', 0)}* jobs\n" f"⏭️ Skipped: *{stats.get('skipped', 0)}* jobs\n" f"❌ Failed: *{stats.get('failed', 0)}* jobs\n" f"🔥 Streak: *{stats.get('current_streak', 0)}* days\n" f"📡 Channels: *{channels_count}* being monitored\n" f"🤖 Bot Status: {sleep_status}\n\n" f"Keep it up! 🚀" ) try: bot.send_message(chat_id, msg, parse_mode=None) except Exception as e: print(f"Error sending daily report: {e}") # --- The Ghosting Preventer --- try: import csv if os.path.exists("applied_jobs_log.csv") and bot and chat_id: with open("applied_jobs_log.csv", "r", encoding="utf-8") as f: rows = list(csv.reader(f)) follow_ups = [] for i in range(1, len(rows)): row = rows[i] if len(row) >= 4 and "Applied" in row[3] and "Followed Up" not in row[3]: date_str = row[0][:10] try: dt = datetime.strptime(date_str, "%Y-%m-%d") if (now - dt).days == 7: follow_ups.append((i, row[1], row[2])) except: pass if follow_ups: msg = f"👻 *GHOSTING PREVENTER ALERT*\nIt has been 7 days since you applied to {len(follow_ups)} jobs.\n\n" for idx, title, url in follow_ups[:10]: # Limit to 10 in message msg += f"💼 *{title[:30]}*\n🔗 [Job Link]({url})\n" # Mark as followed up so it doesn't alert again if len(rows[idx]) >= 5: rows[idx][4] = "Followed Up" else: rows[idx].append("Followed Up") msg += "\n📝 _I highly recommend finding the recruiter on LinkedIn or sending a follow-up email today!_" bot.send_message(chat_id, msg, parse_mode=None, disable_web_page_preview=True) with open("applied_jobs_log.csv", "w", newline="", encoding="utf-8") as f: csv.writer(f).writerows(rows) except Exception as e: print(f"Ghosting Preventer error: {e}") time.sleep(61) # Weekly report every Monday at 9:05 AM if now.weekday() == 0 and now.hour == 9 and now.minute == 5: wk = load_weekly_stats() if bot and chat_id: msg = ( f"📈 *Weekly Report — {wk.get('week', '')}*\n\n" f"✅ Total Applied: *{wk.get('applied', 0)}* jobs\n" f"❌ Total Failed: *{wk.get('failed', 0)}* jobs\n" f"📡 Channels Scanned: *{len(TARGET_CHANNELS)}*\n\n" f"_New week starting! Quotas reset. Bot is fully charged! ⚡_" ) try: bot.send_message(chat_id, msg, parse_mode=None) except Exception as e: print(f"Error sending weekly report: {e}") time.sleep(61) time.sleep(30) def run_telegram_polling(): print("Bot polling started...") # --- Conflict Prevention Feature --- # Detect if running locally (laptop) or on Hugging Face cloud. # Hugging Face sets the 'SPACE_ID' environment variable automatically. is_cloud = bool(os.environ.get("SPACE_ID")) if not is_cloud and not os.environ.get("FORCE_LOCAL"): print("⚠️ [Telegram] Running locally on laptop detected!") print("⚠️ [Telegram] Polling is DISABLED to prevent conflict with your Hugging Face cloud bot.") print("⚠️ [Telegram] Your cloud bot will handle all Telegram commands. Local script will only run background tasks.") print("⚠️ [Telegram] (To force local polling for testing, set FORCE_LOCAL=1 in .env)") while True: time.sleep(3600) # Keep thread alive cleanly so supervisor doesn't detect it as a crash # ----------------------------------- # CRITICAL: Remove any stale webhook to prevent polling conflicts try: bot.remove_webhook() print("[Telegram] Webhook cleared successfully. Starting clean polling...") # Set the bot commands in the menu (shown in "/" dashboard) from telebot.types import BotCommand commands = [ BotCommand("start", "❤️ Wake up & lock your Chat ID"), BotCommand("help", "📖 Full guide & all commands"), BotCommand("status", "🚑 Bot health, API & stats"), BotCommand("notion", "📋 Open Notion Job Tracker"), BotCommand("channels", "📡 View all monitored channels"), BotCommand("lastjob", "💼 See the last application attempt"), BotCommand("history", "📅 View last 10 applications"), BotCommand("radar", "📡 View latest multi-platform jobs"), BotCommand("instahyre", "🚀 Trigger Instahyre mass-apply"), BotCommand("apply", "🎯 Manually apply to a job URL"), BotCommand("pause", "⏸️ Pause auto-scanning"), BotCommand("resume", "▶️ Resume auto-scanning"), BotCommand("profile", "📝 View your resume profile"), BotCommand("setprofile", "✏️ Update a profile field"), BotCommand("qa", "🧠 View saved Q&A answers"), BotCommand("answer", "💡 Teach bot an answer"), BotCommand("clearqa", "🗑️ Delete a saved answer"), BotCommand("download", "📥 Export full CSV log"), ] bot.set_my_commands(commands) print("[Telegram] Bot commands menu updated.") except Exception as e: print(f"[Telegram] Error clearing webhook or setting commands: {e}") time.sleep(1) # Small delay to let webhook removal propagate while True: try: print("[Telegram] Starting polling loop...") bot.polling(non_stop=True, timeout=60, long_polling_timeout=30) except Exception as e: err_str = str(e) if "409" in err_str or "Conflict" in err_str: # Another bot instance is running (e.g. Hugging Face + local at same time) # Wait 60 seconds for the other instance to die before retrying print("[Telegram] 409 Conflict: Another bot instance detected. Waiting 60s...") time.sleep(60) else: print(f"[Telegram] Polling error: {e}") print("[Telegram] Retrying in 15 seconds...") time.sleep(15) # Start Job Radar background thread (runs every 6 hours) def radar_loop(): """Background thread that runs the Job Radar scan every 6 hours and sends results to Telegram.""" print("[Radar] Background radar thread started. First scan in 30 seconds...") time.sleep(30) # Initial delay so bot fully starts first while True: try: from job_radar import run_radar new_jobs = run_radar() # If no new jobs found, send a heartbeat so user knows radar is alive if not new_jobs: chat_id = load_chat_id() if bot and chat_id: try: bot.send_message(chat_id, "📡 *Job Radar Scan Complete*\n\n" "No new jobs found this cycle.\n" "⏰ Next scan in 6 hours.\n\n" "_The radar is running 24/7. You will be notified instantly when new matches appear._", parse_mode=None) except: pass # Sync new radar jobs to Notion CRM if new_jobs: try: gc = get_gemini_client() for job in new_jobs[:5]: # Sync top 5 to Notion try: sync_to_notion( job.get("link", ""), f"{job.get('title', '')} at {job.get('company', '')} - {job.get('location', '')}", "Found (Pending)", gc, override_company=job.get("company"), override_role=job.get("title"), groq_client=groq_client ) except: pass except: pass except Exception as e: print(f"[Radar] Loop error: {e}") time.sleep(6 * 60 * 60) # Sleep 6 hours between scans def cleanup_system_resources(): """Kills orphaned browser processes and cleans cache to prevent resource leaks.""" global playwright_active if playwright_active: print("[Cleanup] Playwright is currently active. Skipping resource cleanup...") return print("[Cleanup] Running periodic resource cleanup...") # 1. Kill orphaned chromium/playwright processes try: import psutil except ImportError: psutil = None if psutil is not None: for proc in psutil.process_iter(['pid', 'name']): try: name = proc.info['name'].lower() if 'chrome' in name or 'chromium' in name or 'playwright' in name: if proc.pid != os.getpid(): print(f"[Cleanup] Terminating orphaned process {name} (PID: {proc.pid})") proc.terminate() except Exception: pass else: # Fallback to OS commands if psutil isn't ready try: if os.name == 'nt': os.system("taskkill /f /im chrome.exe /fi \"pid ne " + str(os.getpid()) + "\" 2>nul") os.system("taskkill /f /im chromedriver.exe 2>nul") else: os.system("pkill -f -9 chromium 2>/dev/null") os.system("pkill -f -9 chrome 2>/dev/null") except Exception as e: print(f"[Cleanup] OS taskkill failed: {e}") # 2. Clean temporary chrome profile cache cache_path = os.path.abspath("chrome_profile/Default/Cache") if os.path.exists(cache_path): try: import shutil shutil.rmtree(cache_path) print("[Cleanup] Cleared chrome profile Cache directory.") except Exception as e: print(f"[Cleanup] Error clearing cache: {e}") def thread_supervisor(): """Monitors and automatically restarts background threads if they crash.""" print("[Supervisor] Thread supervisor loop started.") threads_config = { "Job Monitor": {"target": job_monitor_loop, "thread": None}, "Daily Report": {"target": daily_report_loop, "thread": None}, "Job Radar Loop": {"target": radar_loop, "thread": None}, } if bot: threads_config["Telegram Polling"] = {"target": run_telegram_polling, "thread": None} last_cleanup = 0 while True: try: for name, cfg in threads_config.items(): t = cfg["thread"] if t is None or not t.is_alive(): if t is not None: print(f"⚠️ [Supervisor] WARNING: Thread '{name}' died! Auto-restarting...") new_t = Thread(target=cfg["target"], name=name, daemon=True) new_t.start() cfg["thread"] = new_t # Periodically run system resource cleanup every 4 hours now = time.time() if now - last_cleanup > 4 * 60 * 60: cleanup_system_resources() last_cleanup = now except Exception as e: print(f"[Supervisor] Loop error: {e}") # ⏱️ HF-SAFE: Check thread health every 60s (not 15s). # This reduces the supervisor's own CPU overhead by 4x. time.sleep(60) if __name__ == "__main__": # Start the supervisor thread to spawn and maintain all workers supervisor_thread = Thread(target=thread_supervisor, daemon=True, name="Supervisor") supervisor_thread.start() # Start Flask on port 7860 app.run(host="0.0.0.0", port=7860)