tags)
regex_urls = re.findall(r'(https?://[^\s<>"]+)', message_text)
for u in regex_urls:
u = u.rstrip(").,!*'\"")
if u not in urls_found and not any(d in u.lower() for d in _skip_link_domains):
urls_found.append(u)
if not urls_found:
continue
for job_link in urls_found:
job_link = job_link.rstrip(").,!*'\"")
if job_link in applied_jobs:
continue
# Cross-channel duplicate check (same scan cycle)
if job_link in _seen_this_cycle:
print(f"[Dedup] Skipping duplicate seen in another channel: {job_link}")
continue
_seen_this_cycle.add(job_link)
print(f"[Scraper] @{channel_name} -> New link: {job_link}")
# Resolve redirects
final_url = bypass_blog_redirect(job_link)
print(f"[Scraper] Resolved: {final_url}")
# ─── BUG FIX 1: Detect Government / Registration-Only Portals ───
# These sites require manual registration (Aadhaar, OTP, fee payment etc.)
# Auto-applying here would result in fake/incomplete submissions.
GOVT_REGISTRATION_DOMAINS = [
# Central govt recruiters
"bhel.in", "nmdc.co.in", "rites.com", "ongcindia.com", "indianoil.in",
"ntpc.co.in", "powergridindia.com", "gailonline.com", "nalcoindia.com",
"sail.co.in", "hindustancopper.com", "meclindia.com", "npcil.nic.in",
"ireda.gov.in", "recpdcl.in", "thdcil.co.in",
# Railways
"indianrailways.gov.in", "rrcb.in", "rrcnr.org", "rrcer.com", "rrcpryj.org",
"rrcecr.gov.in", "rrcnwr.com", "rrcaldiengg.com", "scr.indianrailways",
# Defence / Police
"joinindianarmy.nic.in", "joinindiannavy.gov.in", "careerindianairforce.cdac.in",
"agnipathvayu.cdac.in", "ssbcrpf.gov.in", "bsf.gov.in", "cisf.gov.in",
# SSC / UPSC / State PSC
"ssc.nic.in", "ssconline.nic.in", "upsc.gov.in", "upsconline.nic.in",
"tnpsc.gov.in", "mpsconline.gov.in", "kpsc.kar.nic.in",
# Banking
"ibps.in", "sbi.co.in", "rbi.org.in", "nabard.org",
# State Govt / Apprentice portals
"apprenticeshipindia.org", "apprenticeship.gov.in", "mhrdnats.gov.in",
"nats.education.gov.in", "boat-srp.com",
# Generic govt TLDs patterns
".nic.in", ".gov.in",
]
is_govt_portal = any(domain in final_url.lower() for domain in GOVT_REGISTRATION_DOMAINS)
if is_govt_portal:
print(f"[Scraper] ⚠️ Government/Registration portal detected — skipping auto-apply: {final_url}")
applied_jobs.add(job_link)
save_applied_job(job_link)
# Send a dedicated manual-apply alert to Telegram
if bot and active_chat_id:
try:
channel_post_url = f"https://t.me/s/{channel_name}"
# Truncate channel post text to 600 chars for readability
post_preview = message_text[:600].strip()
if len(message_text) > 600:
post_preview += "..."
manual_msg = (
f"🏛️ *Government / Registration Portal Detected!*\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"📡 *Channel:* [@{channel_name}]({channel_post_url})\n\n"
f"📋 *Post Content:*\n{post_preview}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"⚠️ _This portal requires manual registration (Aadhaar/OTP/fee). Auto-apply skipped to avoid fake submissions._\n\n"
f"👉 *Please register manually at the official link below.*"
)
markup = InlineKeyboardMarkup()
markup.row(
InlineKeyboardButton("🏛️ Open Registration Portal", url=final_url),
InlineKeyboardButton("🔗 View Channel Post", url=channel_post_url)
)
bot.send_message(active_chat_id, manual_msg, parse_mode=None,
disable_web_page_preview=True, reply_markup=markup)
except Exception as govt_e:
print(f"[Scraper] Failed to send govt portal alert: {govt_e}")
log_job(final_url, message_text, False, "Government registration portal — manual apply required")
continue
# ──────────────────────────────────────────────────────────────────
# Skip unsupported platforms
if any(domain in final_url.lower() for domain in UNSUPPORTED_DOMAINS):
print(f"[Scraper] Skipping unsupported platform: {final_url}")
applied_jobs.add(job_link)
save_applied_job(job_link)
# Send unsupported platform link to Telegram so user can apply manually
if bot and active_chat_id:
try:
channel_post_url = f"https://t.me/s/{channel_name}"
post_preview = message_text[:500].strip()
if len(message_text) > 500:
post_preview += "..."
unsup_msg = (
f"🔗 *Job Link from @{channel_name}*\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"📋 *Post:*\n{post_preview}\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"⏭️ _Platform requires manual apply (LinkedIn/Naukri/etc.)_"
)
markup = InlineKeyboardMarkup()
markup.row(
InlineKeyboardButton("🌐 Apply Manually", url=final_url),
InlineKeyboardButton("🔗 Channel Post", url=channel_post_url)
)
bot.send_message(active_chat_id, unsup_msg, parse_mode=None,
disable_web_page_preview=True, reply_markup=markup)
except Exception as unsup_e:
print(f"[Scraper] Failed to send unsupported link alert: {unsup_e}")
log_job(final_url, message_text, False, "Unsupported platform")
continue
# AI Smart Filter — strict fresher+engineering check
try:
is_match, job_summary = check_job_match(message_text, profile)
time.sleep(random.uniform(4.5, 6.0)) # Respect RPM limit
except Exception as filter_e:
if "GEMINI_RATE_LIMIT" in str(filter_e):
print("[Scraper] Rate limit in filter. Stopping cycle.")
if bot and active_chat_id:
try: bot.send_message(active_chat_id, "⏳ *API Cooldown Activated*\n\n_Speed limit hit while filtering jobs. Sleeping 15 min. Jobs are safe!_", parse_mode=None)
except: pass
raise Exception("GEMINI_RATE_LIMIT")
is_match, job_summary = True, str(filter_e)
if not is_match:
print(f"[Scraper] AI Rejected: {job_summary[:80]}")
applied_jobs.add(job_link)
save_applied_job(job_link)
log_job(final_url, message_text, False, f"AI Rejected: {job_summary[:80]}")
continue
# ✅ Job matched — send summary card to Telegram BEFORE applying
# BUG FIX 3: Include full channel post text so user sees what the channel posted
if bot and active_chat_id:
try:
channel_post_url = f"https://t.me/s/{channel_name}"
# Truncate channel post to 400 chars to keep message clean
post_preview = message_text[:400].strip()
if len(message_text) > 400:
post_preview += "..."
notification = (
f"🎯 *New Job Found!*\n"
f"📡 *Channel:* [@{channel_name}]({channel_post_url})\n\n"
f"📋 *Channel Post:*\n{post_preview}\n\n"
f"━━━━━━━━━━━━━━━━━━\n"
f"{job_summary}\n\n"
f"━━━━━━━━━━━━━━━━━━\n"
f"🚀 *Now Applying Automatically...*"
)
markup = InlineKeyboardMarkup()
markup.row(
InlineKeyboardButton("🏢 Apply Manually", url=final_url),
InlineKeyboardButton("🔗 View in Channel", url=channel_post_url)
)
bot.send_message(active_chat_id, notification, parse_mode=None, disable_web_page_preview=True, reply_markup=markup)
except Exception as notif_e:
print(f"[Scraper] Failed to send job summary: {notif_e}")
# ✅ Sync to Notion immediately so user sees it in CRM even if application fails
try:
gc = get_gemini_client()
# Attempt to extract exact company from the message (usually line 1 starts with "Title:" or "Company:")
extracted_company = None
first_line = message_text.split('\n')[0]
if "Hiring" in first_line or "Recruitment" in first_line:
# Remove emojis
clean_line = re.sub(r'[^\w\s.,&-]', '', first_line).strip()
# Example: "Title: NTT DATA Hiring Freshers 2026" -> "NTT DATA"
extracted_company = clean_line.replace('Title', '').replace(':', '').split('Hiring')[0].split('Recruitment')[0].strip()
if len(extracted_company) < 2: extracted_company = None
sync_to_notion(final_url, message_text, "Found (Pending)", gc, override_company=extracted_company, groq_client=groq_client)
except Exception as e:
print(f"[Scraper] Failed to sync initial job to Notion: {e}")
# Save last job BEFORE applying
save_last_job({
"channel": channel_name, "url": final_url,
"summary": job_summary,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"status": "⏳ Attempting..."
})
# Apply!
attempts_this_cycle += 1
try:
success = run_playwright_apply(final_url, message_text)
applied_jobs.add(job_link)
save_applied_job(job_link)
# Update last job with result
result_status = "✅ Applied" if success else "❌ Failed"
save_last_job({
"channel": channel_name, "url": final_url,
"summary": job_summary,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"status": result_status
})
if success:
log_job(final_url, message_text, True, "Auto applied")
new_jobs_found += 1
ch_status[channel_name]["jobs_found"] = ch_status[channel_name].get("jobs_found", 0) + 1
wk = load_weekly_stats(); wk["applied"] += 1; save_weekly_stats(wk)
# Update Notion status to Applied
try:
gc = get_gemini_client()
sync_to_notion(final_url, message_text, "Applied", gc, groq_client=groq_client)
except: pass
else:
log_job(final_url, message_text, False, "Failed", is_failed=True)
retry_queue = load_retry_queue()
if final_url not in retry_queue:
retry_queue[final_url] = {"timestamp": time.time(), "attempts": 1}
save_retry_queue(retry_queue)
wk = load_weekly_stats(); wk["failed"] += 1; save_weekly_stats(wk)
# Notify user about failure WITH screenshot
if bot and active_chat_id:
try:
fail_msg = f"❌ *Application Failed*\n🔗 {final_url}\n\n_Job added to retry queue. You can apply manually._"
screenshot_path = "after_submit.png"
if os.path.exists(screenshot_path):
with open(screenshot_path, "rb") as photo:
bot.send_photo(active_chat_id, photo, caption=fail_msg, parse_mode=None)
else:
bot.send_message(active_chat_id, fail_msg, parse_mode=None, disable_web_page_preview=True)
except: pass
time.sleep(random.uniform(30, 60))
except Exception as apply_e:
if "GEMINI_RATE_LIMIT" in str(apply_e):
print("[Scraper] Rate limit during application. Stopping cycle.")
if bot and active_chat_id:
try: bot.send_message(active_chat_id, "⏳ *API Cooldown Activated*\n\n_Speed limit hit while applying. Sleeping 15 min. Jobs are safe!_", parse_mode=None)
except: pass
raise Exception("GEMINI_RATE_LIMIT")
else:
err_str = str(apply_e)
print(f"[Scraper] Error applying: {err_str}")
applied_jobs.add(job_link)
save_applied_job(job_link)
log_job(final_url, message_text, False, f"Error: {err_str}")
# Send error notification to Telegram WITH screenshot
if bot and active_chat_id:
try:
err_msg = f"⚠️ *Application Error*\n🔗 {final_url}\n\n*Error:* `{err_str[:200]}`\n\n_Job saved. Try manually if needed._"
screenshot_path = "after_submit.png"
if os.path.exists(screenshot_path):
with open(screenshot_path, "rb") as photo:
bot.send_photo(active_chat_id, photo, caption=err_msg[:1024], parse_mode=None)
else:
bot.send_message(active_chat_id, err_msg, parse_mode=None, disable_web_page_preview=True)
except: pass
time.sleep(random.uniform(30, 60))
# BUG FIX 2: Raised per-channel cap from 2 to 5 so more jobs are processed
if new_jobs_found >= 5 or attempts_this_cycle >= 5:
print(f"[Scraper] @{channel_name} cycle limit reached.")
break
if new_jobs_found >= 5 or attempts_this_cycle >= 5:
break
ch_status[channel_name]["last_scan"] = datetime.now().strftime("%H:%M")
ch_status[channel_name]["status"] = f"✅ {new_jobs_found} applied"
save_channel_status(ch_status)
return new_jobs_found, attempts_this_cycle
def scrape_telegram_channel():
"""
Loops through all TARGET_CHANNELS and scrapes each one for new engineering fresher jobs.
"""
global BOT_PAUSED, _seen_this_cycle
if BOT_PAUSED:
print("[Loop] Bot is paused. Skipping this cycle.")
return
# Smart sleep: rest between 11 PM and 6 AM
if is_sleep_time():
print("[Loop] Sleep hours (11 PM–6 AM). Bot resting to avoid detection.")
return
# Reset duplicate tracker each cycle
_seen_this_cycle = set()
active_chat_id = load_chat_id()
applied_jobs = load_applied_jobs()
# --- Auto-Retry Failed Applications ---
retry_queue = load_retry_queue()
now_ts = time.time()
for job_url, data in list(retry_queue.items()):
if now_ts - data["timestamp"] > 1800: # 30 minutes
print(f"[Retry] Retrying failed job: {job_url}")
try:
success = run_playwright_apply(job_url)
if success:
print(f"[Retry] Success! {job_url}")
log_job(job_url, "Retried", success, "Auto applied on retry")
del retry_queue[job_url]
else:
data["attempts"] += 1
if data["attempts"] >= 3:
print(f"[Retry] Giving up on {job_url} after 3 attempts.")
del retry_queue[job_url]
save_retry_queue(retry_queue)
time.sleep(random.uniform(30, 60))
except Exception as retry_e:
if "GEMINI_RATE_LIMIT" in str(retry_e):
if bot and active_chat_id:
try: bot.send_message(active_chat_id, "⏳ *API Cooldown Activated*\n\n_Speed limit hit during retry. Sleeping 15 min._", parse_mode=None)
except: pass
return
print(f"[Retry] Error: {retry_e}")
time.sleep(10)
# --- Scrape each channel in the list ---
total_applied = 0
for channel in TARGET_CHANNELS:
# BUG FIX 2: Raised global cycle cap from 3 to 15 so all 20+ channel jobs get processed
if total_applied >= 15:
print("[Loop] Global cycle limit (15 applications) reached. Stopping.")
break
print(f"\n[Loop] === Scanning @{channel} ===")
try:
found, attempts = scrape_single_channel(channel, applied_jobs, active_chat_id)
total_applied += found
if attempts > 0:
time.sleep(random.uniform(10, 20)) # Pause between channels
except Exception as e:
if "GEMINI_RATE_LIMIT" in str(e):
return # Stop entire cycle on rate limit
print(f"[Loop] Error on @{channel}: {e}")
continue
# --- 6. BACKGROUND MONITOR LOOP ---
def job_monitor_loop():
print("[Loop] Background Job Monitor Loop started...")
while True:
if BOT_PAUSED:
time.sleep(5) # Check state every 5 seconds when paused
continue
try:
print("[Loop] Checking Telegram channel for new jobs...")
scrape_telegram_channel()
# Feature: Check for interview requests in email
bot_email = os.getenv("BOT_EMAIL")
bot_pw = os.getenv("BOT_EMAIL_PASSWORD")
if bot_email and bot_pw:
print("[Loop] Checking inbox for Interview Requests...")
alerts = check_for_interviews(bot_email, bot_pw)
for alert in alerts:
active_chat_id = load_chat_id()
if bot and active_chat_id:
try:
if alert["image"] and os.path.exists(alert["image"]):
with open(alert["image"], "rb") as photo:
bot.send_photo(active_chat_id, photo, caption=alert["text"], parse_mode=None)
else:
bot.send_message(active_chat_id, alert["text"], parse_mode=None)
except Exception as e:
print(f"[Loop] Error sending interview alert: {e}")
except Exception as e:
print(f"Error in monitor loop: {e}")
# Feature: Morning Standup Daily Briefing (7 AM)
global last_briefing_date, last_notion_digest_date
now = datetime.now()
if now.hour == 7 and now.date() != last_briefing_date:
print("[Loop] Triggering 7 AM Morning Summary...")
active_chat_id_local = load_chat_id()
if bot and active_chat_id_local:
try:
stats_data = load_stats()
total_app = stats_data.get('applied', 0)
total_skip = stats_data.get('skipped', 0)
total_fail = stats_data.get('failed', 0)
streak = stats_data.get('current_streak', 0)
# Load radar results for found jobs count
radar_found = 0
radar_sources = 0
try:
if os.path.exists("radar_results.json"):
with open("radar_results.json", "r", encoding="utf-8") as rf:
radar_data = json.load(rf)
radar_found = radar_data.get("total", 0)
radar_sources = len(set(j.get("source", "") for j in radar_data.get("jobs", [])))
except: pass
# Load recent applications from CSV
recent_apps = []
try:
import csv as csv_mod
if os.path.exists("applied_jobs_log.csv"):
with open("applied_jobs_log.csv", "r", encoding="utf-8") as cf:
rows = list(csv_mod.reader(cf))
yesterday = (now - __import__('datetime').timedelta(days=1)).strftime("%Y-%m-%d")
for row in rows[1:]:
if len(row) >= 4 and row[0][:10] == yesterday:
recent_apps.append(row)
except: pass
# Build comprehensive morning message
channels_count = len(TARGET_CHANNELS)
briefing = (
"🌅 *Good Morning! Daily Job Summary — 7 AM* ☕\n"
"━━━━━━━━━━━━━━━━━━━━━━\n\n"
"🤖 _I worked all night. Here's your summary:_\n\n"
f"📊 *Yesterday's Results:*\n"
f" ✅ Applied: *{total_app}* jobs\n"
f" ⏭️ Filtered: *{total_skip}* jobs\n"
f" ❌ Failed: *{total_fail}* jobs\n"
f" 🔥 Streak: *{streak}* days\n\n"
f"📡 *Job Radar:*\n"
f" 🆕 Found: *{radar_found}* jobs from *{radar_sources}* sources\n"
f" 📡 Channels: *{channels_count}* monitored\n\n"
)
# Add recent successful applications
if recent_apps:
briefing += "💼 *Recently Applied To:*\n"
for app in recent_apps[:5]:
title = app[1][:35] if len(app) > 1 else "Unknown"
status = app[3][:20] if len(app) > 3 else "Unknown"
briefing += f" • {title} — {status}\n"
if len(recent_apps) > 5:
briefing += f" _...+{len(recent_apps)-5} more_\n"
briefing += "\n"
briefing += (
"━━━━━━━━━━━━━━━━━━━━━━\n"
"_Enjoy your day while I keep hunting! 🚀_"
)
markup = InlineKeyboardMarkup()
# Build Notion CRM link
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"
markup.row(
InlineKeyboardButton("📋 Notion CRM", url=notion_link),
InlineKeyboardButton("📱 Dashboard", url="https://gokuuc-myjob-bot.hf.space")
)
bot.send_message(active_chat_id_local, briefing, parse_mode=None, reply_markup=markup)
# Reset stats for the new day
save_stats({"date": now.strftime("%Y-%m-%d"), "applied": 0, "skipped": 0, "failed": 0, "current_streak": streak, "last_apply_date": stats_data.get("last_apply_date", "")})
last_briefing_date = now.date()
except Exception as err:
print(f"[Loop] Failed to send 7 AM Summary: {err}")
# Feature: Nightly 10 PM Notion CRM Digest
if now.hour == 22 and now.date() != last_notion_digest_date:
print("[Loop] Triggering 10 PM Notion CRM Digest...")
active_chat_id_local = load_chat_id()
if bot and active_chat_id_local:
try:
stats_data = load_stats()
total_app = stats_data.get('applied', 0)
total_skip = stats_data.get('skipped', 0)
# Build Notion CRM link
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"
digest = (
"🌙 *Nightly CRM Digest — 10 PM*\n"
"━━━━━━━━━━━━━━━━━━━━━━\n\n"
f"📊 *Today's Activity:*\n"
f"✅ Applied: *{total_app}* jobs\n"
f"⏭️ Filtered: *{total_skip}* jobs\n\n"
f"📋 *Your Notion Job Tracker has been updated with all jobs found today.*\n\n"
f"Each entry includes:\n"
f"• 🏢 Company Name\n"
f"• 💼 Role / Position\n"
f"• 📊 Status (Found/Applied)\n"
f"• 🔗 Direct Apply Link\n"
f"• 📅 Date Applied\n\n"
f"_Tap the button below to review your full CRM 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.send_message(active_chat_id_local, digest, parse_mode=None, reply_markup=markup)
last_notion_digest_date = now.date()
print("[Loop] 10 PM Notion digest sent successfully!")
except Exception as err:
print(f"[Loop] Failed to send Notion digest: {err}")
# Feature: 11 PM Instahyre Auto-Search — DISABLED for HF free-tier safety.
# The Playwright engine is now MANUAL-ONLY (triggered from dashboard or Telegram).
# Running headless Chrome automatically every night is the #1 reason accounts get banned.
# Use the dashboard "LAUNCH CAMPAIGN NOW" button or /instahyre Telegram command instead.
# if now.hour == 23 and now.date() != getattr(job_monitor_loop, '_last_instahyre_date', None):
# ... (auto-run disabled for HF safety)
# ⏱️ HF-SAFE: Wait 15 minutes before the next scrape cycle.
# Broken into 30-second chunks so the bot can pause instantly.
# This reduces CPU usage by 5x compared to the old 3-minute cycle.
for _ in range(30): # 30 x 30s = 15 minutes
if BOT_PAUSED:
break
time.sleep(30)
# --- 7. FLASK WEB SERVER (PORT 7860 FOR HUGGING FACE) ---
from flask import send_file, jsonify, request
import io
import time as _time_module
_server_start_time = _time_module.time() # Track when server started (for uptime)
app = Flask(__name__)
@app.route("/live")
def live_handoff():
if not HANDOFF_ACTIVE:
return "No active handoff required. Bot is running fine!
", 200
return f"""
Live Takeover Handoff
🚨 Live Browser Takeover
Job URL: {HANDOFF_URL}
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 += '| Status | Job URL / Title | Date | CRM Actions |
'
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'''
| {badge} |
{title} |
{date} |
|
'''
recent_jobs_html += '
'
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)