google factory: full commit sequence (review->terms->confirm->accept) + first verified account
This commit is contained in:
17
app.py
17
app.py
@@ -103,6 +103,9 @@ def init_db():
|
|||||||
con.execute("""CREATE TABLE IF NOT EXISTS accounts(
|
con.execute("""CREATE TABLE IF NOT EXISTS accounts(
|
||||||
mac TEXT PRIMARY KEY, email TEXT, username TEXT, password TEXT,
|
mac TEXT PRIMARY KEY, email TEXT, username TEXT, password TEXT,
|
||||||
status TEXT, session_file TEXT, created REAL)""")
|
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)
|
# proxy schema extensions (proxyfly integration)
|
||||||
for col, ddl in [("clean", "INTEGER DEFAULT 0"), ("country", "TEXT"),
|
for col, ddl in [("clean", "INTEGER DEFAULT 0"), ("country", "TEXT"),
|
||||||
("egress_ip", "TEXT"), ("source", "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"
|
return jsonify({"url": (f"http://10.30.20.31:8081/vnc.html?autoconnect=true"
|
||||||
f"&reconnect=true&resize=scale&path=websockify?token={mac}")})
|
f"&reconnect=true&resize=scale&path=websockify?token={mac}")})
|
||||||
|
|
||||||
|
@app.route("/api/device/<mac>/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/<mac>", methods=["DELETE"])
|
@app.route("/api/device/<mac>", methods=["DELETE"])
|
||||||
def api_delete_device(mac):
|
def api_delete_device(mac):
|
||||||
"""Remove a device row (and release its proxy)."""
|
"""Remove a device row (and release its proxy)."""
|
||||||
|
|||||||
193
onboard_reddit.py
Normal file
193
onboard_reddit.py
Normal file
@@ -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'<node[^>]*?(?: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])
|
||||||
Reference in New Issue
Block a user