lots of changes

This commit is contained in:
2026-03-21 07:32:43 -04:00
parent 21933cdef9
commit 47aeebd86f
7 changed files with 1463 additions and 107 deletions

Binary file not shown.

Binary file not shown.

27
database.py Normal file
View File

@@ -0,0 +1,27 @@
from sqlalchemy import create_engine
from dqlalchemy.orm import sessionmaker, declaritive_base
DATABASE_URL = "sqlite:///./locations.db"
engine = create_engine(
DATABASE_URL, connect_engine("check_same_thread": false}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency for getting database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
def create_all_tables():
"""Creates all tables defined with Base in the database."""
# Note: import models before calling create_all_tables()
Base.metadata_create_all(bind=engine)
print("Database and tables created.")

58
db_models.py Normal file
View File

@@ -0,0 +1,58 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey
from sqlalchemy.orm import relationship
from .database import Base
class Location(Base):
"""
SQLAlchemy model for the 'locations' table.
"""
__tablename__ = "locations"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=False, index=True)
address = Column(String, unique=False, index=True)
latitude = Column(Float, unique=False, index=True)
longitude = Column(Float, unique=False, index=True)
is_favorite = Column(Boolean, unique=False, index=True)
def __repr__(self):
return f"<Location(name='{self.name}', address='{self.address}', latitude='{self.latitude}', longitude='{self.longitude}', is_favorite='{self.is_favorite}')>"
class Route(Base):
"""
SQLAlchemy model for the 'routes' table.
"""
__tablename__ = "routes"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
# Start and Endpoints (One-to-Many relationship)
start_location_id = Column(Integer, ForeignKey('locations.id'))
end_location_id = Column(Integer, ForeignKey('locations.id'))
start_location = relationship("Location", foreign_keys=[start_id])
end_location = relationship("Location", foreign_keys=[end_id])
# Relationship to get waypoints ordered
waypoints = relationship("Waypoint", order_by="Waypoint.order", back_populates="route")
def __repr__(self):
return f"<Route(name='{self.name}', start='{self.start_location.name}', end='{self.end_location.name}')>"
# Association Table for Many-to-Many relationsjip (Routes <-> Waypoints)
class Waypoint(Base):
"""
SQLAlchemy model for the 'waypoints' table.
"""
__tablename__ = 'waypoints'
id = Column(Integer, primary_key=True)
route_id = Column(Integer, ForeignKey('routes.id'))
location_id = Column(Integer, ForeignKey('locations.id'))
order = Column(Integer, nullable=False)
route = relationship("Route", back_populates="waypoints")
location = relationship("Location")

88
icloud.py Normal file
View File

@@ -0,0 +1,88 @@
import asyncio
import json
import os
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloud2FARequiredException
class FindMyMonitor:
def __init__(self, username, password, queue: asyncio.Queue, token_file="icloud_token.txt"):
self.username = username
self.password = password
self.token_file = token_file
self.queue = queue
self.api = None
self.device = None
self.running = True
async def authenticate(self):
"""Authenticates with iCloud, handling 2FA and token storage."""
if os.path.exists(self.token_file):
print("Loading stored session...")
self.api = PyiCloudService(self.username, cookie_directory="./cookies")
else:
print("No stored session. Authenticating...")
self.api = PyiCloudService(self.username, self.password, cookie_directory="./cookies")
if self.api.requires_2fa:
print("Two-factor authentication required.")
code = input("Enter the code you received: ")
result = self.api.validate_2fa_code(code)
print(f"Code validation result: {result}")
if not result:
print("Failed to verify 2FA code")
return False
# Trust the session
self.api.trust_session()
print("Successfully authenticated.")
return True
async def get_location(self):
"""Fetches the latest latitude and longitude."""
if not self.api:
await self.authenticate()
# Refresh API data
self.api.refresh_client()
# Find the device (modify name to match your iPhone name in iCloud)
if not self.device:
# Assuming you have devices, pick the first or match by name
self.device = self.api.devices[0]
print(f"Monitoring device: {self.device.name()}")
location = self.device.location()
if location:
return location['latitude'], location['longitude'], location['timeStamp']
return None
def start(self):
self.running = True
def stop(self):
self.running = False
async def run_monitor(self, interval=60):
"""Runs the monitor loop."""
if not await self.authenticate():
return
if not self.running:
self.start()
while self.running:
try:
lat, lon, ts = await self.get_location()
print(f"[{ts}] Location: {lat}, {lon}")
# Add your logic to update database/API here
await self.queue.put(lat, lng, ts)
except Exception as e:
print(f"Error: {e}")
# Re-authenticate if session expired
await self.authenticate()
await asyncio.sleep(interval)

