Files
sim-location-backend/src/pymd3_vue_location_sim/sio_events.py
T
2026-06-10 11:06:33 -04:00

783 lines
32 KiB
Python

import json
import asyncio
import os
import signal
from dataclasses import dataclass
from typing import Any, Awaitable, Callable
from fastapi.encoders import jsonable_encoder
from .json_formatter import logger
from .models import LatLng
@dataclass(slots=True)
class SocketIOEventHandlers:
context: Any
tunneld_core: Any
get_status: Callable[[], dict]
get_simulation_status: Callable[[], dict]
device_shutdown: Callable[[Any], Awaitable[Any]]
device_reboot: Callable[[Any], Awaitable[Any]]
advance_simulation_queue: Callable[[], Awaitable[Any]]
toggle_test_mode: Callable[[], dict]
add_location_to_simulation_queue: Callable[[Any], Awaitable[dict]]
delete_location_from_simulation_queue: Callable[[Any], Awaitable[dict]]
clear_future_items: Callable[[], Any]
reset_queue: Callable[[], Any]
pause_simulation_queue: Callable[[], Awaitable[Any]]
resume_simulation_queue: Callable[[], Awaitable[Any]]
end_simulation_queue: Callable[[], Awaitable[Any]]
start_simulation_queue: Callable[[], Awaitable[dict]]
restart_simulation_queue: Callable[[], Awaitable[dict]]
toggle_gps_noise: Callable[[], dict]
update_queue_times: Callable[[], Awaitable[Any]]
start_icloud_monitor: Callable[[], Awaitable[Any]]
end_icloud_monitor: Callable[[], Awaitable[Any]]
is_icloud_monitor_running: Callable[[], bool]
refresh_icloud_location: Callable[[], Awaitable[dict]]
start_tunnel_watcher: Callable[[], Awaitable[Any]]
end_tunnel_watcher: Callable[[], Awaitable[Any]]
get_reverse_geocode: Callable[[Any], Awaitable[Any]]
set_favorite: Callable[[Any], Awaitable[tuple[str, str]]]
clear_favorite: Callable[[Any], Awaitable[tuple[str, str]]]
get_favorites: Callable[[], Awaitable[Any]]
get_categories: Callable[[], Awaitable[Any]]
add_category: Callable[[Any], Awaitable[dict]]
delete_category: Callable[[Any], Awaitable[tuple[str, str]]]
def register(self) -> None:
sio = self.context.sio
async def emit_test_prompt(test_prompt_data):
prompt_name = (
test_prompt_data.get("prompt_name")
if isinstance(test_prompt_data, dict)
else getattr(test_prompt_data, "prompt_name", None)
)
client_id = (
test_prompt_data.get("client_id")
if isinstance(test_prompt_data, dict)
else getattr(test_prompt_data, "client_id", None)
)
logger.info(
"Sending test prompt %s sent to client %s", prompt_name, client_id
)
try:
response = await sio.call(
"test_prompt",
{"prompt_name": prompt_name},
namespace="/",
to=client_id,
timeout=60,
)
if response and response.get("confirmed"):
await do_positive_action(prompt_name, client_id)
else:
await do_negative_action(prompt_name, client_id)
except asyncio.TimeoutError:
print(f"Client {client_id} failed to respond in time.")
async def do_positive_action(prompt_name, client_id):
logger.info(
"Confirmation received from client %s fpr prompt %s",
prompt_name,
client_id,
)
async def do_negative_action(prompt_name, client_id):
logger.info(
"Client %s denied confirmation for prompt %s", client_id, prompt_name
)
async def sio_send_status(sid):
"""Send Current Status"""
if sid in self.context.connected_clients:
logger.info("Sending status to: %s", sid)
await sio.emit(
"status", jsonable_encoder(self.get_status()), namespace="/", to=sid
)
else:
logger.info("Sending status to all clients, not only: %s", sid)
await sio.emit(
"status", jsonable_encoder(self.get_status()), namespace="/"
)
def json_fallback(obj):
if hasattr(obj, "__dict__"):
return obj.__dict__
if hasattr(obj, "as_dict"):
return obj.as_dict()
if hasattr(obj, "to_dict"):
return obj.to_dict()
return str(obj)
@sio.event
async def connect(sid, environ):
"""Client connection event handler."""
self.context.connected_clients.add(sid)
logger.info("Client connected: %s", sid)
await sio_send_status(sid)
return "%s connected" % sid
@sio.event
async def disconnect(sid):
"""Client disconnection event handler."""
self.context.connected_clients.discard(sid)
logger.info("Client disconnected: %s", sid)
@sio.event
async def send_test_prompt(sid, data):
"""Send test prompt"""
prompt_name = (
data.get("prompt_name")
if isinstance(data, dict)
else getattr(data, "prompt_name", None)
)
logger.info(
"Received request from %s to send test prompt %s", sid, prompt_name
)
test_prompt_data = {"prompt_name": prompt_name, "client_id": sid}
sio.start_background_task(emit_test_prompt, test_prompt_data)
return {
"prompt_name": prompt_name,
"command_status": "OK",
"message": "Test prompt requested.",
}
@sio.event
async def request_update(sid):
status_update = jsonable_encoder(self.get_status())
logger.info("Update request from %s sending %s", sid, status_update)
return status_update
@sio.event
async def message(sid, data):
logger.info("Received message from %s: %s", sid, data)
return True, "Message received"
@sio.event
async def device_control(sid, data):
"""Device Control"""
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
delay = (
data.get("delay")
if isinstance(data, dict)
else getattr(data, "delay", None)
)
if delay is None:
delay = 5
match command:
case "shutdown":
logger.info(
"Shutdown command received from %s with delay %s", sid, delay
)
await self.device_shutdown(delay)
return {
"command_status": "OK",
"command_class": "device_control",
"command": "shutdown",
"message": f"Device shutdown initiated with {delay} seconds delay",
}
case "reboot":
logger.info(
"Reboot command received from %s with delay %s", sid, delay
)
await self.device_reboot(delay)
return {
"command_status": "OK",
"command_class": "device_control",
"command": "reboot",
"message": f"Device reboot initiated with {delay} seconds delay",
}
case _:
return {
"command_status": "ERROR",
"command_class": "device_control",
"command": command,
"message": f"Invalid command: {command}",
}
@sio.event
async def simulation_control(sid, data):
"""Simulation Control"""
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info(
"Simulation Control command: %s requested from %s", command, sid
)
try:
match command:
case "next":
await self.advance_simulation_queue()
return {
"command": command,
"command_class": "simulation_control",
"command_status": "OK",
"message": "Advanced simulation queue",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "test-mode":
self.toggle_test_mode()
return {
"command": command,
"command_class": "simulation_control",
"command_status": "OK",
"message": "test-mode toggled",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "add":
return await self.add_location_to_simulation_queue(data)
case "delete":
return await self.delete_location_from_simulation_queue(data)
case "clear":
self.clear_future_items()
return {
"command": command,
"command_class": "simulation_control",
"command_status": "OK",
"message": "Simulation cleared",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "reset":
self.reset_queue()
return {
"command": command,
"command_class": "simulation_control",
"command_status": "OK",
"message": "Simulation queue reset",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "pause":
await self.pause_simulation_queue()
return {
"command": command,
"command_class": "simulation_control",
"command_status": "OK",
"message": "Simulation paused",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "resume":
await self.resume_simulation_queue()
return {
"command": command,
"command_status": "OK",
"command_class": "simulation_control",
"message": "Simulation resumed",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "end":
logger.info("End location simulation request from %s", sid)
cstat = "OK" if await self.end_simulation_queue() else "ERROR"
return {
"command_status": cstat,
"command_class": "simulation_control",
"command": command,
"message": "Simulation ended",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "start":
logger.info("Start location simulation request from %s", sid)
return await self.start_simulation_queue()
case "gps-noise":
before_toggle = self.context.simulation_noise
self.toggle_gps_noise()
return {
"command_status": "OK",
"command_class": "simulation_control",
"command": command,
"message": f"GPS noise toggled from {before_toggle} to {self.context.simulation_noise}",
"data": {
"simulation": self.get_simulation_status(),
},
}
case "restart":
logger.info("Restart location simulation request from %s", sid)
return await self.restart_simulation_queue()
case _:
logger.warning(
"Invalid command received from %s: %s", sid, command
)
return {"command_status": "ERROR", "message": "Invalid command"}
finally:
logger.info(
"Simulation Control command: %s completed, sending status update...",
command,
)
await sio_send_status(sid)
@sio.event
async def location_item_update(sid, data):
"""Location Item Control"""
loc_id = (
data.get("loc_id")
if isinstance(data, dict)
else getattr(data, "loc_id", None)
)
key = (
data.get("key")
if isinstance(data, dict)
else getattr(data, "key", None)
)
value = (
data.get("value")
if isinstance(data, dict)
else getattr(data, "value", None)
)
error_data = {
"command_status": "ERROR",
"command": "update",
"command_class": "location_item",
"message": "Invalid request, Location Item Unchanged",
"data": self.context.simulation_queue_data[loc_id],
}
if value is not None:
if loc_id and key:
old_val = self.context.simulation_queue_data[loc_id][key]
self.context.simulation_queue_data[loc_id][key] = value
if key == "delay":
await self.update_queue_times()
logger.info(
"Location Item Update: %s: %s changed from %s to %s",
loc_id,
key,
old_val,
value,
)
return {
"command_status": "OK",
"command": "update",
"command_class": "location_item",
"message": "Location Item Updated",
"data": {
"simulation": {
"data": {
loc_id: self.context.simulation_queue_data[loc_id],
},
},
},
}
else:
return error_data
else:
return error_data
@sio.event
async def queue_order_update(sid, data):
logger.info("Queue order update received: %s", data)
new_simulation_queue_order = []
new_order = (
data.get("newOrder")
if isinstance(data, dict)
else getattr(data, "newOrder", None)
)
if new_order:
for item in new_order:
if item in self.context.simulation_queue_data:
new_simulation_queue_order.append(item)
self.context.simulation_queue_order = new_simulation_queue_order
return {
"command_status": "OK",
"command": "queue_order_update",
"command_class": "queue_order",
"message": "Queue Order Updated",
"data": {
"simulation": {
"order" : self.context.simulation_queue_order,
},
},
}
return {
"command_status": "ERROR",
"command": "queue_order_update",
"command_class": "queue_order",
"message": "Invalid request, Queue Order Unchanged",
"data": {
"simulation": {
"order": self.context.simulation_queue_order,
},
},
}
@sio.event
async def icloud_monitor_control(sid, data):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info(
"iCloud Monitor control command: %s requested from %s", command, sid
)
try:
match command:
case "start":
await self.start_icloud_monitor()
return {
"command": command,
"command_class": "icloud_monitor_control",
"command_status": "OK",
"data": {
"icloud": {
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": self.is_icloud_monitor_running(),
},
},
}
case "stop":
await self.end_icloud_monitor()
return {
"command": command,
"command_class": "icloud_monitor_control",
"command_status": "OK",
"data": {
"icloud": {
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": self.is_icloud_monitor_running(),
},
},
}
case "status":
return {
"command": command,
"command_class": "icloud_monitor_control",
"command_status": "OK",
"data": {
"icloud": {
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": self.is_icloud_monitor_running(),
},
},
}
case "refresh":
resp = {
"command": command,
"command_class": "icloud_monitor_control",
"command_status": "OK",
"data": {
"icloud": {
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": self.is_icloud_monitor_running(),
"fmf_location": json.dumps(
await self.refresh_icloud_location(),
default=json_fallback,
),
},
},
}
return resp
case _:
return {
"command": command,
"command_class": "icloud_monitor_control",
"command_status": "ERROR",
"message": "Invalid command",
}
finally:
await sio_send_status(sid)
@sio.event
async def tunneld_control(sid, data):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info("Tunneld Control command: %s requested from %s", command, sid)
match command:
case "start":
logger.info("Start tunneld request from %s: %s", sid, data)
try:
self.tunneld_core.start()
logger.info("Tunneld started successfully")
return {
"command_status": "OK",
"command_class": "tunneld_control",
"command": command,
"message": "Tunneld started successfully",
}
except Exception as e:
logger.error("Error starting tunneld: %s", e)
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "ERROR",
"message": f"Error starting tunneld: {e}",
}
case "restart":
logger.info("Restart tunneld request from %s: %s", sid, data)
try:
self.tunneld_core.clear()
await asyncio.sleep(2)
self.tunneld_core.start()
return {
"command_status": "OK",
"command_class": "tunneld_control",
"command": command,
"message": "Tunneld started successfully",
}
except Exception as e:
logger.error("Error restarting tunneld: %s", e)
return {
"command_status": "ERROR",
"command_class": "tunneld_control",
"command": command,
"message": f"Error starting tunneld: {e}",
}
case "start-watcher":
logger.info("Start tunneld watcher request from %s: %s", sid, data)
await self.start_tunnel_watcher()
return {
"command_status": "OK",
"command_class": "tunneld_control",
"command": command,
"message": "Tunneld watcher started successfully",
}
case "end-watcher":
logger.info("End tunneld watcher request from %s: %s", sid, data)
await self.end_tunnel_watcher()
return {
"command_status": "OK",
"command_class": "tunneld_control",
"command": command,
"message": "Tunneld watcher stopped successfully",
}
case "shutdown":
logger.info("Shutdown tunneld request from %s: %s", sid, data)
try:
os.kill(os.getpid(), signal.SIGINT)
return {
"command_status": "OK",
"command_class": "tunneld_control",
"command": command,
"message": "Server shutting down...",
}
except Exception as e:
logger.error("Error shutting down tunneld: %s", e)
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "ERROR",
"message": f"Error shutting down tunneld: {e}",
}
case "clear":
logger.info("Clearing tunnels...")
try:
self.tunneld_core.clear()
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "OK",
"message": "Cleared tunnels...",
}
except Exception as e:
logger.error("Error clearing tunnels: %s", e)
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "ERROR",
"message": f"Error clearing tunnels: {e}",
}
case "cancel":
logger.info("Canceling tunnel request from %s: %s", sid, data)
try:
udid = (
data.get("udid")
if isinstance(data, dict)
else getattr(data, "udid", self.context.udid)
)
if udid is None:
udid = self.context.udid
self.tunneld_core.cancel(udid=udid)
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "OK",
"udid": udid,
"message": f"tunnel {udid} Canceled ...",
}
except Exception as e:
logger.error("Error canceling tunnel: %s", e)
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "ERROR",
"message": f"Error canceling tunnel: {e}",
}
case _:
return {
"command": command,
"command_class": "tunneld_control",
"command_status": "ERROR",
"message": f"Unknown operation: {command}",
}
@sio.event
async def reverse_geocode(sid, data: LatLng):
logger.info("OSM Proxy Request from %s, data: %s", sid, data)
return await self.get_reverse_geocode(data)
@sio.event
async def favorite_control(sid, data: dict):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info("Favorite Control command: %s requested from %s", command, sid)
match command:
case "set":
cstat, cmessage = await self.set_favorite(data)
if cstat == "ERROR":
logger.error("Favorite set failed: %s", cmessage)
return {
"command": command,
"command_class": "favorite_control",
"command_status": cstat,
"message": cmessage,
"data": {
"favorites": await self.get_favorites(),
},
}
case "clear":
cstat, cmessage = await self.clear_favorite(data)
return {
"command": command,
"command_class": "favorite_control",
"command_status": cstat,
"message": cmessage,
"data": {
"favorites": await self.get_favorites(),
},
}
case "get":
favs = await self.get_favorites()
if favs:
cstat = "OK"
cmessage = "Favorites retrieved successfully"
else:
cstat = "ERROR"
cmessage = "No favorites found"
return {
"command": command,
"command_class": "favorite_control",
"command_status": cstat,
"message": cmessage,
"data": {
"favorites": favs,
},
}
case _:
return {
"command": command,
"command_class": "favorite_control",
"command_status": "ERROR",
"message": f"Invalid command: {command}",
}
@sio.event
async def category_control(sid, data: dict):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info("Category Control command: %s requested from %s", command, sid)
match command:
case "add":
cstat, cmessage = await self.add_category(data)
if cstat == "ERROR":
logger.error("Category set failed: %s", cmessage)
return {
"command": command,
"command_class": "category_control",
"command_status": cstat,
"message": cmessage,
"data": {
"categories": await self.get_categories(),
},
}
case "delete":
cstat, cmessage = await self.delete_category(data)
return {
"command": command,
"command_class": "category_control",
"command_status": cstat,
"message": cmessage,
"data": {
"categories": await self.get_categories(),
},
}
case "get":
cats = await self.get_categories()
if cats:
cstat = "OK"
cmessage = "Categories retrieved successfully"
else:
cstat = "ERROR"
cmessage = "No categories found"
logger.info("Categories: %s", cats)
return {
"command": command,
"command_class": "category_control",
"command_status": cstat,
"message": cmessage,
"data": {
"categories": cats,
},
}
case _:
return {
"command": command,
"command_class": "category_control",
"command_status": "ERROR",
"message": f"Invalid command: {command}",
}