Spaces:
Sleeping
Sleeping
| import io | |
| import os | |
| import sys | |
| import time | |
| import socket | |
| import shutil | |
| import subprocess | |
| # Fix Windows cp1252 encoding | |
| if hasattr(sys.stdout, "buffer"): | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") | |
| if hasattr(sys.stderr, "buffer"): | |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") | |
| from selenium import webdriver | |
| from selenium.webdriver.chrome.service import Service | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from selenium.webdriver.common.keys import Keys | |
| from selenium.webdriver.common.action_chains import ActionChains | |
| from selenium.webdriver.support.ui import WebDriverWait | |
| from selenium.webdriver.support import expected_conditions as EC | |
| from selenium.common.exceptions import ( | |
| TimeoutException, | |
| NoSuchElementException, | |
| StaleElementReferenceException, | |
| WebDriverException, | |
| ) | |
| from webdriver_manager.chrome import ChromeDriverManager | |
| # ───────────────────────────────────────────── | |
| # CONFIG | |
| # ───────────────────────────────────────────── | |
| DEBUG_PORT = 9224 | |
| AUTH_TIMEOUT = 120 # seconds to wait for you to log in manually | |
| ELEMENT_TIMEOUT = 30 | |
| # Google Sheets — paste the full URL of your sheet here | |
| # e.g. "https://docs.google.com/spreadsheets/d/XXXX/edit" | |
| GOOGLE_SHEET_URL = "https://docs.google.com/spreadsheets/d/14HcGbkrDpCTcvoParYHWCmT1Y-p4Q1Gz7m2QzdHqdx0/edit?usp=sharing" | |
| # Discord — paste your webhook URL here | |
| # e.g. "https://discord.com/api/webhooks/123456/abcdef" | |
| DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1482211219187437750/LNBwNAIWiT4IXJvZLeWOL2uEw3Rspvt7YBfm3kKB9ha6jJ4bWQhWz_ZzYydVHWAXT2N8" | |
| # ───────────────────────────────────────────── | |
| # HELPERS | |
| # ───────────────────────────────────────────── | |
| def log(msg): print(f"[+] {msg}", flush=True) | |
| def warn(msg): print(f"[!] {msg}", flush=True) | |
| def err(msg): print(f"[ERROR] {msg}", flush=True) | |
| def find_chrome(): | |
| for path in [ | |
| os.path.join(os.environ.get("ProgramFiles", ""), "Google", "Chrome", "Application", "chrome.exe"), | |
| os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Google", "Chrome", "Application", "chrome.exe"), | |
| os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "Application", "chrome.exe"), | |
| ]: | |
| if path and os.path.exists(path): | |
| return path | |
| raise FileNotFoundError("Chrome not found.") | |
| def kill_chrome(): | |
| log("Killing Chrome and chromedriver ...") | |
| for _ in range(3): | |
| subprocess.run(["taskkill", "/f", "/im", "chrome.exe"], capture_output=True) | |
| subprocess.run(["taskkill", "/f", "/im", "chromedriver.exe"], capture_output=True) | |
| time.sleep(1) | |
| for _ in range(15): | |
| r = subprocess.run('tasklist /fi "imagename eq chrome.exe" /fo csv /nh', | |
| capture_output=True, shell=True, text=True) | |
| if "chrome.exe" not in r.stdout.lower(): | |
| log("Chrome is dead.") | |
| return | |
| time.sleep(1) | |
| warn("Chrome may still be running.") | |
| def clone_profile(): | |
| local = os.environ.get("LOCALAPPDATA", "") | |
| src = os.path.join(local, "Google", "Chrome", "User Data") | |
| tmp = os.path.join(os.environ.get("TEMP", "C:\\Temp"), "chrome_demo_profile") | |
| log(f"Nuking old temp profile at {tmp} ...") | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| time.sleep(1) | |
| dest_profile = os.path.join(tmp, "Default") | |
| os.makedirs(dest_profile, exist_ok=True) | |
| # Local State — needed for DPAPI cookie decryption | |
| ls_src = os.path.join(src, "Local State") | |
| if os.path.exists(ls_src): | |
| try: | |
| shutil.copy2(ls_src, os.path.join(tmp, "Local State")) | |
| log("Copied Local State.") | |
| except Exception as e: | |
| warn(f"Local State copy failed: {e}") | |
| for item in ["Cookies", "Preferences", "Network"]: | |
| s = os.path.join(src, "Default", item) | |
| d = os.path.join(dest_profile, item) | |
| if not os.path.exists(s): | |
| continue | |
| try: | |
| if os.path.isdir(s): | |
| shutil.copytree(s, d) | |
| else: | |
| shutil.copy2(s, d) | |
| log(f"Copied {item}") | |
| except Exception as e: | |
| warn(f"Could not copy {item}: {e}") | |
| return tmp, "Default" | |
| def launch_chrome(user_data_dir, profile_dir, chrome_exe): | |
| cmd = [ | |
| chrome_exe, | |
| f"--user-data-dir={user_data_dir}", | |
| f"--profile-directory={profile_dir}", | |
| f"--remote-debugging-port={DEBUG_PORT}", | |
| "--start-maximized", | |
| "--no-sandbox", | |
| "--disable-dev-shm-usage", | |
| "--disable-extensions", | |
| "--no-first-run", | |
| "--no-default-browser-check", | |
| "--disable-popup-blocking", | |
| "about:blank", | |
| ] | |
| log(f"Launching Chrome (port {DEBUG_PORT}) ...") | |
| subprocess.Popen(cmd) | |
| log("Waiting for debug port ...") | |
| for i in range(30): | |
| try: | |
| with socket.create_connection(("127.0.0.1", DEBUG_PORT), timeout=1): | |
| log(f"Port open after {i+1}s.") | |
| time.sleep(2) | |
| return | |
| except OSError: | |
| time.sleep(1) | |
| warn("Debug port never opened.") | |
| def attach_selenium(): | |
| opts = Options() | |
| opts.add_experimental_option("debuggerAddress", f"127.0.0.1:{DEBUG_PORT}") | |
| svc = Service(ChromeDriverManager().install()) | |
| driver = webdriver.Chrome(service=svc, options=opts) | |
| log("Selenium attached.") | |
| return driver | |
| def wait_for_url(driver, fragment, timeout=AUTH_TIMEOUT, label=""): | |
| log(f"Waiting for URL fragment '{fragment}' ({label}) ...") | |
| try: | |
| WebDriverWait(driver, timeout).until( | |
| lambda d: fragment in d.current_url.lower() | |
| ) | |
| log("URL matched.") | |
| time.sleep(2) | |
| return True | |
| except TimeoutException: | |
| err(f"Timed out waiting for '{fragment}'.") | |
| return False | |
| def safe_click(driver, el): | |
| try: | |
| el.click() | |
| except Exception: | |
| driver.execute_script("arguments[0].click();", el) | |
| def wait_and_find(driver, css, timeout=ELEMENT_TIMEOUT): | |
| return WebDriverWait(driver, timeout).until( | |
| EC.element_to_be_clickable((By.CSS_SELECTOR, css)) | |
| ) | |
| def new_tab(driver, url): | |
| driver.execute_script(f"window.open('{url}', '_blank');") | |
| driver.switch_to.window(driver.window_handles[-1]) | |
| time.sleep(3) | |
| # ───────────────────────────────────────────── | |
| # PHASE 1 — Gmail | |
| # ───────────────────────────────────────────── | |
| def phase_gmail(driver): | |
| log("--- PHASE 1: Gmail ---") | |
| driver.get("https://mail.google.com/mail/u/0/#inbox") | |
| time.sleep(3) | |
| if any(x in driver.current_url.lower() for x in ["accounts.google", "signin", "servicelogin"]): | |
| log("Please log in to Gmail in the browser ...") | |
| if not wait_for_url(driver, "mail.google.com", label="Gmail"): | |
| return "" | |
| log("Waiting for inbox to load ...") | |
| try: | |
| WebDriverWait(driver, 30).until( | |
| EC.presence_of_element_located((By.CSS_SELECTOR, "tr.zA")) | |
| ) | |
| except TimeoutException: | |
| warn("Inbox rows not found.") | |
| return "" | |
| rows = driver.find_elements(By.CSS_SELECTOR, "tr.zA") | |
| if not rows: | |
| warn("No emails found.") | |
| return "" | |
| log(f"Found {len(rows)} emails. Finding first non-automated email ...") | |
| # Skip emails from automated/noreply senders | |
| skip_keywords = ["noreply", "no-reply", "donotreply", "discord", "google", | |
| "automated", "notification", "mailer", "support@", "verify"] | |
| target_row = None | |
| for row in rows[:10]: # check first 10 only | |
| try: | |
| sender_el = row.find_element(By.CSS_SELECTOR, "span.yP, span[email]") | |
| sender = (sender_el.get_attribute("email") or sender_el.text).lower() | |
| if not any(skip in sender for skip in skip_keywords): | |
| log(f"Selected email from: {sender}") | |
| target_row = row | |
| break | |
| except Exception: | |
| continue | |
| # Fall back to first email if nothing passed the filter | |
| if target_row is None: | |
| warn("No non-automated email found, using first email anyway.") | |
| target_row = rows[0] | |
| driver.execute_script("arguments[0].style.outline='3px solid red'", target_row) | |
| time.sleep(0.5) | |
| safe_click(driver, target_row) | |
| try: | |
| body = WebDriverWait(driver, 10).until( | |
| EC.presence_of_element_located((By.CSS_SELECTOR, "div.a3s.aiL, div.a3s")) | |
| ) | |
| driver.execute_script("arguments[0].style.outline='3px solid green'", body) | |
| text = body.text[:300].replace("\n", " ").strip() | |
| log(f"Email body scraped: {text[:80]}...") | |
| return text | |
| except TimeoutException: | |
| warn("Could not read email body.") | |
| return "" | |
| # ───────────────────────────────────────────── | |
| # PHASE 2 — Google Sheets (Selenium) | |
| # ───────────────────────────────────────────── | |
| def phase_sheets(driver, email_text): | |
| log("--- PHASE 2: Google Sheets ---") | |
| if GOOGLE_SHEET_URL == "PASTE_YOUR_GOOGLE_SHEET_URL_HERE": | |
| warn("GOOGLE_SHEET_URL not set — skipping Sheets phase.") | |
| return | |
| # Hardcoded fallback if Gmail scraped nothing useful | |
| content = email_text.strip() if email_text and len(email_text.strip()) > 10 \ | |
| else "New lead identified — follow up required. Source: Gmail inbox triage." | |
| new_tab(driver, GOOGLE_SHEET_URL) | |
| # Wait for sheet to load — Name Box is the most reliable indicator | |
| log("Waiting for spreadsheet to load ...") | |
| name_box_el = None | |
| for sel in ["div.waffle-name-box input", ".cell-input", "#t-name-box", "input.waffle-name-box"]: | |
| try: | |
| name_box_el = WebDriverWait(driver, 40).until( | |
| EC.element_to_be_clickable((By.CSS_SELECTOR, sel)) | |
| ) | |
| log(f"Sheet loaded (Name Box found via {sel}).") | |
| break | |
| except TimeoutException: | |
| continue | |
| if name_box_el is None: | |
| err("Sheet never loaded — cannot write row.") | |
| return | |
| time.sleep(2) | |
| try: | |
| from datetime import datetime | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| # ── Navigate to A1, detect last used row, jump to next empty ── | |
| name_box_el.click() | |
| time.sleep(0.2) | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform() | |
| name_box_el.send_keys("A1") | |
| name_box_el.send_keys(Keys.RETURN) | |
| time.sleep(0.5) | |
| # Read A1 value from formula bar to check if sheet is empty | |
| a1_empty = True | |
| for fb_sel in ["#t-formula-bar-input", ".cell-input[id*='formula']", "input[id*='formula']"]: | |
| try: | |
| fb = driver.find_element(By.CSS_SELECTOR, fb_sel) | |
| a1_empty = not (fb.get_attribute("value") or "").strip() | |
| break | |
| except NoSuchElementException: | |
| continue | |
| if a1_empty: | |
| next_row = "A1" | |
| log("Sheet is empty — writing to A1.") | |
| else: | |
| # Jump to bottom of data in column A, read the row number | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys(Keys.DOWN).key_up(Keys.CONTROL).perform() | |
| time.sleep(0.4) | |
| name_box_el.click() | |
| time.sleep(0.2) | |
| addr = (name_box_el.get_attribute("value") or "").strip() | |
| if addr: | |
| row_num = int("".join(filter(str.isdigit, addr))) + 1 | |
| next_row = f"A{row_num}" | |
| else: | |
| next_row = "A2" | |
| log(f"Writing to {next_row}.") | |
| # ── Navigate to target cell ── | |
| name_box_el.click() | |
| time.sleep(0.2) | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform() | |
| name_box_el.send_keys(next_row) | |
| name_box_el.send_keys(Keys.RETURN) | |
| time.sleep(0.4) | |
| # ── Type the three columns: Timestamp | Type | Content ── | |
| log("Typing row data ...") | |
| ActionChains(driver).send_keys(timestamp).perform(); time.sleep(0.15) | |
| ActionChains(driver).send_keys(Keys.TAB).perform(); time.sleep(0.15) | |
| ActionChains(driver).send_keys("Gmail Lead").perform(); time.sleep(0.15) | |
| ActionChains(driver).send_keys(Keys.TAB).perform(); time.sleep(0.15) | |
| ActionChains(driver).send_keys(content[:200]).perform(); time.sleep(0.15) | |
| ActionChains(driver).send_keys(Keys.RETURN).perform() | |
| time.sleep(1) | |
| # ── Ctrl+S ── | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("s").key_up(Keys.CONTROL).perform() | |
| time.sleep(2) | |
| # Scroll back to the row we just wrote so user can see it | |
| name_box_el.click() | |
| time.sleep(0.2) | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform() | |
| name_box_el.send_keys(next_row) | |
| name_box_el.send_keys(Keys.RETURN) | |
| time.sleep(1) | |
| log("Row written and saved — visible in sheet.") | |
| except Exception as e: | |
| err(f"Sheets write failed: {e}") | |
| import traceback; traceback.print_exc() | |
| # ───────────────────────────────────────────── | |
| # PHASE 3 — Discord (Selenium) | |
| # ───────────────────────────────────────────── | |
| def phase_discord(driver, email_text): | |
| log("--- PHASE 3: Discord ---") | |
| if DISCORD_WEBHOOK_URL == "PASTE_YOUR_DISCORD_WEBHOOK_URL_HERE": | |
| warn("DISCORD_WEBHOOK_URL not set — skipping Discord phase.") | |
| return | |
| # Hardcoded fallback if Gmail scraped nothing useful | |
| content = email_text.strip() if email_text and len(email_text.strip()) > 10 \ | |
| else "New lead identified — follow up required. Source: Gmail inbox triage." | |
| message = f"[NEW LEAD] {content[:200]}" | |
| new_tab(driver, "https://discord.com/app") | |
| time.sleep(5) # Discord is slow to boot | |
| # Wait for the message textbox — the only reliable signal Discord is ready | |
| log("Waiting for Discord message box ...") | |
| textbox = None | |
| for sel in [ | |
| "div[role='textbox'][contenteditable='true']", | |
| "div[role='textbox']", | |
| "div[data-slate-editor='true']", | |
| "div[contenteditable='true'][spellcheck='true']", | |
| ]: | |
| try: | |
| textbox = WebDriverWait(driver, 20).until( | |
| EC.element_to_be_clickable((By.CSS_SELECTOR, sel)) | |
| ) | |
| if textbox.is_displayed(): | |
| log(f"Textbox found: {sel}") | |
| break | |
| textbox = None | |
| except TimeoutException: | |
| textbox = None | |
| if textbox is None: | |
| err("Discord message box not found.") | |
| return | |
| driver.execute_script("arguments[0].style.outline='3px solid purple'", textbox) | |
| time.sleep(0.5) | |
| # ── Type via clipboard (PowerShell) — most reliable for Discord's Slate editor ── | |
| try: | |
| safe_msg = message.replace('"', "'") | |
| ps_cmd = f'Set-Clipboard -Value "{safe_msg}"' | |
| subprocess.run(["powershell", "-command", ps_cmd], capture_output=True) | |
| time.sleep(0.3) | |
| textbox.click() | |
| time.sleep(0.3) | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("a").key_up(Keys.CONTROL).perform() | |
| time.sleep(0.1) | |
| ActionChains(driver).key_down(Keys.CONTROL).send_keys("v").key_up(Keys.CONTROL).perform() | |
| time.sleep(0.8) | |
| log("Pasted message via clipboard.") | |
| except Exception as e: | |
| warn(f"Clipboard paste failed ({e}), falling back to JS insertText ...") | |
| textbox.click() | |
| time.sleep(0.3) | |
| driver.execute_script(""" | |
| arguments[0].focus(); | |
| document.execCommand('selectAll', false, null); | |
| document.execCommand('insertText', false, arguments[1]); | |
| """, textbox, message) | |
| time.sleep(0.8) | |
| # Confirm text is in the box before sending | |
| typed = textbox.text.strip() | |
| if not typed: | |
| warn("Nothing in textbox — message may not send.") | |
| else: | |
| log(f"Text confirmed in box: {typed[:60]}...") | |
| # ── Send with Enter ── | |
| # Click the box first to make sure it has focus, THEN press Enter | |
| textbox.click() | |
| time.sleep(0.3) | |
| textbox.send_keys(Keys.RETURN) | |
| time.sleep(1) | |
| log("Discord message sent.") | |
| time.sleep(2) | |
| # ───────────────────────────────────────────── | |
| # PHASE 4 — Google Meet | |
| # ───────────────────────────────────────────── | |
| def phase_meet(driver): | |
| log("--- PHASE 4: Google Meet ---") | |
| new_tab(driver, "https://meet.google.com/new") | |
| time.sleep(5) | |
| log(f"Meeting room: {driver.current_url}") | |
| # ───────────────────────────────────────────── | |
| # MAIN | |
| # ───────────────────────────────────────────── | |
| def run(): | |
| log("=== STARTING AUTOMATION ===") | |
| chrome_exe = find_chrome() | |
| log(f"Chrome: {chrome_exe}") | |
| kill_chrome() | |
| try: | |
| user_data_dir, profile_dir = clone_profile() | |
| except Exception as e: | |
| warn(f"Profile clone failed ({e}) — using fresh profile.") | |
| user_data_dir = os.path.join(os.environ.get("TEMP", "C:\\Temp"), "chrome_fresh") | |
| profile_dir = "Default" | |
| os.makedirs(user_data_dir, exist_ok=True) | |
| launch_chrome(user_data_dir, profile_dir, chrome_exe) | |
| try: | |
| driver = attach_selenium() | |
| email_text = phase_gmail(driver) | |
| time.sleep(3) # let user see the scraped email | |
| phase_sheets(driver, email_text) | |
| time.sleep(5) # let user see the row written in the sheet | |
| phase_discord(driver, email_text) | |
| time.sleep(5) # let user see the message sent in Discord | |
| phase_meet(driver) | |
| log("=== WORKFLOW COMPLETE ===") | |
| time.sleep(10) | |
| except WebDriverException as e: | |
| err(f"WebDriver error: {e}") | |
| except Exception as e: | |
| err(f"Unexpected error: {e}") | |
| import traceback; traceback.print_exc() | |
| finally: | |
| log("Done. Browser left open.") | |
| if __name__ == "__main__": | |
| run() |