323
server.py
View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
from icloud import FindMyMonitor
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import json import json
import uuid import uuid
@@ -9,11 +10,13 @@ import signal
import traceback import traceback
import warnings import warnings
import random import random
from operator import truediv
from pydantic import BaseModel, RootModel from pydantic import BaseModel, RootModel
import socketio import socketio
from contextlib import asynccontextmanager, suppress from contextlib import asynccontextmanager, suppress
from typing import Optional from typing import Optional, Dict
from pymobiledevice3.services.dvt.instruments.location_simulation_base import LocationSimulationBase from pymobiledevice3.services.dvt.instruments.location_simulation_base import LocationSimulationBase
@@ -129,24 +132,37 @@ class SimulationRequestResponse(BaseModel):
status: bool status: bool
data: Optional[SimulationRequestResponseData] data: Optional[SimulationRequestResponseData]
class SimulationQueueDict(BaseModel):
location_id: Dict[str, SimulationRequestResponseData]
class iCloudLocationData(BaseModel):
latitude: number
longitude: number
timestamp: string
class LocationSimulationState: class LocationSimulationState:
def __init__(self): def __init__(self):
self.current_location: Optional[Dict[str, SimulationRequestResponseData]] = None
self.next_location: Optional[Dict[str, SimulationRequestResponseData]] = None
self.loc_id: Optional[str] = None
self.latitude: Optional[float] = None self.latitude: Optional[float] = None
self.longitude: Optional[float] = None self.longitude: Optional[float] = None
self.next_move: Optional[float] = None self.next_move: Optional[float] = None
self.udid: Optional[str] = None self.udid: Optional[str] = None
self.simulation_active: bool = False self.simulation_active: bool = False
self.loc_id: Optional[str] = None
self.set_location_enabled: bool = True self.set_location_enabled: bool = True
self.queue: asyncio.Queue = asyncio.Queue() self.queue: asyncio.Queue = asyncio.Queue()
self.queue_list: list[SimulationRequestResponseData] = [] self.queue_order: list[str] = []
self.queue_data: Dict = {}
self.queue_status: Optional[asyncio.Event] = asyncio.Event() self.queue_status: Optional[asyncio.Event] = asyncio.Event()
self.queue_state: str = "STOPPED" self.queue_state: str = "STOPPED"
self.test_mode: bool = True self.test_mode: bool = False
self.simulation_task: Optional[asyncio.Task] = None self.simulation_task: Optional[asyncio.Task] = None
self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
self.tunnel: Optional[RemoteServiceDiscoveryService] = None self.tunnel: Optional[RemoteServiceDiscoveryService] = None
self.fmf_queue: asyncio.Queue = asyncio.Queue
self.fmf_location: Optional[iCloudLocationData] = None
class TunneldRunnerSio: class TunneldRunnerSio:
@@ -325,10 +341,7 @@ class TunneldRunnerSio:
return return
logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid) logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid)
# tun = await asyncio.wait_for(
# get_tun(self.context.udid),
# timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS,
# )
tun = await get_tun(self.context.udid) tun = await get_tun(self.context.udid)
logger.info("Simulation worker: tunnel acquired, connecting DVT provider") logger.info("Simulation worker: tunnel acquired, connecting DVT provider")
dvt_provider = DvtProvider(tun) dvt_provider = DvtProvider(tun)
@@ -369,6 +382,19 @@ class TunneldRunnerSio:
self.context.simulation_active = False self.context.simulation_active = False
self.context.simulation_task = None self.context.simulation_task = None
async def start_icloud_monitor()
"""Start Apple iCloud Find My Monitor to retreive actual reported device location"""
monitor = FindMyMonitor(apple_id, apple_pw, self.context.fmf_queue)
monitor_task = asyncio.create_task(monitor.run_monitor(interval=30))
while True:
updated_location = await self.context.fmf_queue.get()
if self.context.fmf_location !== updated_location:
self.context.fmf_location = update_location
self.context.sio.emit("fmf_update", updated_location, namespace="/",)
async def pause_simulation_queue(): async def pause_simulation_queue():
"""Pauses asyncio.Queue playback""" """Pauses asyncio.Queue playback"""
self.context.queue_state = "PAUSED" self.context.queue_state = "PAUSED"
@@ -390,23 +416,95 @@ class TunneldRunnerSio:
except asyncio.QueueEmpty: except asyncio.QueueEmpty:
break break
async def end_simulation_queue() -> str: def add_item(item_id, payload):
self.context.queue_data[item_id] = payload
self.context.queue_order.append(item_id)
def remove_item(item_id):
if item_id in self.context.queue_order:
self.context.queue_order.remove(item_id)
def get_item(item_id):
return self.context.queue_data[item_id]
def update_item(item_id, **updates):
if item_id in self.context.queue_data:
self.context.queue_data[item_id].update(updates)
def get_item_index(item_id):
return self.context.queue_order.index(item_id)
def get_item_id_by_index(index):
return self.context.queue_order[index]
def get_items_in_order():
return [self.context.queue_data[i] for i in self.context.queue_order]
async def end_simulation_queue() -> bool:
"""Ends asyncio.Queue playback and closes tunnel""" """Ends asyncio.Queue playback and closes tunnel"""
logger.info("End location simulation request from %s", sid) logger.info("End location simulation request from %s", sid)
if self.context.simulation_task is not None and not self.context.simulation_task.done(): try:
q = self.context.queue if self.context.test_mode:
if q.qsize() > 0: q = self.context.queue
await empty_simulation_queue() if q.qsize() > 0:
while q.empty() and q.qsize() == 0: self.context.set_location_enabled = False
while not q.empty():
try:
item = q.get_nowait()
q.task_done()
logger.info("Discarding item from queue: %s", item)
except asyncio.QueueEmpty:
break
await q.join() await q.join()
with suppress(asyncio.CancelledError): # with suppress(asyncio.CancelledError):
await self.context.simulation_task # await self.context.simulation_task
if self.context.tunnel is not None: self.context.simulation_active = False
async with DvtProvider(self.context.tunnel) as dvt, LocationSimulation(dvt) as locate_simulation: self.context.queue_state = "SHUTDOWN"
await locate_simulation.clear() return True
self.context.simulation_active = False if not self.context.test_mode:
self.context.queue_state = "SHUTDOWN" if self.context.simulation_task is not None and not self.context.simulation_task.done():
return "ended" q = self.context.queue
if q.qsize() > 0:
await empty_simulation_queue()
while q.empty() and q.qsize() == 0:
await q.join()
with suppress(asyncio.CancelledError):
await self.context.simulation_task
if self.context.tunnel is not None:
async with DvtProvider(self.context.tunnel) as dvt, LocationSimulation(dvt) as locate_simulation:
await locate_simulation.clear()
self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN"
return True
except Exception as e:
logger.error(f"Error ending simulation queue: {e}")
return False
def get_status():
data = {
"current_location": self.context.current_location,
"next_location": self.context.next_location,
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": self.context.next_move,
"udid": self.context.udid,
"simulation_active": self.context.simulation_active,
"loc_id": self.context.loc_id,
"set_location_enable": self.context.set_location_enabled,
"queue_length": self.context.queue.qsize() if self.context.queue else 0,
"queue_state": self.context.queue_state,
"queue_order": self.context.queue_order,
"queue_data": self.context.queue_data,
"queue_status": self.context.queue_status.is_set() if self.context.queue_status else False,
"test_mode": self.context.test_mode,
"simulation_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
"tunnel": self.context.tunnel.service.address[0] if self.context.tunnel else None,
}
return data
""" FastAPI HTTP Functions""" """ FastAPI HTTP Functions"""
def generate_http_response( def generate_http_response(
@@ -646,27 +744,22 @@ class TunneldRunnerSio:
@self._app.get("/context-status") @self._app.get("/context-status")
async def app_context_status() -> fastapi.Response: async def app_context_status() -> fastapi.Response:
data = { data = get_status()
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_more": self.context.next_move,
"udid": self.context.udid,
"simulation_active": self.context.simulation_active,
"loc_id": self.context.loc_id,
"set_location_enable": self.context.set_location_enabled,
"queue_state": self.context.queue_state,
"queue_list": self.context.queue_list
}
return generate_http_response(data) return generate_http_response(data)
""" Socket.IO Functions""" """ Socket.IO Functions"""
async def sio_send_status(sid):
""" Send Current Status"""
await self.context.sio.emit("status", get_status(), namespace="/", to=sid)
"""Socket.IO Connection Events""" """Socket.IO Connection Events"""
@self.context.sio.event @self.context.sio.event
async def connect(sid, environ): async def connect(sid, environ):
"""Client connection event handler.""" """Client connection event handler."""
logger.info("Client connected: %s", sid) logger.info("Client connected: %s", sid)
return('%s connected' % sid) await sio_send_status(sid)
return '%s connected' % sid
@self.context.sio.event @self.context.sio.event
async def disconnect(sid): async def disconnect(sid):
@@ -680,8 +773,9 @@ class TunneldRunnerSio:
@self.context.sio.event @self.context.sio.event
async def message(sid, data): async def message(sid, data):
logger.info("Received message from %s: %s", data, sid) logger.info("Received message from %s: %s", sid, data)
await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/") return True, "Message received"
# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
""" Device Control""" """ Device Control"""
@self.context.sio.event @self.context.sio.event
@@ -708,83 +802,86 @@ class TunneldRunnerSio:
return { "command": command, "status": "error", "message": f"Invalid command: {command}" } return { "command": command, "status": "error", "message": f"Invalid command: {command}" }
@self.context.sio.event @self.context.sio.event
async def simulate_control(sid, data): async def simulation_control(sid, data):
""" Simulation Control """ """ Simulation Control """
command = data.get("command") if isinstance(data, dict) else getattr(data, "command", None) command = data.get("command") if isinstance(data, dict) else getattr(data, "command", None)
logger.info("Simulation Control command: %s requested from %s", command, sid) logger.info("Simulation Control command: %s requested from %s", command, sid)
match command: try:
case "add": match command:
""" Add a location to the simulation queue""" case "add":
loc_id = str(uuid.uuid4()) """ Add a location to the simulation queue"""
latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None) loc_id = str(uuid.uuid4())
longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None) latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None)
delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0) longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None)
delay = 0 if delay is None else delay delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0)
if latitude is not None and longitude is not None: delay = 0 if delay is None else delay
logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude, if latitude is not None and longitude is not None:
delay) logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude,
await self.context.queue.put((loc_id, latitude, longitude, delay)) delay)
if delay == 0: accrued_delay = 0
start_time = datetime.now(timezone.utc).isoformat() if self.context.queue_data:
else: accrued_delay = sum(item.get('delay', 0) for item in self.context.queue_data.values())
now_time = datetime.now(timezone.utc) now_time = datetime.now(timezone.utc)
new_time = now_time + timedelta(seconds=delay) new_time = now_time + timedelta(seconds=accrued_delay) + timedelta(seconds=delay)
start_time = new_time.isoformat() start_time = new_time.isoformat()
location_item = {
location_item = {
loc_id: {
"loc_id": loc_id, "loc_id": loc_id,
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
"delay": delay, "delay": delay,
"start": start_time "start": start_time
} }
} ack = {
ack = { "command": command,
"command": command, "status": "added",
"status": "added", "message": f"Location {loc_id} added to the queue",
"message": f"Location {loc_id} added to the queue", "item": location_item
"item": location_item }
} await self.context.queue.put(loc_id)
self.context.queue_list.append(location_item) add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id) logger.info("Location %s added to the queue", loc_id)
return ack return ack
else: else:
logger.warning("Invalid location data received from %s: %s", sid, data) logger.warning("Invalid location data received from %s: %s", sid, data)
return {"command": command, "status": "error", "message": "Invalid location data"} return {"command": command, "status": "error", "message": "Invalid location data", "data": location_item}
case "clear": case "clear":
""" Clear the simulation queue""" """ Clear the simulation queue"""
await empty_simulation_queue() await empty_simulation_queue()
return {"command": command, "status": "cleared", "message": "Simulation cleared"} return {"command": command, "status": "cleared", "message": "Simulation cleared"}
case "pause": case "pause":
""" Pause the simulation queue""" """ Pause the simulation queue"""
await pause_simulation_queue() await pause_simulation_queue()
return {"command": command, "status": "paused", "message": "Simulation paused"} return {"command": command, "status": "paused", "message": "Simulation paused"}
case "resume": case "resume":
""" Resume the simulation queue""" """ Resume the simulation queue"""
await resume_simulation_queue() await resume_simulation_queue()
return {"command": command, "status": "resumed", "message": "Simulation resumed"} return {"command": command, "status": "resumed", "message": "Simulation resumed"}
case "end": case "end":
""" End the simulation queue""" """ End the simulation queue"""
logger.info("End location simulation request from %s", sid) logger.info("End location simulation request from %s", sid)
end_task = asyncio.create_task(end_simulation_queue(), name="end-simulation-worker") end_task = asyncio.create_task(end_simulation_queue(), name="end-simulation-worker")
result = await end_task result = await end_task
return {"command": command, "status": result, "message": "Simulation ended"} simstatus = not result
case "start": return {"command": command, "status": simstatus, "message": "Simulation ended"}
""" Start the simulation queue""" case "start":
logger.info("Start location simulation request from %s", sid) """ Start the simulation queue"""
if self.context.simulation_task is None or self.context.simulation_task.done(): logger.info("Start location simulation request from %s", sid)
self.context.simulation_active = True if self.context.simulation_task is None or self.context.simulation_task.done():
self.context.simulation_task = asyncio.create_task( self.context.simulation_active = True
start_simulation_queue(), self.context.queue_state = "RUNNING"
name="location-simulation-worker", self.context.simulation_task = asyncio.create_task(
) start_simulation_queue(),
return {"command": command, "status": "started", "message": "Simulation started"} name="location-simulation-worker",
else: )
return {"command": command, "status": "error", "message": "Simulation already running"} return {"command": command, "status": self.context.queue_state, "message": "Simulation started"}
case _: else:
logger.warning("Invalid command received from %s: %s", sid, command) return {"command": command, "status": "error", "message": "Simulation already running"}
return {"status": "error", "message": "Invalid command"} case _:
logger.warning("Invalid command received from %s: %s", sid, command)
return {"status": "error", "message": "Invalid command"}
finally:
await sio_send_status(sid)
""" Tunnel Control """ """ Tunnel Control """
@self.context.sio.event @self.context.sio.event
@@ -798,7 +895,7 @@ class TunneldRunnerSio:
try: try:
self._tunneld_core.start() self._tunneld_core.start()
logger.info("Tunneld started successfully") logger.info("Tunneld started successfully")
return {"status": "started", "message": "Tunneld started successfully"} return {"status": "running", "message": "Tunneld started successfully"}
except Exception as e: except Exception as e:
logger.error("Error starting tunneld: %s", e) logger.error("Error starting tunneld: %s", e)
return {"command": command, "status": "error", "message": f"Error starting tunneld: {e}"} return {"command": command, "status": "error", "message": f"Error starting tunneld: {e}"}
@@ -855,9 +952,15 @@ class LocationSimulationQueue(LocationSimulation):
continue continue
if self.context.queue_state == "SHUTDOWN": if self.context.queue_state == "SHUTDOWN":
break break
loc_id, latitude, longitude, delay = await self.context.queue.get() loc_id = await self.context.queue.get()
if (loc_id, latitude, longitude, delay) == (None, None, None, None): if loc_id == None:
break break
location_item = self.context.queue_data.get(loc_id)
latitude = location_item.get("latitude")
longitude = location_item.get("longitude")
delay = location_item.get("delay")
delay = 0 if delay is None else delay
start_time = location_item.get("start_time")
if self.context.set_location_enabled: if self.context.set_location_enabled:
if delay > 0 and not disable_sleep: if delay > 0 and not disable_sleep:
if timing_randomness_range > 0: if timing_randomness_range > 0:
@@ -941,9 +1044,14 @@ class LocationSimulationTestQueue(LocationSimulationBase):
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
if self.context.queue_state == "SHUTDOWN": if self.context.queue_state == "SHUTDOWN":
break break
loc_id, latitude, longitude, delay = await self.context.queue.get() loc_id = await self.context.queue.get()
if (loc_id, latitude, longitude, delay) == (None, None, None, None): if loc_id == None:
break break
location_item = self.context.queue_data.get(loc_id)
latitude = location_item.get("latitude")
longitude = location_item.get("longitude")
delay = location_item.get("delay")
start_time = location_item.get("start_time")
if self.context.set_location_enabled: if self.context.set_location_enabled:
if delay > 0 and not disable_sleep: if delay > 0 and not disable_sleep:
if timing_randomness_range > 0: if timing_randomness_range > 0:
@@ -969,6 +1077,7 @@ class LocationSimulationTestQueue(LocationSimulationBase):
self.context.longitude = longitude self.context.longitude = longitude
self.context.loc_id = loc_id self.context.loc_id = loc_id
await self.context.sio.emit( await self.context.sio.emit(
"simulation_status", "simulation_status",
{ {
"status": self.context.simulation_active, "status": self.context.simulation_active,

1074
server_recover.py Normal file

File diff suppressed because it is too large Load Diff