Files
pogo-mover/server.py
T

262 lines
8.8 KiB
Python
Raw Normal View History

2026-05-25 15:50:52 -07:00
import json
import math
import mimetypes
import os
import subprocess
import sys
import threading
import time
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
PORT = 7777
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
COORDS_FILE = os.path.join(BASE_DIR, ".last_coords.json")
DAEMON_SCRIPT = os.path.join(BASE_DIR, "location_daemon.py")
DEFAULT_COORDS = {"lat": 33.4484, "lng": -112.0740}
def load_coords():
try:
with open(COORDS_FILE, "r") as f:
data = json.load(f)
if "lat" in data and "lng" in data:
print(f"Resuming from last position: {data['lat']:.5f}, {data['lng']:.5f}")
return data
except Exception:
pass
return dict(DEFAULT_COORDS)
def save_coords(lat, lng):
try:
with open(COORDS_FILE, "w") as f:
json.dump({"lat": lat, "lng": lng}, f)
except Exception:
pass
current_coords = load_coords()
last_result = {"method": None, "ok": False, "error": "not tried yet"}
_daemon_proc = None
_daemon_lock = threading.Lock()
_cached_rsd_host = None
_cached_rsd_port = None
_rsd_cache_time = 0
RSD_CACHE_TTL = 30
def get_rsd():
global _cached_rsd_host, _cached_rsd_port, _rsd_cache_time
now = time.time()
if _cached_rsd_host and (now - _rsd_cache_time) < RSD_CACHE_TTL:
return _cached_rsd_host, _cached_rsd_port
try:
with urllib.request.urlopen("http://127.0.0.1:49151/", timeout=3) as r:
tunnels = json.loads(r.read())
if isinstance(tunnels, dict) and tunnels:
entry = list(tunnels.values())[0]
if isinstance(entry, list) and entry:
t = entry[0]
host = t.get("tunnel-address")
port = t.get("tunnel-port")
if host and port:
_cached_rsd_host = str(host)
_cached_rsd_port = int(port)
_rsd_cache_time = now
return _cached_rsd_host, _cached_rsd_port
except Exception:
pass
return None, None
def start_daemon():
global _daemon_proc, _cached_rsd_host, _cached_rsd_port
host, port = get_rsd()
if not host:
print(" [!] No RSD tunnel found — daemon not started", flush=True)
return False
try:
proc = subprocess.Popen(
[sys.executable, DAEMON_SCRIPT, host, str(port)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
ready_line = proc.stdout.readline().strip()
if ready_line == "ready":
_daemon_proc = proc
print(f" [✓] Location daemon connected via RSD {host}:{port}", flush=True)
return True
else:
print(f" [!] Daemon startup failed: {ready_line}", flush=True)
proc.kill()
return False
except Exception as e:
print(f" [!] Could not start daemon: {e}", flush=True)
return False
def send_via_daemon(lat, lng):
global _daemon_proc, last_result
with _daemon_lock:
if _daemon_proc is None or _daemon_proc.poll() is not None:
print(" [~] Daemon not running — restarting...", flush=True)
start_daemon()
if _daemon_proc is None:
return False
try:
_daemon_proc.stdin.write(f"{lat},{lng}\n")
_daemon_proc.stdin.flush()
response = _daemon_proc.stdout.readline().strip()
if response == "ok":
last_result = {"method": "DVT daemon (persistent)", "ok": True, "error": None}
return True
else:
print(f" [!] Daemon response: {response}", flush=True)
return False
except Exception as e:
print(f" [!] Daemon write failed: {e}", flush=True)
_daemon_proc = None
return False
def send_location_cli(lat, lng):
global last_result, _cached_rsd_host, _cached_rsd_port
host, port = get_rsd()
if host and port:
try:
r = subprocess.run(
["pymobiledevice3", "developer", "dvt", "simulate-location",
"set", "--rsd", host, str(port), "--", str(lat), str(lng)],
capture_output=True, text=True, timeout=5
)
if r.returncode == 0:
last_result = {"method": "dvt --rsd (cli)", "ok": True, "error": None}
return True
except (subprocess.TimeoutExpired, Exception):
_cached_rsd_host = None
last_result = {"method": None, "ok": False, "error": "all methods failed"}
return False
import random
def add_gps_jitter(lat, lng):
meter_lat = 1.0 / 111320.0
meter_lng = 1.0 / (111320.0 * max(abs(math.cos(math.radians(lat))), 0.001))
jitter_m = 2.0
return (
lat + random.uniform(-jitter_m, jitter_m) * meter_lat,
lng + random.uniform(-jitter_m, jitter_m) * meter_lng
)
def set_location(lat, lng):
jlat, jlng = add_gps_jitter(lat, lng)
if not send_via_daemon(jlat, jlng):
send_location_cli(jlat, jlng)
_target_lat = current_coords.get("lat")
_target_lng = current_coords.get("lng")
_target_updated = time.time()
_target_lock = threading.Lock()
_last_sent_lat = None
_last_sent_lng = None
def location_loop():
global _last_sent_lat, _last_sent_lng
while True:
with _target_lock:
lat = _target_lat
lng = _target_lng
if lat is None or lng is None:
time.sleep(0.1)
continue
if lat != _last_sent_lat or lng != _last_sent_lng:
set_location(lat, lng)
_last_sent_lat = lat
_last_sent_lng = lng
else:
time.sleep(0.1)
def set_target(lat, lng):
global _target_lat, _target_lng, _target_updated
with _target_lock:
_target_lat = lat
_target_lng = lng
_target_updated = time.time()
class Handler(BaseHTTPRequestHandler):
def handle_error(self, request, client_address):
import sys as _sys
exc = _sys.exc_info()[1]
if isinstance(exc, (BrokenPipeError, ConnectionResetError)):
return
super().handle_error(request, client_address)
def log_message(self, format, *args):
pass
def send_json(self, data, status=200):
body = json.dumps(data, indent=2).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
self.end_headers()
def do_GET(self):
path = urlparse(self.path).path
if path == "/coords":
return self.send_json(current_coords)
if path == "/status":
return self.send_json({"ok": True, "spoof": last_result})
if path in ("/", "/index.html"):
path = "/index.html"
file_path = os.path.join(BASE_DIR, path.lstrip("/"))
if os.path.isfile(file_path):
mime, _ = mimetypes.guess_type(file_path)
with open(file_path, "rb") as f:
body = f.read()
self.send_response(200)
self.send_header("Content-Type", mime or "application/octet-stream")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
global current_coords
if urlparse(self.path).path == "/move":
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
try:
data = json.loads(body)
lat = float(data["lat"])
lng = float(data["lng"])
current_coords = {"lat": lat, "lng": lng}
save_coords(lat, lng)
set_target(lat, lng)
self.send_json({"ok": True, "lat": lat, "lng": lng})
except Exception as e:
self.send_json({"error": str(e)}, 400)
else:
self.send_response(404)
self.end_headers()
if __name__ == "__main__":
print(f"PoGo Mover running at http://localhost:{PORT}")
print(" Starting location daemon...", flush=True)
start_daemon()
threading.Thread(target=location_loop, daemon=True).start()
server = HTTPServer(("localhost", PORT), Handler)
server.serve_forever()