#!/usr/bin/env python3 """ Macro: Reddit App Setup Purpose: Create a Reddit API application for the Latent Signal ingestor Justification: User's own account, no API alternative for app creation """ import asyncio import os import random from datetime import datetime from pathlib import Path from patchright.async_api import async_playwright # Paths MACRO_DIR = Path(__file__).parent SCREENSHOTS_DIR = MACRO_DIR / "screenshots" ENV_FILE = MACRO_DIR.parent.parent / ".env" SCREENSHOTS_DIR.mkdir(exist_ok=True) # App configuration APP_NAME = "LatentSignalBot" APP_DESCRIPTION = "Latent signal detection for adverse event monitoring" APP_REDIRECT_URI = os.getenv("REDDIT_REDIRECT_URI", "http://localhost:8080") async def human_delay(min_ms=500, max_ms=1500): """Add human-like delay between actions.""" await asyncio.sleep(random.randint(min_ms, max_ms) / 1000) async def screenshot(page, name: str): """Capture a timestamped screenshot.""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") path = SCREENSHOTS_DIR / f"{timestamp}_{name}.png" await page.screenshot(path=str(path)) print(f"[Screenshot] {path.name}") return path async def human_type(page, selector: str, text: str): """Type text with human-like delays.""" await page.click(selector) await human_delay(100, 300) for char in text: await page.keyboard.type(char) await asyncio.sleep(random.uniform(0.03, 0.10)) await human_delay(200, 400) async def wait_for_login(page): """Wait for user to complete login if needed.""" print("\n[*] Checking if login is required...") await screenshot(page, "01_initial_load") # Check if we're on a login page or need to log in login_button = await page.query_selector('a[href*="login"], button:has-text("Log In")') if login_button: print("\n" + "=" * 50) print("LOGIN REQUIRED") print("=" * 50) print("Please log in to Reddit in the browser window.") print("") print("TIP: Use Reddit username/password login, NOT Google OAuth") print(" (Google detects automated browsers)") print("") print("The macro will continue automatically after login.") print("=" * 50 + "\n") # Wait for the apps page to load (indicates successful login) # Poll instead of wait_for_url to be more resilient for _ in range(600): # 10 minutes max await asyncio.sleep(1) url = page.url if "/prefs/apps" in url: print("[OK] Login detected!") await human_delay(1000, 2000) return # Also check if we're now logged in on the current page login_btn = await page.query_selector('a[href*="login"], button:has-text("Log In")') if not login_btn: # Logged in, navigate to apps page print("[OK] Login detected, navigating to apps page...") await page.goto("https://www.reddit.com/prefs/apps") await page.wait_for_load_state("networkidle") await human_delay(1000, 2000) return print("[!] Login timeout - please try again") raise Exception("Login timeout") async def create_app(page): """Create a new Reddit application.""" print("\n[*] Looking for 'create app' button...") await screenshot(page, "02_apps_page") # Look for the "create another app" or "are you a developer? create an app" link create_button = await page.query_selector('button:has-text("create another app"), a:has-text("create another app"), button:has-text("create app"), a:has-text("create an app")') if not create_button: # Try alternative selectors for Reddit's UI create_button = await page.query_selector('.create-app-button, [href*="create"], button[type="submit"]') if not create_button: await screenshot(page, "02_no_create_button") print("[!] Could not find 'create app' button. Please check the screenshot.") print("[*] Attempting to scroll and find it...") # Scroll down to find it await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") await human_delay(500, 1000) await screenshot(page, "02b_scrolled") create_button = await page.query_selector('button:has-text("create"), a:has-text("create")') if create_button: print("[*] Found create button, clicking...") await human_delay(300, 600) await create_button.click() await human_delay(1000, 1500) await screenshot(page, "03_create_form") else: print("[!] Still can't find create button - manual intervention needed") input("Press Enter after clicking 'create another app' manually...") await screenshot(page, "03_manual_create_form") async def fill_app_form(page): """Fill in the app creation form.""" print("\n[*] Filling app form...") # Wait for form to be visible await human_delay(500, 1000) # Name field name_field = await page.query_selector('input[name="name"], #app-name, input[placeholder*="name"]') if name_field: await human_type(page, 'input[name="name"]', APP_NAME) print(f" Name: {APP_NAME}") await human_delay(300, 600) # Select "script" type script_radio = await page.query_selector('input[value="script"], input[name="app_type"][value="script"]') if script_radio: await script_radio.click() print(" Type: script") else: # Try clicking label script_label = await page.query_selector('label:has-text("script")') if script_label: await script_label.click() print(" Type: script (via label)") await human_delay(300, 600) # Description field desc_field = await page.query_selector('textarea[name="description"], #app-description') if desc_field: await desc_field.click() await page.keyboard.type(APP_DESCRIPTION) print(f" Description: {APP_DESCRIPTION}") await human_delay(300, 600) # Redirect URI field redirect_field = await page.query_selector('input[name="redirect_uri"], #app-redirect-uri') if redirect_field: await human_type(page, 'input[name="redirect_uri"]', APP_REDIRECT_URI) print(f" Redirect URI: {APP_REDIRECT_URI}") await screenshot(page, "04_form_filled") # Submit the form print("\n[*] Submitting form...") await human_delay(500, 1000) submit_button = await page.query_selector('button[type="submit"]:has-text("create app"), input[type="submit"]') if submit_button: await submit_button.click() else: # Try generic submit await page.keyboard.press("Enter") await human_delay(2000, 3000) await screenshot(page, "05_after_submit") async def extract_credentials(page): """Extract the client_id and client_secret from the page.""" print("\n[*] Extracting credentials...") await screenshot(page, "06_credentials_page") # Get page content to find credentials content = await page.content() # The client_id appears under the app name (it's a short alphanumeric string) # The client_secret appears next to "secret" client_id = None client_secret = None # Try to find the app we just created app_info = await page.query_selector(f'div:has-text("{APP_NAME}")') if app_info: # Get all text in the app info section app_text = await app_info.inner_text() print(f"[DEBUG] App section text:\n{app_text[:500]}...") # Look for the client_id (appears right after "personal use script") # and secret lines = app_text.split('\n') for i, line in enumerate(lines): line = line.strip() if 'personal use script' in line.lower(): # Next non-empty line should be client_id for next_line in lines[i+1:]: next_line = next_line.strip() if next_line and len(next_line) > 10 and len(next_line) < 30: client_id = next_line break if 'secret' in line.lower(): # The secret might be on this line or next parts = line.split() for part in parts: if len(part) > 20: client_secret = part break # Alternative: use JavaScript to extract if not client_id or not client_secret: print("[*] Trying JavaScript extraction...") # Reddit shows client_id in a specific element try: client_id = await page.evaluate(""" () => { const apps = document.querySelectorAll('.app-details, .developed-app'); for (const app of apps) { const text = app.innerText; if (text.includes('LatentSignalBot')) { // Find the ID (short string after "personal use script") const match = text.match(/personal use script[\\s\\n]+([a-zA-Z0-9_-]+)/i); if (match) return match[1]; } } return null; } """) client_secret = await page.evaluate(""" () => { const apps = document.querySelectorAll('.app-details, .developed-app'); for (const app of apps) { const text = app.innerText; if (text.includes('LatentSignalBot')) { // Find secret const match = text.match(/secret[:\\s]+([a-zA-Z0-9_-]+)/i); if (match) return match[1]; } } return null; } """) except Exception as e: print(f"[!] JavaScript extraction failed: {e}") return client_id, client_secret async def save_credentials(client_id: str, client_secret: str): """Save credentials to .env file.""" print(f"\n[*] Saving credentials to {ENV_FILE}") env_content = f"""REDDIT_CLIENT_ID={client_id} REDDIT_CLIENT_SECRET={client_secret} REDDIT_USER_AGENT=LatentSignalBot/0.1 """ # Read existing .env if it exists existing = {} if ENV_FILE.exists(): with open(ENV_FILE) as f: for line in f: if '=' in line and not line.startswith('#'): key, value = line.strip().split('=', 1) existing[key] = value # Update with new values existing['REDDIT_CLIENT_ID'] = client_id existing['REDDIT_CLIENT_SECRET'] = client_secret existing['REDDIT_USER_AGENT'] = 'LatentSignalBot/0.1' # Write back with open(ENV_FILE, 'w') as f: for key, value in existing.items(): f.write(f"{key}={value}\n") print(f"[OK] Credentials saved!") print(f" REDDIT_CLIENT_ID={client_id}") print(f" REDDIT_CLIENT_SECRET={'*' * len(client_secret)}") async def main(): print("=" * 50) print("Reddit App Setup Macro") print("=" * 50) # Use copied Chrome profile to avoid conflicts with running Chrome TEMP_PROFILE = Path("/tmp/chrome-jordanbot2000") async with async_playwright() as p: # Launch with copied profile context = await p.chromium.launch_persistent_context( user_data_dir=str(TEMP_PROFILE), channel="chrome", # Use installed Chrome headless=False, viewport={"width": 1280, "height": 900}, locale="en-US", timezone_id="America/New_York", ) page = context.pages[0] if context.pages else await context.new_page() try: # Step 1: Navigate to Reddit apps page print("\n[*] Navigating to Reddit apps page...") await page.goto("https://www.reddit.com/prefs/apps") await page.wait_for_load_state("networkidle") await human_delay(1000, 2000) # Step 2: Handle login if needed await wait_for_login(page) # Step 3: Create new app await create_app(page) # Step 4: Fill form await fill_app_form(page) # Step 5: Extract credentials client_id, client_secret = await extract_credentials(page) if client_id and client_secret: # Step 6: Save to .env await save_credentials(client_id, client_secret) await screenshot(page, "99_success") print("\n" + "=" * 50) print("SUCCESS!") print("=" * 50) print(f"Credentials saved to: {ENV_FILE}") print("You can now run the Reddit ingestor:") print(" python main.py") else: await screenshot(page, "99_manual_needed") print("\n" + "=" * 50) print("MANUAL EXTRACTION NEEDED") print("=" * 50) print("Could not automatically extract credentials.") print("Please copy them manually from the browser:") print(" - Client ID: shown under 'personal use script'") print(" - Secret: shown next to 'secret'") print(f"\nThen add to: {ENV_FILE}") # Keep browser open for manual copying input("\nPress Enter when done copying credentials...") except Exception as e: await screenshot(page, "error") print(f"\n[ERROR] {e}") raise finally: print("\n[*] Closing browser...") await context.close() if __name__ == "__main__": asyncio.run(main())