android-fleet-godhead v1: fleet control node + ProxyFly engine + dashboard
This commit is contained in:
313
onboard.py
Normal file
313
onboard.py
Normal 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])
|
||||
Reference in New Issue
Block a user