android-fleet-godhead v1: fleet control node + ProxyFly engine + dashboard

This commit is contained in:
drjones
2026-08-13 16:41:45 -07:00
commit 01bd139af7
10 changed files with 2000 additions and 0 deletions

313
onboard.py Normal file
View File

@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""
Android fleet onboarding: turn a bare Android-x86 clone into a fully-set-up
phone with a fresh Instagram account.
Steps: basics -> Instagram install -> account creation (device proxy) ->
IMAP email verification -> app login (best-effort UI) -> DB record.
"""
import imaplib
import json
import os
import re
import sqlite3
import subprocess
import sys
import time
DB_PATH = "/opt/android-fleet/fleet.db"
IG_APK = "/opt/android-fleet/ig_instagram.apk"
SESSIONS_DIR = "/opt/android-fleet/sessions"
GMAIL = "jones.henry666q@gmail.com"
APP_PASS = "hkdbecbdkncmvshg"
PASSWORD = "aaaa1111"
os.makedirs(SESSIONS_DIR, exist_ok=True)
def log(msg):
print(f"[onboard] {msg}", flush=True)
def dbcon():
con = sqlite3.connect(DB_PATH, timeout=15)
con.row_factory = sqlite3.Row
return con
def get_device(mac):
con = dbcon()
row = con.execute("SELECT * FROM devices WHERE mac=?", (mac,)).fetchone()
con.close()
return dict(row) if row else None
def adb_shell(serial, cmd, timeout=45):
r = subprocess.run(["adb", "-s", serial, "shell", cmd],
capture_output=True, text=True, timeout=timeout)
return r.stdout.strip()
def adb_cmd(serial, args, timeout=60):
return subprocess.run(["adb", "-s", serial] + args,
capture_output=True, text=True, timeout=timeout)
def next_counter():
con = dbcon()
row = con.execute("SELECT COUNT(*) AS n FROM accounts").fetchone()
con.close()
return row["n"] + 1
def setup_basics(serial):
log("basics: timezone, screen, animations")
adb_shell(serial, "settings put global stay_on_while_plugged_in 3")
adb_shell(serial, "settings put system screen_off_timeout 2147483647")
adb_shell(serial, "settings put global window_animation_scale 0")
adb_shell(serial, "settings put global transition_animation_scale 0")
adb_shell(serial, "settings put global animator_duration_scale 0")
adb_shell(serial, "setprop persist.sys.timezone America/Los_Angeles")
adb_shell(serial, "settings put global airplane_mode_on 0")
log("basics done")
def install_instagram(serial):
"""Try the APK; arm64-only builds won't install on x86 — non-fatal."""
log("attempting Instagram APK install (arm64 builds fail on x86 — that's fine)...")
try:
r = adb_cmd(serial, ["install", "-r", IG_APK], timeout=300)
out = (r.stdout or "") + (r.stderr or "")
log(f"install result: {out.strip()[:120]}")
if "Success" in out:
return "installed"
if "NO_MATCHING_ABIS" in out:
log("APK is arm64-only — skipping app install, using API-level account")
return "skipped"
return "failed"
except Exception as e:
log(f"install error: {e}")
return "failed"
def fetch_verification(timeout=240):
"""Poll Gmail IMAP for IG confirm: 6-digit code OR confirmation 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, '(FROM "Instagram")')
nums = data[0].split()[-6:]
for num in reversed(nums):
typ, msg = M.fetch(num, "(BODY.PEEK[])")
raw = msg[0][1]
body = raw.decode(errors="replace")
m = re.search(r"\b(\d{6})\b", body)
if m:
M.logout()
return {"code": m.group(1)}
links = re.findall(r'https?://[^\s"<>]+(?:confirm|verify|challenge)[^\s"<>]*', body)
if links:
M.logout()
return {"link": links[0]}
M.logout()
except Exception as e:
log(f"imap poll: {e}")
time.sleep(8)
return None
def create_instagram_account(serial, proxy_url, counter):
log(f"creating IG account #{counter} via device proxy")
from instagrapi import Client
cl = Client()
if proxy_url:
cl.set_proxy(proxy_url)
email = f"igbot{counter}+{GMAIL}"
username = f"fleet.jones{counter}"
status = "created"
try:
cl.signup(username=username, password=PASSWORD, email=email, full_name="Android User")
log("signup ok, no verification needed")
except Exception as e:
msg = str(e).lower()
log(f"signup challenge: {str(e)[:100]}")
if "checkpoint" in msg or "challenge" in msg:
return {"status": "checkpoint", "email": email, "username": username}
if "sent" in msg or "verification" in msg or "confirm" in msg:
ver = fetch_verification()
if not ver:
return {"status": "no_code", "email": email, "username": username}
if "code" in ver:
try:
cl.signup(username=username, password=PASSWORD, email=email,
full_name="Android User", verification_code=ver["code"])
log("signup ok after email code")
except Exception as e2:
log(f"verify signup failed: {str(e2)[:100]}")
return {"status": f"verify_failed: {str(e2)[:60]}",
"email": email, "username": username}
elif "link" in ver:
log(f"clicking confirm link through device proxy: {ver['link'][:60]}...")
try:
import requests as _rq
proxies = None
if proxy_url:
proxies = {"http": proxy_url, "https": proxy_url}
r = _rq.get(ver["link"], proxies=proxies, timeout=20,
headers={"User-Agent": "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36"})
log(f"link click: HTTP {r.status_code}")
time.sleep(5)
try:
cl.login(username, PASSWORD)
log("login after link confirm OK")
except Exception as le:
log(f"login after link: {str(le)[:80]}")
status = "created_unverified"
except Exception as ce:
log(f"link click failed: {ce}")
status = "created_unverified"
else:
return {"status": f"signup_failed: {str(e)[:60]}", "email": email, "username": username}
sess = os.path.join(SESSIONS_DIR, f"{username}.json")
cl.dump_settings(sess)
return {"status": status, "email": email, "username": username,
"session_file": sess, "password": PASSWORD}
def login_app(serial, username):
"""Best-effort: launch IG and log in via UI taps (uiautomator-based)."""
log("launching IG app for login")
adb_shell(serial, "am start -n com.instagram.android/.activity.MainTabActivity", 15)
time.sleep(8)
def find_node(text):
dump = adb_shell(serial, "uiautomator dump /sdcard/ui.xml && cat /sdcard/ui.xml", 20)
m = re.search(r'text="[^"]*' + re.escape(text) + r'[^"]*"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', dump)
if m:
x = (int(m.group(1)) + int(m.group(3))) // 2
y = (int(m.group(2)) + int(m.group(4))) // 2
return x, y
return None
try:
p = find_node("Log in")
if p:
adb_shell(serial, f"input tap {p[0]} {p[1]}", 10)
time.sleep(4)
p = find_node("Username")
if p:
adb_shell(serial, f"input tap {p[0]} {p[1]}", 10)
adb_shell(serial, f"input text {username}", 10)
time.sleep(1)
p = find_node("Password")
if p:
adb_shell(serial, f"input tap {p[0]} {p[1]}", 10)
adb_shell(serial, f"input text {PASSWORD}", 10)
time.sleep(1)
p = find_node("Log In") or find_node("Log in")
if p:
adb_shell(serial, f"input tap {p[0]} {p[1]}", 10)
log("login tapped")
return True
except Exception as e:
log(f"app login UI best-effort failed: {e}")
return False
def login_existing(mac, username, password):
"""Login an existing IG account through the device's proxy. Session saved."""
dev = get_device(mac)
if not dev or not dev["ip"]:
return
con = dbcon()
pr = con.execute("SELECT * FROM proxies WHERE assigned_mac=?", (mac,)).fetchone()
con.close()
from instagrapi import Client
cl = Client()
if pr:
cred = f"{pr['user']}:{pr['pass']}@" if pr["user"] else ""
cl.set_proxy(f"socks5://{cred}{pr['host']}:{pr['port']}")
try:
cl.login(username, password)
sess = os.path.join(SESSIONS_DIR, f"{username}.json")
cl.dump_settings(sess)
con = dbcon()
con.execute("""INSERT OR REPLACE INTO accounts(mac, email, username, password, status, session_file, created)
VALUES(?,?,?,?,?,?,?)""",
(mac, None, username, password, "logged_in", sess, time.time()))
con.commit()
con.close()
log(f"{mac}: login OK for {username} — session saved")
except Exception as e:
log(f"{mac}: login failed for {username}: {str(e)[:120]}")
con = dbcon()
con.execute("""INSERT OR REPLACE INTO accounts(mac, email, username, password, status, session_file, created)
VALUES(?,?,?,?,?,?,?)""",
(mac, None, username, password, f"login_failed: {str(e)[:60]}", None, time.time()))
con.commit()
con.close()
def onboard(mac):
dev = get_device(mac)
if not dev or not dev["ip"]:
log(f"device {mac} offline — aborting")
return
serial = f"{dev['ip']}:5555"
log(f"onboarding {mac} @ {serial}")
try:
adb_cmd(serial, ["connect", serial], timeout=15)
except Exception:
pass
counter = next_counter()
con = dbcon()
con.execute("INSERT OR REPLACE INTO accounts(mac, status, created) VALUES(?,?,?)",
(mac, "onboarding", time.time()))
con.commit()
con.close()
setup_basics(serial)
try:
adb_cmd(serial, ["root"], timeout=15)
time.sleep(2)
adb_cmd(serial, ["connect", serial], timeout=15)
except Exception:
pass
apk_state = install_instagram(serial)
# proxy for this device
con = dbcon()
pr = con.execute("SELECT * FROM proxies WHERE assigned_mac=?", (mac,)).fetchone()
con.close()
proxy_url = None
if pr:
cred = f"{pr['user']}:{pr['pass']}@" if pr["user"] else ""
proxy_url = f"socks5://{cred}{pr['host']}:{pr['port']}"
result = create_instagram_account(serial, proxy_url, counter)
result["apk"] = apk_state
if result.get("session_file"):
login_app(serial, result["username"])
_set_status(mac, result["status"], extra=result)
def _set_status(mac, status, extra=None):
con = dbcon()
if extra:
con.execute("""UPDATE accounts SET status=?, email=?, username=?, password=?,
session_file=?, created=? WHERE mac=?""",
(status, extra.get("email"), extra.get("username"),
extra.get("password", PASSWORD), extra.get("session_file"),
time.time(), mac))
else:
con.execute("UPDATE accounts SET status=? WHERE mac=?", (status, mac))
con.commit()
con.close()
log(f"account status: {status}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("usage: onboard.py <mac>")
sys.exit(1)
onboard(sys.argv[1])