diff --git a/app.py b/app.py index a378305..f018544 100644 --- a/app.py +++ b/app.py @@ -103,6 +103,9 @@ def init_db(): con.execute("""CREATE TABLE IF NOT EXISTS accounts( mac TEXT PRIMARY KEY, email TEXT, username TEXT, password TEXT, status TEXT, session_file TEXT, created REAL)""") + con.execute("""CREATE TABLE IF NOT EXISTS reddit_accounts( + mac TEXT PRIMARY KEY, username TEXT, password TEXT, email TEXT, + status TEXT, created REAL)""") # proxy schema extensions (proxyfly integration) for col, ddl in [("clean", "INTEGER DEFAULT 0"), ("country", "TEXT"), ("egress_ip", "TEXT"), ("source", "TEXT"), @@ -748,6 +751,20 @@ def api_vnc(mac): return jsonify({"url": (f"http://10.30.20.31:8081/vnc.html?autoconnect=true" f"&reconnect=true&resize=scale&path=websockify?token={mac}")}) +@app.route("/api/device//reddit", methods=["POST"]) +def api_reddit(mac): + """Create + email-verify a Reddit profile on this device (rides device proxy).""" + import onboard_reddit as rd + threading.Thread(target=rd.run, args=(mac,), daemon=True).start() + return jsonify({"ok": True, "status": "reddit onboarding started"}) + +@app.route("/api/reddit_accounts") +def api_reddit_accounts(): + con = db() + rows = [dict(r) for r in con.execute("SELECT * FROM reddit_accounts").fetchall()] + con.close() + return jsonify(rows) + @app.route("/api/device/", methods=["DELETE"]) def api_delete_device(mac): """Remove a device row (and release its proxy).""" diff --git a/onboard_reddit.py b/onboard_reddit.py new file mode 100644 index 0000000..7ff0644 --- /dev/null +++ b/onboard_reddit.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Reddit account factory — one verified Reddit profile per fleet device. +Browser-based (Chrome on device, rides the device proxy) + email verification +via Gmail plus-addressing and the IMAP link-clicker. +uiautomator CAN see Chrome's DOM (unlike Google's auth webview). +""" +import imaplib +import json +import os +import random +import re +import sqlite3 +import subprocess +import time +import urllib.request +import urllib.parse + +BASE = "http://10.30.20.31:8080" +TOKEN = "godhead-fleet-2026" +GMAIL = "jones.henry666q@gmail.com" +APP_PASS = "hkdbecbdkncmvshg" +DB = "/opt/android-fleet/fleet.db" +COUNTER = "/opt/android-fleet/rdt_counter" + +def api(path, method="GET", payload=None, timeout=120): + data = json.dumps(payload).encode() if payload is not None else None + req = urllib.request.Request(BASE + path, data=data, method=method, + headers={"Content-Type": "application/json", + "X-Auth-Token": TOKEN}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read()) + +def shell(mac, cmd, timeout=60): + try: + return api(f"/api/device/{mac}/exec", "POST", {"cmd": cmd}, timeout).get("output", "") + except Exception as e: + return f"ERR {e}" + +def tap(mac, x, y): + api(f"/api/device/{mac}/tap", "POST", {"x": int(x), "y": int(y)}, 30) + +def text(mac, s): + api(f"/api/device/{mac}/text", "POST", {"text": s}, 40) + +def next_counter(): + n = int(open(COUNTER).read()) if os.path.exists(COUNTER) else 0 + n += 1 + open(COUNTER, "w").write(str(n)) + return n + +def dump(mac): + shell(mac, "uiautomator dump /sdcard/ui.xml >/dev/null 2>&1; cat /sdcard/ui.xml", 30) + try: + req = urllib.request.Request(BASE + f"/api/device/{mac}/exec", + data=json.dumps({"cmd": "cat /sdcard/ui.xml"}).encode(), + headers={"Content-Type": "application/json", + "X-Auth-Token": TOKEN}, method="POST") + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()).get("output", "") + except Exception: + return "" + +def find_in_dump(xml, *needles, kind="text"): + """Find node containing any needle in text/content-desc/hint. Return center.""" + pat = re.compile(r']*?(?:text|content-desc)="([^"]*)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"') + hits = [] + for m in pat.finditer(xml): + label = m.group(1) + if any(n.lower() in label.lower() for n in needles): + x1, y1, x2, y2 = (int(m.group(i)) for i in (2, 3, 4, 5)) + hits.append(((x1 + x2) // 2, (y1 + y2) // 2, label)) + return hits + +def wait_dump(mac, tries=6): + for _ in range(tries): + xml = dump(mac) + if xml.strip().startswith("<"): + return xml + time.sleep(3) + return "" + +REDDIT_URL = "https://old.reddit.com/register" # server-rendered, works on old engines + +def create_reddit(mac): + n = next_counter() + username = f"jones_{random.choice(['pulse','drift','echo','flare','ember','wisp','forge','zephyr'])}{n}" + password = "J0nes123!xYz" + email = f"rdt{n}+{GMAIL}" + log = [] + + # open register page in Chrome (device proxy applies) + shell(mac, f"am start -a android.intent.action.VIEW -d '{REDDIT_URL}' com.android.chrome") + time.sleep(12) + xml = wait_dump(mac) + # Chrome first-run welcome screen? accept & continue, then reload the URL + if "accept" in xml.lower(): + hits = find_in_dump(xml, "Accept & continue", "Accept and continue") + if hits: + tap(mac, *hits[0][:2]); time.sleep(5) + shell(mac, f"am start -a android.intent.action.VIEW -d '{REDDIT_URL}' com.android.chrome") + time.sleep(12) + xml = wait_dump(mac) + if not xml: + return {"status": "chrome_dump_failed", "log": log} + + # username field + hits = find_in_dump(xml, "username") + if not hits: + hits = find_in_dump(xml, "choose a username") + if not hits: + return {"status": "username_field_missing", "log": log} + tap(mac, *hits[0][:2]); time.sleep(2); text(mac, username); time.sleep(1) + log.append(f"username: {username}") + + # password field (often next after username; re-dump) + shell(mac, "input keyevent 4"); time.sleep(1) + xml = wait_dump(mac) + hits = find_in_dump(xml, "password") + if not hits: + return {"status": "password_field_missing", "log": log} + tap(mac, *hits[0][:2]); time.sleep(2); text(mac, password); time.sleep(1) + shell(mac, "input keyevent 4"); time.sleep(1) + + # email field + xml = wait_dump(mac) + hits = find_in_dump(xml, "email") + if not hits: + return {"status": "email_field_missing", "log": log} + tap(mac, *hits[0][:2]); time.sleep(2); text(mac, email); time.sleep(1) + shell(mac, "input keyevent 4"); time.sleep(1) + + # submit + xml = wait_dump(mac) + for btn in ("Continue", "Sign up", "Create account"): + hits = find_in_dump(xml, btn) + if hits: + tap(mac, *hits[0][:2]); time.sleep(10) + break + else: + return {"status": "submit_missing", "log": log} + + # save account row + con = sqlite3.connect(DB) + con.execute("""CREATE TABLE IF NOT EXISTS reddit_accounts( + mac TEXT PRIMARY KEY, username TEXT, password TEXT, email TEXT, + status TEXT, created REAL)""") + con.execute("INSERT OR REPLACE INTO reddit_accounts(mac, username, password, email, status, created) " + "VALUES(?,?,?,?,'created',?)", + (mac, username, password, email, time.time())) + con.commit() + con.close() + return {"status": "created", "username": username, "email": email, "log": log} + +def verify_email(email, timeout=300): + """Poll Gmail IMAP for the Reddit verification email, click the link.""" + end = time.time() + timeout + while time.time() < end: + try: + M = imaplib.IMAP4_SSL("imap.gmail.com") + M.login(GMAIL, APP_PASS) + M.select("INBOX") + typ, data = M.search(None, f'(FROM "reddit")') + for num in data[0].split(): + typ, msg = M.fetch(num, "(RFC822)") + body = msg[0][1].decode("utf-8", "ignore") + m = re.search(r"https://[^\s\"<>]+verif[^\s\"<>]*", body) + if m: + link = m.group(0).rstrip(".,;") + urllib.request.urlopen(link, timeout=30) + M.logout() + return True, link[:80] + M.logout() + except Exception: + pass + time.sleep(15) + return False, "" + +def run(mac): + r = create_reddit(mac) + if r["status"] == "created": + ok, link = verify_email(r["email"]) + r["verified"] = ok + if ok: + r["status"] = "verified" + con = sqlite3.connect(DB) + con.execute("UPDATE reddit_accounts SET status='verified' WHERE mac=?", (mac,)) + con.commit(); con.close() + print(json.dumps(r, indent=1)) + +if __name__ == "__main__": + import sys + run(sys.argv[1])