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}", }