diff --git a/app/routers/nodes.py b/app/routers/nodes.py new file mode 100644 index 0000000..23b8239 --- /dev/null +++ b/app/routers/nodes.py @@ -0,0 +1,119 @@ +import json +from quart import Blueprint, jsonify, request, abort, current_app +from werkzeug.exceptions import HTTPException +from enum import Enum + +nodes_bp = Blueprint('nodes', __name__) + +class NodeCommands(str, Enum): + JOIN = "join_server" + LEAVE = "leave_server" + + +async def register_client(websocket, client_id): + """Registers a new client connection.""" + current_app.active_clients[client_id] = websocket + print(f"Client {client_id} connected.") + + +async def unregister_client(client_id): + """Unregisters a disconnected client.""" + if client_id in current_app.active_clients: + del current_app.active_clients[client_id] + print(f"Client {client_id} disconnected.") + + +async def send_command_to_client(client_id, command_name, *args): + """Sends a command to a specific client.""" + if client_id in current_app.active_clients: + websocket = current_app.active_clients[client_id] + message = json.dumps({"type": "command", "name": command_name, "args": args}) + try: + await websocket.send(message) + print(f"Sent command '{command_name}' to client {client_id}") + except websockets.exceptions.ConnectionClosedError: + print(f"Failed to send to client {client_id}: connection closed.") + await unregister_client(client_id) + else: + print(f"Client {client_id} not found.") + + +async def send_command_to_all_clients(command_name, *args): + """Sends a command to all connected clients.""" + message = json.dumps({"type": "command", "name": command_name, "args": args}) + # Use a list of items to avoid issues if clients disconnect during iteration + clients_to_send = list(current_app.active_clients.items()) + for client_id, websocket in clients_to_send: + try: + await websocket.send(message) + print(f"Sent command '{command_name}' to client {client_id}") + except websockets.exceptions.ConnectionClosedError: + print(f"Failed to send to client {client_id}: connection closed.") + await unregister_client(client_id) + + + +@nodes_bp.route("/", methods=['GET']) +async def get_nodes(): + """API endpoint to list currently connected client IDs.""" + return jsonify(list(current_app.active_clients.keys())) + + +@nodes_bp.route("/join", methods=['POST']) +async def join(): + """ + Send a join command to the specific system specified + """ + data = await request.get_json() + + client_id = data.get("client_id") + system_id = data.get("system_id") + guild_id = data.get("guild_id") + channel_id = data.get("channel_id") + + # Check to make sure the client is online + if client_id not in current_app.active_clients: + return jsonify({"error": f"Client {client_id} not found, it might be offline"}), 404 + + try: + args = [system_id, guild_id, channel_id] + + if not isinstance(args, list): + return jsonify({"error": "'args' must be a list"}), 400 + + # Send the command asynchronously + await send_command_to_client(client_id, NodeCommands.JOIN, *args) + + return jsonify({"status": "command sent", "client_id": client_id, "command": NodeCommands.JOIN}), 200 + + except Exception as e: + return jsonify({"error": f"Failed to send command: {e}"}), 500 + + +@nodes_bp.route("/leave", methods=['POST']) +async def leave(): + """ + Send a join command to the specific system specified + """ + data = await request.get_json() + + client_id = data.get("client_id") + guild_id = data.get("guild_id") + + # Check to make sure the client is online + if client_id not in current_app.active_clients: + return jsonify({"error": f"Client {client_id} not found, it might be offline"}), 404 + + try: + args = [guild_id] + + if not isinstance(args, list): + return jsonify({"error": "'args' must be a list"}), 400 + + # Send the command asynchronously + await send_command_to_client(client_id, NodeCommands.LEAVE, *args) + + return jsonify({"status": "command sent", "client_id": client_id, "command": NodeCommands.LEAVE}), 200 + + except Exception as e: + return jsonify({"error": f"Failed to send command: {e}"}), 500 \ No newline at end of file diff --git a/app/routers/systems.py b/app/routers/systems.py index d9d3e8f..1c7721a 100644 --- a/app/routers/systems.py +++ b/app/routers/systems.py @@ -1,6 +1,7 @@ from quart import Blueprint, jsonify, request, abort -from internal.db_wrappers import System, SystemDbController +from internal.db_wrappers import SystemDbController from werkzeug.exceptions import HTTPException +from internal.types import System systems_bp = Blueprint('systems', __name__) db_h = SystemDbController() @@ -257,3 +258,29 @@ async def dismiss_client_from_system_route(system_id: str): except Exception as e: print(f"Error during system de-assignment: {e}") abort(500, f"Internal server error: {e}") + + +@systems_bp.route('/search', methods=['GET']) +async def search_systems_route(): + """ + API endpoint to search for systems based on query parameters. + Allows searching by 'name', 'frequency_khz', or any other field present in the System model. + Example: /systems/search?name=MySystem&frequency_khz=1000 + """ + print("\n--- Handling GET /systems/search ---") + try: + query_params = dict(request.args) + + systems = await db_h.find_systems(query_params) + + if systems: + # If systems are found, return them as a list of dictionaries + return jsonify([system.to_dict() for system in systems]), 200 # 200 OK + else: + # If no systems match the query, return 404 Not Found + return jsonify({"message": "No systems found matching the criteria"}), 404 + except HTTPException: + raise + except Exception as e: + print(f"Error searching systems: {e}") + abort(500, f"Internal server error: {e}") diff --git a/app/server.py b/app/server.py index 3cdfa62..02ace77 100644 --- a/app/server.py +++ b/app/server.py @@ -4,48 +4,12 @@ import json import uuid from quart import Quart, jsonify, request from routers.systems import systems_bp +from routers.nodes import nodes_bp, register_client, unregister_client # --- WebSocket Server Components --- # Dictionary to store active clients: {client_id: websocket} active_clients = {} -async def register_client(websocket, client_id): - """Registers a new client connection.""" - active_clients[client_id] = websocket - print(f"Client {client_id} connected.") - -async def unregister_client(client_id): - """Unregisters a disconnected client.""" - if client_id in active_clients: - del active_clients[client_id] - print(f"Client {client_id} disconnected.") - -async def send_command_to_client(client_id, command_name, *args): - """Sends a command to a specific client.""" - if client_id in active_clients: - websocket = active_clients[client_id] - message = json.dumps({"type": "command", "name": command_name, "args": args}) - try: - await websocket.send(message) - print(f"Sent command '{command_name}' to client {client_id}") - except websockets.exceptions.ConnectionClosedError: - print(f"Failed to send to client {client_id}: connection closed.") - await unregister_client(client_id) - else: - print(f"Client {client_id} not found.") - -async def send_command_to_all_clients(command_name, *args): - """Sends a command to all connected clients.""" - message = json.dumps({"type": "command", "name": command_name, "args": args}) - # Use a list of items to avoid issues if clients disconnect during iteration - clients_to_send = list(active_clients.items()) - for client_id, websocket in clients_to_send: - try: - await websocket.send(message) - print(f"Sent command '{command_name}' to client {client_id}") - except websockets.exceptions.ConnectionClosedError: - print(f"Failed to send to client {client_id}: connection closed.") - await unregister_client(client_id) async def websocket_server_handler(websocket): """Handles incoming WebSocket connections and messages from clients.""" @@ -79,40 +43,15 @@ async def websocket_server_handler(websocket): if client_id: await unregister_client(client_id) -# --- Radio Channel Data (Placeholder) --- -# In a real app, this would likely come from a database -channels = { - "channel_1": { - "id": "channel_1", - "name": "Local News Radio", - "frequency_list_khz": ["98500"], - "decode_mode": "P25", - "location": "Cityville", - "tags": [], - "tag_whitelist": [1,2,3], - "avail_on_nodes": ["client-abc123", "client-def456"], # List of client IDs that can tune this channel - "description": "Your source for local news and weather." - }, - "channel_2": { - "id": "channel_2", - "name": "Music Mix FM", - "frequency_list_khz": [101300], - "decode_mode": "P25", - "location": "Townsville", - "tags": [], - "tag_whitelist": [6,7,8], - "avail_on_nodes": ["client-def456", "client-ghi789"], - "description": "Playing the hits, all day long." - } - # Add more channels here -} - # --- Quart API Components --- app = Quart(__name__) # Store the websocket server instance websocket_server_instance = None +# Make active_clients accessible via the app instance. +app.active_clients = active_clients + @app.before_serving async def startup_websocket_server(): """Starts the WebSocket server when the Quart app starts.""" @@ -139,16 +78,12 @@ async def shutdown_websocket_server(): app.register_blueprint(systems_bp, url_prefix="/systems") +app.register_blueprint(nodes_bp, url_prefix="/nodes") @app.route('/') async def index(): return "Welcome to the Radio App Server API!" -@app.route('/clients', methods=['GET']) -async def list_clients(): - """API endpoint to list currently connected client IDs.""" - return jsonify(list(active_clients.keys())) - @app.route('/request_token', methods=['POST']) async def request_token(): """API endpoint to list currently connected client IDs.""" @@ -157,36 +92,6 @@ async def request_token(): "token": "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA" }) -# --- Example API endpoint to trigger a WebSocket command --- -# This is a simple example. More complex logic might be needed -# to validate commands or arguments before sending. -@app.route('/command//', methods=['POST']) -async def api_send_command(client_id, command_name): - """ - API endpoint to send a command to a specific client via WebSocket. - Expects JSON body with 'args': [...] - e.g., POST to /command/client-abc123/print_message with body {"args": ["Hello!"]} - """ - if client_id not in active_clients: - return jsonify({"error": f"Client {client_id} not found"}), 404 - - # if command_name not in ["join_server", "leave_server", "set_status", "run_task"]: # Basic validation - # return jsonify({"error": f"Unknown command: {command_name}"}), 400 - - try: - request_data = await request.get_json() - args = request_data.get("args", []) - if not isinstance(args, list): - return jsonify({"error": "'args' must be a list"}), 400 - - # Send the command asynchronously - await send_command_to_client(client_id, command_name, *args) - - return jsonify({"status": "command sent", "client_id": client_id, "command": command_name}), 200 - - except Exception as e: - return jsonify({"error": f"Failed to send command: {e}"}), 500 - # --- Main Execution --- if __name__ == "__main__": # Quart's app.run() will start the asyncio event loop and manage it.