From 73b47961d6c8189cc4f32a465a710e12c5a4a49d Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 01:35:27 +0000 Subject: [PATCH 01/20] Add unpair client to API --- src/moonlight-server/api/api.hpp | 7 ++++++- src/moonlight-server/api/endpoints.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 1270e3a6..668b251b 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -17,6 +17,11 @@ struct PairRequest { rfl::Description<"The PIN created by the remote Moonlight client", std::string> pin; }; +struct UnpairClientRequest { + std::string client_id; + rfl::Description<"The client ID to unpair", std::string> client_id; +}; + struct GenericSuccessResponse { bool success = true; }; @@ -114,7 +119,7 @@ class UnixSocketServer { void endpoint_Apps(const HTTPRequest &req, std::shared_ptr socket); void endpoint_AddApp(const HTTPRequest &req, std::shared_ptr socket); void endpoint_RemoveApp(const HTTPRequest &req, std::shared_ptr socket); - + void endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket); void endpoint_StreamSessions(const HTTPRequest &req, std::shared_ptr socket); void endpoint_StreamSessionAdd(const HTTPRequest &req, std::shared_ptr socket); void endpoint_StreamSessionStart(const HTTPRequest &req, std::shared_ptr socket); diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index d9cb6b75..a390ee2e 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -51,6 +51,32 @@ void UnixSocketServer::endpoint_PairedClients(const HTTPRequest &req, std::share send_http(socket, 200, rfl::json::write(res)); } +void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket) { + try { + auto payload = rfl::json::read(req.body); + if (!payload) { + auto res = GenericErrorResponse{.error = "Invalid request format"}; + send_http(socket, 400, rfl::json::write(res)); + return; + } + + auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload->client_id)); + if (!client) { + auto res = GenericErrorResponse{.error = "Client not found"}; + send_http(socket, 404, rfl::json::write(res)); + return; + } + + state::unpair(this->state_->app_state->config, *client); + + auto res = GenericSuccessResponse{.success = true}; + send_http(socket, 200, rfl::json::write(res)); + } catch (const std::exception &e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 500, rfl::json::write(res)); + } +} + void UnixSocketServer::endpoint_Apps(const HTTPRequest &req, std::shared_ptr socket) { auto res = AppListResponse{.success = true}; auto apps = state_->app_state->config->apps->load(); From 3c15b3d17cde7330d43f096117a51e0b62d7b859 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 02:21:48 +0000 Subject: [PATCH 02/20] FIX: Remove duplicate field definition --- src/moonlight-server/api/api.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 668b251b..326d791c 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -18,7 +18,6 @@ struct PairRequest { }; struct UnpairClientRequest { - std::string client_id; rfl::Description<"The client ID to unpair", std::string> client_id; }; From 6b2d470cd38bf2a56288ecb3cdeceb9a5a213da7 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 02:54:54 +0000 Subject: [PATCH 03/20] refactor: improve error handling in UnpairClient endpoint --- src/moonlight-server/api/endpoints.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index a390ee2e..7a0a277b 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -53,14 +53,15 @@ void UnixSocketServer::endpoint_PairedClients(const HTTPRequest &req, std::share void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket) { try { - auto payload = rfl::json::read(req.body); - if (!payload) { + auto payload_result = rfl::json::read(req.body); + if (!payload_result) { auto res = GenericErrorResponse{.error = "Invalid request format"}; send_http(socket, 400, rfl::json::write(res)); return; } - auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload->client_id)); + const auto& payload = payload_result.value(); // Unwrap the Result + auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload.client_id)); if (!client) { auto res = GenericErrorResponse{.error = "Client not found"}; send_http(socket, 404, rfl::json::write(res)); From ddc5d51f921a042f5177ed27f8de13e285f7597a Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 06:20:39 +0000 Subject: [PATCH 04/20] fix: update client ID retrieval in UnpairClient endpoint to use optional payload --- src/moonlight-server/api/endpoints.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index 7a0a277b..9f8e1344 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -61,7 +61,7 @@ void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared } const auto& payload = payload_result.value(); // Unwrap the Result - auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload.client_id)); + auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload.client_id.get())); if (!client) { auto res = GenericErrorResponse{.error = "Client not found"}; send_http(socket, 404, rfl::json::write(res)); From d3ac8672b0dd7cf351bb01e9ee5a8fb1951f8979 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 07:24:53 +0000 Subject: [PATCH 05/20] feat: add Unpair Client endpoint to the API with request and response schema definitions --- src/moonlight-server/api/unix_socket_server.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/moonlight-server/api/unix_socket_server.cpp b/src/moonlight-server/api/unix_socket_server.cpp index f2e0c95e..e1ee48e0 100644 --- a/src/moonlight-server/api/unix_socket_server.cpp +++ b/src/moonlight-server/api/unix_socket_server.cpp @@ -56,6 +56,16 @@ UnixSocketServer::UnixSocketServer(boost::asio::io_context &io_context, .handler = [this](auto req, auto socket) { endpoint_Pair(req, socket); }, }); + state_->http.add(HTTPMethod::POST, + "/api/v1/unpair/client", + { + .summary = "Unpair a client", + .request_description = APIDescription{.json_schema = rfl::json::to_schema()}, + .response_description = {{200, {.json_schema = rfl::json::to_schema()}}, + {500, {.json_schema = rfl::json::to_schema()}}}, + .handler = [this](auto req, auto socket) { endpoint_UnpairClient(req, socket); }, + }); + state_->http.add(HTTPMethod::GET, "/api/v1/clients", { From c28b2e8d1743dc674cb71a595483dd08cf3690cd Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 08:40:49 +0000 Subject: [PATCH 06/20] Updating Spec so its referenced in the doc --- docs/modules/dev/partials/spec.json | 2167 ++++++++++++++------------- 1 file changed, 1109 insertions(+), 1058 deletions(-) diff --git a/docs/modules/dev/partials/spec.json b/docs/modules/dev/partials/spec.json index 68c16c63..f914646a 100644 --- a/docs/modules/dev/partials/spec.json +++ b/docs/modules/dev/partials/spec.json @@ -1,1131 +1,1182 @@ { "openapi": "3.1.0", "info": { - "title": "Wolf API", - "description": "API for the Wolf server", - "version": "0.1" + "title": "Wolf API", + "description": "API for the Wolf server", + "version": "0.1" }, "servers": [ - { - "url": "http://localhost/", - "description": "Local development server" - } + { + "url": "http://localhost/", + "description": "Local development server" + } ], "paths": { - "/api/v1/apps": { - "get": { - "summary": "Get all apps", - "description": "This endpoint returns a list of all apps.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__AppListResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/clients": { - "get": { - "summary": "Get paired clients", - "description": "This endpoint returns a list of all paired clients.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PairedClientsResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/events": { - "get": { - "summary": "Subscribe to events", - "description": "This endpoint allows clients to subscribe to events using SSE", - "responses": {} - } - }, - "/api/v1/openapi-schema": { - "get": { - "summary": "Return this OpenAPI schema as JSON", - "description": "", - "responses": {} - } - }, - "/api/v1/pair/pending": { - "get": { - "summary": "Get pending pair requests", - "description": "This endpoint returns a list of Moonlight clients that are currently waiting to be paired.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PendingPairRequestsResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions": { - "get": { - "summary": "Get all stream sessions", - "description": "This endpoint returns a list of all active stream sessions.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionListResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/apps/add": { - "post": { - "summary": "Add an app", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/apps/delete": { - "post": { - "summary": "Remove an app", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__AppDeleteRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/pair/client": { - "post": { - "summary": "Pair a client", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PairRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/runners/start": { - "post": { - "summary": "Start a runner in a given session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__RunnerStartRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions/add": { - "post": { - "summary": "Create a new stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionCreated" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions/input": { - "post": { - "summary": "Handle input for a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionHandleInputRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions/pause": { - "post": { - "summary": "Pause a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionPauseRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions/start": { - "post": { - "summary": "Start a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionStartRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - }, - "/api/v1/sessions/stop": { - "post": { - "summary": "Stop a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionStopRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } - } - } - } - }, - "components": { - "schemas": { - "rfl__Reflector_wolf__core__events__App___ReflType": { - "type": "object", - "properties": { - "av1_gst_pipeline": { - "type": "string" - }, - "h264_gst_pipeline": { - "type": "string" - }, - "hevc_gst_pipeline": { - "type": "string" - }, - "icon_png_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" + "/api/v1/apps": { + "get": { + "summary": "Get all apps", + "description": "This endpoint returns a list of all apps.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__AppListResponse" + } + } + }, + "description": "" + } } - ] - }, - "id": { - "type": "string" - }, - "opus_gst_pipeline": { - "type": "string" - }, - "render_node": { - "type": "string" - }, - "runner": { - "anyOf": [ - { - "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" - } - ] - }, - "start_audio_server": { - "type": "boolean" - }, - "start_virtual_compositor": { - "type": "boolean" - }, - "support_hdr": { - "type": "boolean" - }, - "title": { - "type": "string" - } - }, - "required": [ - "av1_gst_pipeline", - "h264_gst_pipeline", - "hevc_gst_pipeline", - "id", - "opus_gst_pipeline", - "render_node", - "runner", - "start_audio_server", - "start_virtual_compositor", - "support_hdr", - "title" - ] - }, - "rfl__Reflector_wolf__core__events__StreamSession___ReflType": { - "type": "object", - "properties": { - "app_id": { - "type": "string" - }, - "audio_channel_count": { - "type": "integer" - }, - "client_id": { - "type": "string" - }, - "client_ip": { - "type": "string" - }, - "client_settings": { - "$ref": "#/components/schemas/wolf__config__ClientSettings" - }, - "video_height": { - "type": "integer" - }, - "video_refresh_rate": { - "type": "integer" - }, - "video_width": { - "type": "integer" - } - }, - "required": [ - "app_id", - "audio_channel_count", - "client_id", - "client_ip", - "client_settings", - "video_height", - "video_refresh_rate", - "video_width" - ] - }, - "wolf__api__AppDeleteRequest": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ] - }, - "wolf__api__AppListResponse": { - "type": "object", - "properties": { - "apps": { - "type": "array", - "items": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" - } - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "apps", - "success" - ] - }, - "wolf__api__GenericErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "success": { - "type": "boolean" } - }, - "required": [ - "error", - "success" - ] }, - "wolf__api__GenericSuccessResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean" + "/api/v1/clients": { + "get": { + "summary": "Get paired clients", + "description": "This endpoint returns a list of all paired clients.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PairedClientsResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "success" - ] }, - "wolf__api__PairRequest": { - "type": "object", - "properties": { - "pair_secret": { - "type": "string" - }, - "pin": { - "type": "string", - "description": "The PIN created by the remote Moonlight client" + "/api/v1/events": { + "get": { + "summary": "Subscribe to events", + "description": "This endpoint allows clients to subscribe to events using SSE", + "responses": {} } - }, - "required": [ - "pair_secret", - "pin" - ] }, - "wolf__api__PairedClient": { - "type": "object", - "properties": { - "app_state_folder": { - "type": "string" - }, - "client_id": { - "type": "integer" + "/api/v1/openapi-schema": { + "get": { + "summary": "Return this OpenAPI schema as JSON", + "description": "", + "responses": {} } - }, - "required": [ - "app_state_folder", - "client_id" - ] }, - "wolf__api__PairedClientsResponse": { - "type": "object", - "properties": { - "clients": { - "type": "array", - "items": { - "$ref": "#/components/schemas/wolf__api__PairedClient" - } - }, - "success": { - "type": "boolean" + "/api/v1/pair/pending": { + "get": { + "summary": "Get pending pair requests", + "description": "This endpoint returns a list of Moonlight clients that are currently waiting to be paired.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PendingPairRequestsResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "clients", - "success" - ] }, - "wolf__api__PendingPairRequestsResponse": { - "type": "object", - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/wolf__api__PairRequest" - } - }, - "success": { - "type": "boolean" + "/api/v1/sessions": { + "get": { + "summary": "Get all stream sessions", + "description": "This endpoint returns a list of all active stream sessions.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionListResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "requests", - "success" - ] }, - "wolf__api__RunnerStartRequest": { - "type": "object", - "properties": { - "runner": { - "anyOf": [ - { - "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" + "/api/v1/apps/add": { + "post": { + "summary": "Add an app", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" + } + } + }, + "description": "", + "required": true }, - { - "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } } - ] - }, - "session_id": { - "type": "string" - }, - "stop_stream_when_over": { - "type": "boolean" } - }, - "required": [ - "runner", - "session_id", - "stop_stream_when_over" - ] }, - "wolf__api__StreamSessionCreated": { - "type": "object", - "properties": { - "session_id": { - "type": "string" - }, - "success": { - "type": "boolean" + "/api/v1/apps/delete": { + "post": { + "summary": "Remove an app", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__AppDeleteRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "session_id", - "success" - ] }, - "wolf__api__StreamSessionHandleInputRequest": { - "type": "object", - "properties": { - "input_packet_hex": { - "type": "string", - "description": "A HEX encoded Moonlight input packet, for the full format see: games-on-whales.github.io/wolf/stable/protocols/input-data.html" - }, - "session_id": { - "type": "string" + "/api/v1/pair/client": { + "post": { + "summary": "Pair a client", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PairRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "input_packet_hex", - "session_id" - ] }, - "wolf__api__StreamSessionListResponse": { - "type": "object", - "properties": { - "sessions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" - } - }, - "success": { - "type": "boolean" + "/api/v1/runners/start": { + "post": { + "summary": "Start a runner in a given session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__RunnerStartRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "sessions", - "success" - ] }, - "wolf__api__StreamSessionPauseRequest": { - "type": "object", - "properties": { - "session_id": { - "type": "string" + "/api/v1/sessions/add": { + "post": { + "summary": "Create a new stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionCreated" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "session_id" - ] }, - "wolf__api__StreamSessionStartRequest": { - "type": "object", - "properties": { - "audio_session": { - "$ref": "#/components/schemas/wolf__core__events__AudioSession" - }, - "session_id": { - "type": "string" - }, - "video_session": { - "$ref": "#/components/schemas/wolf__core__events__VideoSession" + "/api/v1/sessions/input": { + "post": { + "summary": "Handle input for a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionHandleInputRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "audio_session", - "session_id", - "video_session" - ] }, - "wolf__api__StreamSessionStopRequest": { - "type": "object", - "properties": { - "session_id": { - "type": "string" + "/api/v1/sessions/pause": { + "post": { + "summary": "Pause a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionPauseRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "session_id" - ] }, - "wolf__config__AppCMD__tagged": { - "type": "object", - "properties": { - "run_cmd": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "process" - ] + "/api/v1/sessions/start": { + "post": { + "summary": "Start a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionStartRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "run_cmd", - "type" - ] }, - "wolf__config__AppChildSession__tagged": { - "type": "object", - "properties": { - "parent_session_id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "child_session" - ] + "/api/v1/sessions/stop": { + "post": { + "summary": "Stop a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionStopRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } } - }, - "required": [ - "parent_session_id", - "type" - ] }, - "wolf__config__AppDocker__tagged": { - "type": "object", - "properties": { - "base_create_json": { - "anyOf": [ - { - "type": "string" + "/api/v1/unpair/client": { + "post": { + "summary": "Unpair a client", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__UnpairClientRequest" + } + } + }, + "description": "", + "required": true }, - { - "type": "null" + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } } - ] - }, - "devices": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "image": { - "type": "string" - }, - "mounts": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "docker" - ] } - }, - "required": [ - "devices", - "env", - "image", - "mounts", - "name", - "ports", - "type" - ] - }, - "wolf__config__ClientSettings": { - "type": "object", - "properties": { - "controllers_override": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "XBOX", - "PS", - "NINTENDO", - "AUTO" + } + }, + "components": { + "schemas": { + "rfl__Reflector_wolf__core__events__App___ReflType": { + "type": "object", + "properties": { + "av1_gst_pipeline": { + "type": "string" + }, + "h264_gst_pipeline": { + "type": "string" + }, + "hevc_gst_pipeline": { + "type": "string" + }, + "icon_png_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "type": "string" + }, + "opus_gst_pipeline": { + "type": "string" + }, + "render_node": { + "type": "string" + }, + "runner": { + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + } + ] + }, + "start_audio_server": { + "type": "boolean" + }, + "start_virtual_compositor": { + "type": "boolean" + }, + "support_hdr": { + "type": "boolean" + }, + "title": { + "type": "string" + } + }, + "required": [ + "av1_gst_pipeline", + "h264_gst_pipeline", + "hevc_gst_pipeline", + "id", + "opus_gst_pipeline", + "render_node", + "runner", + "start_audio_server", + "start_virtual_compositor", + "support_hdr", + "title" ] - } - }, - "h_scroll_acceleration": { - "type": "number" - }, - "mouse_acceleration": { - "type": "number" - }, - "run_gid": { - "type": "integer" - }, - "run_uid": { - "type": "integer" }, - "v_scroll_acceleration": { - "type": "number" - } - }, - "required": [ - "controllers_override", - "h_scroll_acceleration", - "mouse_acceleration", - "run_gid", - "run_uid", - "v_scroll_acceleration" - ] - }, - "wolf__core__audio__AudioMode": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer" - }, - "channels": { - "type": "integer" - }, - "coupled_streams": { - "type": "integer" - }, - "sample_rate": { - "type": "integer" - }, - "speakers": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "FRONT_LEFT", - "FRONT_RIGHT", - "FRONT_CENTER", - "LOW_FREQUENCY", - "BACK_LEFT", - "BACK_RIGHT", - "SIDE_LEFT", - "SIDE_RIGHT", - "MAX_SPEAKERS" + "rfl__Reflector_wolf__core__events__StreamSession___ReflType": { + "type": "object", + "properties": { + "app_id": { + "type": "string" + }, + "audio_channel_count": { + "type": "integer" + }, + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "client_settings": { + "$ref": "#/components/schemas/wolf__config__ClientSettings" + }, + "video_height": { + "type": "integer" + }, + "video_refresh_rate": { + "type": "integer" + }, + "video_width": { + "type": "integer" + } + }, + "required": [ + "app_id", + "audio_channel_count", + "client_id", + "client_ip", + "client_settings", + "video_height", + "video_refresh_rate", + "video_width" ] - } }, - "streams": { - "type": "integer" - } - }, - "required": [ - "bitrate", - "channels", - "coupled_streams", - "sample_rate", - "speakers", - "streams" - ] - }, - "wolf__core__events__AudioSession": { - "type": "object", - "properties": { - "aes_iv": { - "type": "string" - }, - "aes_key": { - "type": "string" - }, - "audio_mode": { - "$ref": "#/components/schemas/wolf__core__audio__AudioMode" + "wolf__api__AppDeleteRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] }, - "client_ip": { - "type": "string" + "wolf__api__AppListResponse": { + "type": "object", + "properties": { + "apps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "apps", + "success" + ] }, - "encrypt_audio": { - "type": "boolean" + "wolf__api__GenericErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "error", + "success" + ] }, - "gst_pipeline": { - "type": "string" + "wolf__api__GenericSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] }, - "packet_duration": { - "type": "integer" + "wolf__api__PairRequest": { + "type": "object", + "properties": { + "pair_secret": { + "type": "string" + }, + "pin": { + "type": "string", + "description": "The PIN created by the remote Moonlight client" + } + }, + "required": [ + "pair_secret", + "pin" + ] }, - "port": { - "type": "integer" + "wolf__api__PairedClient": { + "type": "object", + "properties": { + "app_state_folder": { + "type": "string" + }, + "client_id": { + "type": "integer" + } + }, + "required": [ + "app_state_folder", + "client_id" + ] }, - "session_id": { - "type": "integer" + "wolf__api__PairedClientsResponse": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/wolf__api__PairedClient" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "clients", + "success" + ] }, - "wait_for_ping": { - "type": "boolean" - } - }, - "required": [ - "aes_iv", - "aes_key", - "audio_mode", - "client_ip", - "encrypt_audio", - "gst_pipeline", - "packet_duration", - "port", - "session_id", - "wait_for_ping" - ] - }, - "wolf__core__events__VideoSession": { - "type": "object", - "properties": { - "bitrate_kbps": { - "type": "integer" + "wolf__api__PendingPairRequestsResponse": { + "type": "object", + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/wolf__api__PairRequest" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "requests", + "success" + ] }, - "client_ip": { - "type": "string" + "wolf__api__RunnerStartRequest": { + "type": "object", + "properties": { + "runner": { + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + } + ] + }, + "session_id": { + "type": "string" + }, + "stop_stream_when_over": { + "type": "boolean" + } + }, + "required": [ + "runner", + "session_id", + "stop_stream_when_over" + ] }, - "color_range": { - "type": "string", - "enum": [ - "JPEG", - "MPEG" - ] + "wolf__api__StreamSessionCreated": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "session_id", + "success" + ] }, - "color_space": { - "type": "string", - "enum": [ - "BT601", - "BT709", - "BT2020" - ] + "wolf__api__StreamSessionHandleInputRequest": { + "type": "object", + "properties": { + "input_packet_hex": { + "type": "string", + "description": "A HEX encoded Moonlight input packet, for the full format see: games-on-whales.github.io/wolf/stable/protocols/input-data.html" + }, + "session_id": { + "type": "string" + } + }, + "required": [ + "input_packet_hex", + "session_id" + ] }, - "display_mode": { - "$ref": "#/components/schemas/wolf__core__virtual_display__DisplayMode" + "wolf__api__StreamSessionListResponse": { + "type": "object", + "properties": { + "sessions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "sessions", + "success" + ] }, - "fec_percentage": { - "type": "integer" + "wolf__api__StreamSessionPauseRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ] }, - "frames_with_invalid_ref_threshold": { - "type": "integer" + "wolf__api__StreamSessionStartRequest": { + "type": "object", + "properties": { + "audio_session": { + "$ref": "#/components/schemas/wolf__core__events__AudioSession" + }, + "session_id": { + "type": "string" + }, + "video_session": { + "$ref": "#/components/schemas/wolf__core__events__VideoSession" + } + }, + "required": [ + "audio_session", + "session_id", + "video_session" + ] }, - "gst_pipeline": { - "type": "string" + "wolf__api__StreamSessionStopRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ] }, - "min_required_fec_packets": { - "type": "integer" + "wolf__api__UnpairClientRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "description": "The client ID to unpair" + } + }, + "required": [ + "client_id" + ] }, - "packet_size": { - "type": "integer" + "wolf__config__AppCMD__tagged": { + "type": "object", + "properties": { + "run_cmd": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "process" + ] + } + }, + "required": [ + "run_cmd", + "type" + ] }, - "port": { - "type": "integer" + "wolf__config__AppChildSession__tagged": { + "type": "object", + "properties": { + "parent_session_id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "child_session" + ] + } + }, + "required": [ + "parent_session_id", + "type" + ] }, - "session_id": { - "type": "integer" + "wolf__config__AppDocker__tagged": { + "type": "object", + "properties": { + "base_create_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "devices": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "image": { + "type": "string" + }, + "mounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "docker" + ] + } + }, + "required": [ + "devices", + "env", + "image", + "mounts", + "name", + "ports", + "type" + ] }, - "slices_per_frame": { - "type": "integer" + "wolf__config__ClientSettings": { + "type": "object", + "properties": { + "controllers_override": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "XBOX", + "PS", + "NINTENDO", + "AUTO" + ] + } + }, + "h_scroll_acceleration": { + "type": "number" + }, + "mouse_acceleration": { + "type": "number" + }, + "run_gid": { + "type": "integer" + }, + "run_uid": { + "type": "integer" + }, + "v_scroll_acceleration": { + "type": "number" + } + }, + "required": [ + "controllers_override", + "h_scroll_acceleration", + "mouse_acceleration", + "run_gid", + "run_uid", + "v_scroll_acceleration" + ] }, - "timeout_ms": { - "type": "integer" + "wolf__core__audio__AudioMode": { + "type": "object", + "properties": { + "bitrate": { + "type": "integer" + }, + "channels": { + "type": "integer" + }, + "coupled_streams": { + "type": "integer" + }, + "sample_rate": { + "type": "integer" + }, + "speakers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "FRONT_LEFT", + "FRONT_RIGHT", + "FRONT_CENTER", + "LOW_FREQUENCY", + "BACK_LEFT", + "BACK_RIGHT", + "SIDE_LEFT", + "SIDE_RIGHT", + "MAX_SPEAKERS" + ] + } + }, + "streams": { + "type": "integer" + } + }, + "required": [ + "bitrate", + "channels", + "coupled_streams", + "sample_rate", + "speakers", + "streams" + ] }, - "wait_for_ping": { - "type": "boolean" - } - }, - "required": [ - "bitrate_kbps", - "client_ip", - "color_range", - "color_space", - "display_mode", - "fec_percentage", - "frames_with_invalid_ref_threshold", - "gst_pipeline", - "min_required_fec_packets", - "packet_size", - "port", - "session_id", - "slices_per_frame", - "timeout_ms", - "wait_for_ping" - ] - }, - "wolf__core__virtual_display__DisplayMode": { - "type": "object", - "properties": { - "height": { - "type": "integer" + "wolf__core__events__AudioSession": { + "type": "object", + "properties": { + "aes_iv": { + "type": "string" + }, + "aes_key": { + "type": "string" + }, + "audio_mode": { + "$ref": "#/components/schemas/wolf__core__audio__AudioMode" + }, + "client_ip": { + "type": "string" + }, + "encrypt_audio": { + "type": "boolean" + }, + "gst_pipeline": { + "type": "string" + }, + "packet_duration": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "session_id": { + "type": "integer" + }, + "wait_for_ping": { + "type": "boolean" + } + }, + "required": [ + "aes_iv", + "aes_key", + "audio_mode", + "client_ip", + "encrypt_audio", + "gst_pipeline", + "packet_duration", + "port", + "session_id", + "wait_for_ping" + ] }, - "refreshRate": { - "type": "integer" + "wolf__core__events__VideoSession": { + "type": "object", + "properties": { + "bitrate_kbps": { + "type": "integer" + }, + "client_ip": { + "type": "string" + }, + "color_range": { + "type": "string", + "enum": [ + "JPEG", + "MPEG" + ] + }, + "color_space": { + "type": "string", + "enum": [ + "BT601", + "BT709", + "BT2020" + ] + }, + "display_mode": { + "$ref": "#/components/schemas/wolf__core__virtual_display__DisplayMode" + }, + "fec_percentage": { + "type": "integer" + }, + "frames_with_invalid_ref_threshold": { + "type": "integer" + }, + "gst_pipeline": { + "type": "string" + }, + "min_required_fec_packets": { + "type": "integer" + }, + "packet_size": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "session_id": { + "type": "integer" + }, + "slices_per_frame": { + "type": "integer" + }, + "timeout_ms": { + "type": "integer" + }, + "wait_for_ping": { + "type": "boolean" + } + }, + "required": [ + "bitrate_kbps", + "client_ip", + "color_range", + "color_space", + "display_mode", + "fec_percentage", + "frames_with_invalid_ref_threshold", + "gst_pipeline", + "min_required_fec_packets", + "packet_size", + "port", + "session_id", + "slices_per_frame", + "timeout_ms", + "wait_for_ping" + ] }, - "width": { - "type": "integer" + "wolf__core__virtual_display__DisplayMode": { + "type": "object", + "properties": { + "height": { + "type": "integer" + }, + "refreshRate": { + "type": "integer" + }, + "width": { + "type": "integer" + } + }, + "required": [ + "height", + "refreshRate", + "width" + ] } - }, - "required": [ - "height", - "refreshRate", - "width" - ] } - } } -} +} \ No newline at end of file From f3ece3f55b50e922e2c0eb16fe342f8fb657230b Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 23 Jan 2025 08:48:20 +0000 Subject: [PATCH 07/20] fix: setting api doc's to light mode as darkmode is broken currently --- docs/modules/dev/pages/api.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/modules/dev/pages/api.adoc b/docs/modules/dev/pages/api.adoc index f32bdfbf..9d9dc05b 100644 --- a/docs/modules/dev/pages/api.adoc +++ b/docs/modules/dev/pages/api.adoc @@ -64,6 +64,10 @@ curl localhost:8080/api/v1/openapi-schema From 23e35cab08c82b29e317149a0abcf0455b643736 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Sat, 25 Jan 2025 13:47:21 +0000 Subject: [PATCH 08/20] fix: change api endoint PairedClient to output ClientID as string instead of number. Done for consistency and aid in UI development --- src/moonlight-server/api/api.hpp | 2 +- src/moonlight-server/api/endpoints.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 326d791c..fe6f372a 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -36,7 +36,7 @@ struct PendingPairRequestsResponse { }; struct PairedClient { - std::size_t client_id; + std::string client_id; std::string app_state_folder; }; diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index 9f8e1344..15dc658d 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -46,7 +46,7 @@ void UnixSocketServer::endpoint_PairedClients(const HTTPRequest &req, std::share auto clients = state_->app_state->config->paired_clients->load(); for (const auto &client : clients.get()) { res.clients.push_back( - PairedClient{.client_id = state::get_client_id(client), .app_state_folder = client->app_state_folder}); + PairedClient{.client_id = std::to_string(state::get_client_id(client)), .app_state_folder = client->app_state_folder}); } send_http(socket, 200, rfl::json::write(res)); } From b938c0c3914c75829739b2aca5aab78aa27e1719 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Mon, 27 Jan 2025 10:28:49 +0000 Subject: [PATCH 09/20] Add updating of client app_state_folder and settings --- src/moonlight-server/api/api.hpp | 17 +++++ src/moonlight-server/api/endpoints.cpp | 32 ++++++++ .../api/unix_socket_server.cpp | 15 ++++ src/moonlight-server/state/config.hpp | 8 +- src/moonlight-server/state/configTOML.cpp | 73 +++++++++++++++++++ 5 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index fe6f372a..7cf9094a 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -45,6 +45,21 @@ struct PairedClientsResponse { std::vector clients; }; +struct PartialClientSettings { + std::optional run_uid; + std::optional run_gid; + std::optional> controllers_override; + std::optional mouse_acceleration; + std::optional v_scroll_acceleration; + std::optional h_scroll_acceleration; +}; + +struct UpdateClientSettingsRequest { + rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; + rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; + rfl::Description<"Client settings to update (only specified fields will be updated)", PartialClientSettings> settings; +}; + struct AppListResponse { bool success = true; std::vector::ReflType> apps; @@ -128,6 +143,8 @@ class UnixSocketServer { void endpoint_RunnerStart(const HTTPRequest &req, std::shared_ptr socket); + void endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket); + void sse_broadcast(const std::string &payload); void sse_keepalive(const boost::system::error_code &e); diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index 15dc658d..e63763d4 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -306,4 +306,36 @@ void UnixSocketServer::endpoint_RunnerStart(const wolf::api::HTTPRequest &req, s } } +void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket) { + try { + auto payload_result = rfl::json::read(req.body); + if (!payload_result) { + auto res = GenericErrorResponse{.error = "Invalid request format"}; + send_http(socket, 400, rfl::json::write(res)); + return; + } + + const auto& payload = payload_result.value(); + + // Update the client settings + try { + state::update_client_settings( + this->state_->app_state->config, + payload.client_id.get(), + payload.app_state_folder.get(), + payload.settings.get() + ); + + auto res = GenericSuccessResponse{.success = true}; + send_http(socket, 200, rfl::json::write(res)); + } catch (const std::runtime_error& e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 404, rfl::json::write(res)); + } + } catch (const std::exception &e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 500, rfl::json::write(res)); + } +} + } // namespace wolf::api \ No newline at end of file diff --git a/src/moonlight-server/api/unix_socket_server.cpp b/src/moonlight-server/api/unix_socket_server.cpp index e1ee48e0..ad0a48ab 100644 --- a/src/moonlight-server/api/unix_socket_server.cpp +++ b/src/moonlight-server/api/unix_socket_server.cpp @@ -75,6 +75,21 @@ UnixSocketServer::UnixSocketServer(boost::asio::io_context &io_context, .handler = [this](auto req, auto socket) { endpoint_PairedClients(req, socket); }, }); + state_->http.add(HTTPMethod::POST, + "/api/v1/clients/settings", + { + .summary = "Update client settings", + .description = "Update a client's settings including app state folder and client-specific settings", + .request_description = APIDescription{.json_schema = rfl::json::to_schema()}, + .response_description = { + {200, {.json_schema = rfl::json::to_schema()}}, + {400, {.json_schema = rfl::json::to_schema()}}, + {404, {.json_schema = rfl::json::to_schema()}}, + {500, {.json_schema = rfl::json::to_schema()}} + }, + .handler = [this](auto req, auto socket) { endpoint_UpdateClientSettings(req, socket); }, + }); + /** * Apps API */ diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index 97d304bb..1065345a 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -69,7 +69,7 @@ inline std::size_t get_client_id(const PairedClient ¤t_client) { return std::hash{}(current_client.client_cert); } -inline std::optional get_client_by_id(const Config &cfg, std::size_t client_id) { +inline std::optional get_client_by_id(const Config &cfg, const std::string &client_id) { auto paired_clients = cfg.paired_clients->load(); auto search_result = std::find_if(paired_clients->begin(), paired_clients->end(), [client_id](const PairedClient &pair_client) { @@ -138,4 +138,10 @@ static moonlight::control::pkts::CONTROLLER_TYPE get_controller_type(const Contr } return moonlight::control::pkts::CONTROLLER_TYPE::AUTO; } + +std::optional get_client_by_id(const Config &cfg, const std::string &client_id); +void update_client_settings(const Config &cfg, + const std::string &client_id, + const std::string &new_folder, + const wolf::config::ClientSettings &new_settings); } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index fa9335e8..c0024bc2 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -305,4 +305,77 @@ void unpair(const Config &cfg, const PairedClient &client) { rfl::toml::save(cfg.config_source, tml); } +std::optional get_client_by_id(const Config &cfg, const std::string &client_id) { + auto paired_clients = cfg.paired_clients->load(); + auto search_result = std::find_if(paired_clients->begin(), + paired_clients->end(), + [&client_id](const PairedClient &client) { + return client.app_state_folder == client_id; + }); + + if (search_result != paired_clients->end()) { + return *search_result; + } + return std::nullopt; +} + +void update_client_settings(const Config &cfg, + const std::string &client_id, + const std::optional &new_folder, + const PartialClientSettings &settings_update) { + // Get existing client first + auto client = get_client_by_id(cfg, client_id); + if (!client) { + throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); + } + + // Merge new settings with existing ones + auto merged_settings = client->settings; + // Only update fields that are present in the update + if (settings_update.run_uid) merged_settings.run_uid = *settings_update.run_uid; + if (settings_update.run_gid) merged_settings.run_gid = *settings_update.run_gid; + if (settings_update.controllers_override) merged_settings.controllers_override = *settings_update.controllers_override; + if (settings_update.mouse_acceleration) merged_settings.mouse_acceleration = *settings_update.mouse_acceleration; + if (settings_update.v_scroll_acceleration) merged_settings.v_scroll_acceleration = *settings_update.v_scroll_acceleration; + if (settings_update.h_scroll_acceleration) merged_settings.h_scroll_acceleration = *settings_update.h_scroll_acceleration; + + // Update the in-memory config atomically + cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { + return paired_clients + | ranges::views::transform([&](const auto &client) { + if (client->app_state_folder == client_id) { + return immer::box(PairedClient{ + .client_cert = client->client_cert, + .app_state_folder = new_folder.value_or(client->app_state_folder), + .settings = merged_settings + }); + } + return client; + }) + | ranges::to(); + }); + + // Update the TOML file + auto toml_config = rfl::toml::load(cfg.config_source).value(); + + bool found = false; + for (auto &toml_client : toml_config.paired_clients) { + if (toml_client.app_state_folder == client_id) { + if (new_folder) { + toml_client.app_state_folder = *new_folder; + } + toml_client.settings = merged_settings; + found = true; + break; + } + } + + if (!found) { + throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); + } + + // Save back to file + rfl::toml::save(cfg.config_source, toml_config); +} + } // namespace state \ No newline at end of file From 43ebd1c78651f2b9654053b7f5b480e4bf0f9a7e Mon Sep 17 00:00:00 2001 From: salty2011 Date: Mon, 27 Jan 2025 11:06:23 +0000 Subject: [PATCH 10/20] removed duplicate function and corrected logic for looking up client --- src/moonlight-server/state/config.hpp | 3 +++ src/moonlight-server/state/configTOML.cpp | 18 ++---------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index 1065345a..edac2cc4 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -65,6 +65,9 @@ inline std::optional get_client_via_ssl(const Config &cfg, const s return get_client_via_ssl(cfg, x509::cert_from_string(client_cert)); } +/** + * Returns the first PairedClient with the given client_id + */ inline std::size_t get_client_id(const PairedClient ¤t_client) { return std::hash{}(current_client.client_cert); } diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index c0024bc2..696515ac 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -305,20 +305,6 @@ void unpair(const Config &cfg, const PairedClient &client) { rfl::toml::save(cfg.config_source, tml); } -std::optional get_client_by_id(const Config &cfg, const std::string &client_id) { - auto paired_clients = cfg.paired_clients->load(); - auto search_result = std::find_if(paired_clients->begin(), - paired_clients->end(), - [&client_id](const PairedClient &client) { - return client.app_state_folder == client_id; - }); - - if (search_result != paired_clients->end()) { - return *search_result; - } - return std::nullopt; -} - void update_client_settings(const Config &cfg, const std::string &client_id, const std::optional &new_folder, @@ -343,7 +329,7 @@ void update_client_settings(const Config &cfg, cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { return paired_clients | ranges::views::transform([&](const auto &client) { - if (client->app_state_folder == client_id) { + if (state::get_client_id(client) == client_id) { return immer::box(PairedClient{ .client_cert = client->client_cert, .app_state_folder = new_folder.value_or(client->app_state_folder), @@ -360,7 +346,7 @@ void update_client_settings(const Config &cfg, bool found = false; for (auto &toml_client : toml_config.paired_clients) { - if (toml_client.app_state_folder == client_id) { + if (state::get_client_id(toml_client) == client_id) { if (new_folder) { toml_client.app_state_folder = *new_folder; } From 7039c190f5fdf635871606e026a7de2efad9efdd Mon Sep 17 00:00:00 2001 From: salty2011 Date: Wed, 29 Jan 2025 08:41:44 +0000 Subject: [PATCH 11/20] refactor: Move PartialClientSettings to data-structures and update function signature --- src/moonlight-server/api/api.hpp | 9 --------- src/moonlight-server/state/config.hpp | 4 ++-- src/moonlight-server/state/data-structures.hpp | 9 +++++++++ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 7cf9094a..19bae073 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -45,15 +45,6 @@ struct PairedClientsResponse { std::vector clients; }; -struct PartialClientSettings { - std::optional run_uid; - std::optional run_gid; - std::optional> controllers_override; - std::optional mouse_acceleration; - std::optional v_scroll_acceleration; - std::optional h_scroll_acceleration; -}; - struct UpdateClientSettingsRequest { rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index edac2cc4..9cab7e0f 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -145,6 +145,6 @@ static moonlight::control::pkts::CONTROLLER_TYPE get_controller_type(const Contr std::optional get_client_by_id(const Config &cfg, const std::string &client_id); void update_client_settings(const Config &cfg, const std::string &client_id, - const std::string &new_folder, - const wolf::config::ClientSettings &new_settings); + const std::optional &new_folder, + const PartialClientSettings &settings_update); } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index e7bf7014..932bd715 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -226,4 +226,13 @@ const static immer::array DISPLAY_CONFIGURATIONS = {{ {.width = 7680, .height = 4320, .refreshRate = 60}, }}; +struct PartialClientSettings { + std::optional run_uid; + std::optional run_gid; + std::optional> controllers_override; + std::optional mouse_acceleration; + std::optional v_scroll_acceleration; + std::optional h_scroll_acceleration; +}; + } // namespace state \ No newline at end of file From d4653bfe7906aa5f70026a95a42eda97fa3f5237 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Wed, 29 Jan 2025 09:04:57 +0000 Subject: [PATCH 12/20] refactor: Update client lookup and settings update logic with immer::box --- src/moonlight-server/api/api.hpp | 2 +- src/moonlight-server/state/config.hpp | 37 +++++++++++++++-------- src/moonlight-server/state/configTOML.cpp | 9 ++++-- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 19bae073..1d11a46c 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -48,7 +48,7 @@ struct PairedClientsResponse { struct UpdateClientSettingsRequest { rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; - rfl::Description<"Client settings to update (only specified fields will be updated)", PartialClientSettings> settings; + rfl::Description<"Client settings to update (only specified fields will be updated)", state::PartialClientSettings> settings; }; struct AppListResponse { diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index 9cab7e0f..1c8f44f1 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -41,8 +41,10 @@ void unpair(const Config &cfg, const PairedClient &client); inline std::optional get_client_via_ssl(const Config &cfg, x509::x509_ptr client_cert) { auto paired_clients = cfg.paired_clients->load(); auto search_result = - std::find_if(paired_clients->begin(), paired_clients->end(), [&client_cert](const PairedClient &pair_client) { - auto paired_cert = x509::cert_from_string(pair_client.client_cert); + std::find_if(paired_clients->begin(), + paired_clients->end(), + [&client_cert](const immer::box& pair_client) { + auto paired_cert = x509::cert_from_string(pair_client->client_cert); auto verification_error = x509::verification_error(paired_cert, client_cert); if (verification_error) { logs::log(logs::trace, "X509 certificate verification error: {}", verification_error.value()); @@ -52,7 +54,7 @@ inline std::optional get_client_via_ssl(const Config &cfg, x509::x } }); if (search_result != paired_clients->end()) { - return *search_result; + return **search_result; } else { return std::nullopt; } @@ -66,23 +68,32 @@ inline std::optional get_client_via_ssl(const Config &cfg, const s } /** - * Returns the first PairedClient with the given client_id + * Returns the client ID for a given client */ inline std::size_t get_client_id(const PairedClient ¤t_client) { return std::hash{}(current_client.client_cert); } +/** + * Returns the first PairedClient with the given client_id + */ inline std::optional get_client_by_id(const Config &cfg, const std::string &client_id) { - auto paired_clients = cfg.paired_clients->load(); - auto search_result = - std::find_if(paired_clients->begin(), paired_clients->end(), [client_id](const PairedClient &pair_client) { - return get_client_id(pair_client) == client_id; - }); - if (search_result != paired_clients->end()) { - return *search_result; - } else { + auto paired_clients = cfg.paired_clients->load(); + auto client_id_num = std::stoull(client_id); + + auto search_result = std::find_if( + paired_clients->begin(), + paired_clients->end(), + [client_id_num](const immer::box& pair_client) -> bool { + auto id = get_client_id(*pair_client); + return id == client_id_num; + } + ); + + if (search_result != paired_clients->end()) { + return **search_result; + } return std::nullopt; - } } /** diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index 696515ac..d70a7853 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -326,10 +326,12 @@ void update_client_settings(const Config &cfg, if (settings_update.h_scroll_acceleration) merged_settings.h_scroll_acceleration = *settings_update.h_scroll_acceleration; // Update the in-memory config atomically + auto client_id_num = std::stoull(client_id); cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { return paired_clients - | ranges::views::transform([&](const auto &client) { - if (state::get_client_id(client) == client_id) { + | ranges::views::transform([&](const immer::box& client) -> immer::box { + auto id = get_client_id(*client); + if (id == client_id_num) { return immer::box(PairedClient{ .client_cert = client->client_cert, .app_state_folder = new_folder.value_or(client->app_state_folder), @@ -346,7 +348,8 @@ void update_client_settings(const Config &cfg, bool found = false; for (auto &toml_client : toml_config.paired_clients) { - if (state::get_client_id(toml_client) == client_id) { + auto id = get_client_id(toml_client); + if (id == client_id_num) { if (new_folder) { toml_client.app_state_folder = *new_folder; } From 9f231a38b5f128b9b177d4baceb0cd27c3a4421e Mon Sep 17 00:00:00 2001 From: salty2011 Date: Wed, 29 Jan 2025 09:40:04 +0000 Subject: [PATCH 13/20] refactor: Simplify client settings update with direct value extraction --- src/moonlight-server/api/endpoints.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index e63763d4..86e306b6 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -61,7 +61,7 @@ void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared } const auto& payload = payload_result.value(); // Unwrap the Result - auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(payload.client_id.get())); + auto client = state::get_client_by_id(this->state_->app_state->config, payload.client_id); if (!client) { auto res = GenericErrorResponse{.error = "Client not found"}; send_http(socket, 404, rfl::json::write(res)); @@ -152,7 +152,7 @@ void UnixSocketServer::endpoint_StreamSessionAdd(const HTTPRequest &req, std::sh return; } - auto client = state::get_client_by_id(this->state_->app_state->config, std::stoul(ss.client_id)); + auto client = state::get_client_by_id(this->state_->app_state->config, ss.client_id); if (!client) { logs::log(logs::warning, "[API] Invalid client_id: {}", ss.client_id); auto res = GenericErrorResponse{.error = "Invalid client_id"}; @@ -317,13 +317,18 @@ void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std const auto& payload = payload_result.value(); + // Get the values from the Description fields - keep client_id as string + const std::string& client_id = payload.client_id.get(); + const auto& app_state_folder = payload.app_state_folder; + const auto& settings = payload.settings.get(); + // Update the client settings try { state::update_client_settings( this->state_->app_state->config, - payload.client_id.get(), - payload.app_state_folder.get(), - payload.settings.get() + client_id, // Pass as string + app_state_folder, + settings ); auto res = GenericSuccessResponse{.success = true}; From 6603c4bda7928d637d08d0067713f7626dc717e1 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 29 Jan 2025 18:48:51 +0000 Subject: [PATCH 14/20] fix: compilation error, automatic clang format --- src/moonlight-server/api/api.hpp | 3 +- src/moonlight-server/api/endpoints.cpp | 107 ++++++++-------- src/moonlight-server/state/config.hpp | 44 ++++--- src/moonlight-server/state/configTOML.cpp | 119 +++++++++--------- .../state/data-structures.hpp | 12 +- 5 files changed, 141 insertions(+), 144 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 1d11a46c..4ea38b74 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -48,7 +48,8 @@ struct PairedClientsResponse { struct UpdateClientSettingsRequest { rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; - rfl::Description<"Client settings to update (only specified fields will be updated)", state::PartialClientSettings> settings; + rfl::Description<"Client settings to update (only specified fields will be updated)", state::PartialClientSettings> + settings; }; struct AppListResponse { diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index 86e306b6..b036e2bb 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -45,37 +45,37 @@ void UnixSocketServer::endpoint_PairedClients(const HTTPRequest &req, std::share auto res = PairedClientsResponse{.success = true}; auto clients = state_->app_state->config->paired_clients->load(); for (const auto &client : clients.get()) { - res.clients.push_back( - PairedClient{.client_id = std::to_string(state::get_client_id(client)), .app_state_folder = client->app_state_folder}); + res.clients.push_back(PairedClient{.client_id = std::to_string(state::get_client_id(client)), + .app_state_folder = client->app_state_folder}); } send_http(socket, 200, rfl::json::write(res)); } void UnixSocketServer::endpoint_UnpairClient(const HTTPRequest &req, std::shared_ptr socket) { - try { - auto payload_result = rfl::json::read(req.body); - if (!payload_result) { - auto res = GenericErrorResponse{.error = "Invalid request format"}; - send_http(socket, 400, rfl::json::write(res)); - return; - } - - const auto& payload = payload_result.value(); // Unwrap the Result - auto client = state::get_client_by_id(this->state_->app_state->config, payload.client_id); - if (!client) { - auto res = GenericErrorResponse{.error = "Client not found"}; - send_http(socket, 404, rfl::json::write(res)); - return; - } - - state::unpair(this->state_->app_state->config, *client); - - auto res = GenericSuccessResponse{.success = true}; - send_http(socket, 200, rfl::json::write(res)); - } catch (const std::exception &e) { - auto res = GenericErrorResponse{.error = e.what()}; - send_http(socket, 500, rfl::json::write(res)); + try { + auto payload_result = rfl::json::read(req.body); + if (!payload_result) { + auto res = GenericErrorResponse{.error = "Invalid request format"}; + send_http(socket, 400, rfl::json::write(res)); + return; + } + + const auto &payload = payload_result.value(); // Unwrap the Result + auto client = state::get_client_by_id(this->state_->app_state->config, payload.client_id.value()); + if (!client) { + auto res = GenericErrorResponse{.error = "Client not found"}; + send_http(socket, 404, rfl::json::write(res)); + return; } + + state::unpair(this->state_->app_state->config, *client); + + auto res = GenericSuccessResponse{.success = true}; + send_http(socket, 200, rfl::json::write(res)); + } catch (const std::exception &e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 500, rfl::json::write(res)); + } } void UnixSocketServer::endpoint_Apps(const HTTPRequest &req, std::shared_ptr socket) { @@ -307,40 +307,33 @@ void UnixSocketServer::endpoint_RunnerStart(const wolf::api::HTTPRequest &req, s } void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket) { + try { + auto payload_result = rfl::json::read(req.body); + if (!payload_result) { + auto res = GenericErrorResponse{.error = "Invalid request format"}; + send_http(socket, 400, rfl::json::write(res)); + return; + } + + const auto &payload = payload_result.value(); + + // Update the client settings try { - auto payload_result = rfl::json::read(req.body); - if (!payload_result) { - auto res = GenericErrorResponse{.error = "Invalid request format"}; - send_http(socket, 400, rfl::json::write(res)); - return; - } - - const auto& payload = payload_result.value(); - - // Get the values from the Description fields - keep client_id as string - const std::string& client_id = payload.client_id.get(); - const auto& app_state_folder = payload.app_state_folder; - const auto& settings = payload.settings.get(); - - // Update the client settings - try { - state::update_client_settings( - this->state_->app_state->config, - client_id, // Pass as string - app_state_folder, - settings - ); - - auto res = GenericSuccessResponse{.success = true}; - send_http(socket, 200, rfl::json::write(res)); - } catch (const std::runtime_error& e) { - auto res = GenericErrorResponse{.error = e.what()}; - send_http(socket, 404, rfl::json::write(res)); - } - } catch (const std::exception &e) { - auto res = GenericErrorResponse{.error = e.what()}; - send_http(socket, 500, rfl::json::write(res)); + state::update_client_settings(this->state_->app_state->config, + payload.client_id.value(), + payload.app_state_folder.value(), + payload.settings.value()); + + auto res = GenericSuccessResponse{.success = true}; + send_http(socket, 200, rfl::json::write(res)); + } catch (const std::runtime_error &e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 404, rfl::json::write(res)); } + } catch (const std::exception &e) { + auto res = GenericErrorResponse{.error = e.what()}; + send_http(socket, 500, rfl::json::write(res)); + } } } // namespace wolf::api \ No newline at end of file diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index 1c8f44f1..57d276c1 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -40,10 +40,10 @@ void unpair(const Config &cfg, const PairedClient &client); */ inline std::optional get_client_via_ssl(const Config &cfg, x509::x509_ptr client_cert) { auto paired_clients = cfg.paired_clients->load(); - auto search_result = - std::find_if(paired_clients->begin(), - paired_clients->end(), - [&client_cert](const immer::box& pair_client) { + auto search_result = std::find_if( + paired_clients->begin(), + paired_clients->end(), + [&client_cert](const immer::box &pair_client) { auto paired_cert = x509::cert_from_string(pair_client->client_cert); auto verification_error = x509::verification_error(paired_cert, client_cert); if (verification_error) { @@ -78,22 +78,20 @@ inline std::size_t get_client_id(const PairedClient ¤t_client) { * Returns the first PairedClient with the given client_id */ inline std::optional get_client_by_id(const Config &cfg, const std::string &client_id) { - auto paired_clients = cfg.paired_clients->load(); - auto client_id_num = std::stoull(client_id); - - auto search_result = std::find_if( - paired_clients->begin(), - paired_clients->end(), - [client_id_num](const immer::box& pair_client) -> bool { - auto id = get_client_id(*pair_client); - return id == client_id_num; - } - ); + auto paired_clients = cfg.paired_clients->load(); + auto client_id_num = std::stoull(client_id); - if (search_result != paired_clients->end()) { - return **search_result; - } - return std::nullopt; + auto search_result = std::find_if(paired_clients->begin(), + paired_clients->end(), + [client_id_num](const immer::box &pair_client) -> bool { + auto id = get_client_id(*pair_client); + return id == client_id_num; + }); + + if (search_result != paired_clients->end()) { + return **search_result; + } + return std::nullopt; } /** @@ -154,8 +152,8 @@ static moonlight::control::pkts::CONTROLLER_TYPE get_controller_type(const Contr } std::optional get_client_by_id(const Config &cfg, const std::string &client_id); -void update_client_settings(const Config &cfg, - const std::string &client_id, - const std::optional &new_folder, - const PartialClientSettings &settings_update); +void update_client_settings(const Config &cfg, + const std::string &client_id, + const std::optional &new_folder, + const PartialClientSettings &settings_update); } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index d70a7853..6bc026d9 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -305,66 +305,71 @@ void unpair(const Config &cfg, const PairedClient &client) { rfl::toml::save(cfg.config_source, tml); } -void update_client_settings(const Config &cfg, - const std::string &client_id, - const std::optional &new_folder, - const PartialClientSettings &settings_update) { - // Get existing client first - auto client = get_client_by_id(cfg, client_id); - if (!client) { - throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); - } +void update_client_settings(const Config &cfg, + const std::string &client_id, + const std::optional &new_folder, + const PartialClientSettings &settings_update) { + // Get existing client first + auto client = get_client_by_id(cfg, client_id); + if (!client) { + throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); + } - // Merge new settings with existing ones - auto merged_settings = client->settings; - // Only update fields that are present in the update - if (settings_update.run_uid) merged_settings.run_uid = *settings_update.run_uid; - if (settings_update.run_gid) merged_settings.run_gid = *settings_update.run_gid; - if (settings_update.controllers_override) merged_settings.controllers_override = *settings_update.controllers_override; - if (settings_update.mouse_acceleration) merged_settings.mouse_acceleration = *settings_update.mouse_acceleration; - if (settings_update.v_scroll_acceleration) merged_settings.v_scroll_acceleration = *settings_update.v_scroll_acceleration; - if (settings_update.h_scroll_acceleration) merged_settings.h_scroll_acceleration = *settings_update.h_scroll_acceleration; - - // Update the in-memory config atomically - auto client_id_num = std::stoull(client_id); - cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { - return paired_clients - | ranges::views::transform([&](const immer::box& client) -> immer::box { - auto id = get_client_id(*client); - if (id == client_id_num) { - return immer::box(PairedClient{ - .client_cert = client->client_cert, - .app_state_folder = new_folder.value_or(client->app_state_folder), - .settings = merged_settings - }); - } - return client; - }) - | ranges::to(); - }); - - // Update the TOML file - auto toml_config = rfl::toml::load(cfg.config_source).value(); - - bool found = false; - for (auto &toml_client : toml_config.paired_clients) { - auto id = get_client_id(toml_client); - if (id == client_id_num) { - if (new_folder) { - toml_client.app_state_folder = *new_folder; - } - toml_client.settings = merged_settings; - found = true; - break; - } - } - - if (!found) { - throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); + // Merge new settings with existing ones + auto merged_settings = client->settings; + // Only update fields that are present in the update + if (settings_update.run_uid) + merged_settings.run_uid = *settings_update.run_uid; + if (settings_update.run_gid) + merged_settings.run_gid = *settings_update.run_gid; + if (settings_update.controllers_override) + merged_settings.controllers_override = *settings_update.controllers_override; + if (settings_update.mouse_acceleration) + merged_settings.mouse_acceleration = *settings_update.mouse_acceleration; + if (settings_update.v_scroll_acceleration) + merged_settings.v_scroll_acceleration = *settings_update.v_scroll_acceleration; + if (settings_update.h_scroll_acceleration) + merged_settings.h_scroll_acceleration = *settings_update.h_scroll_acceleration; + + // Update the in-memory config atomically + auto client_id_num = std::stoull(client_id); + cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { + return paired_clients | + ranges::views::transform([&](const immer::box &client) -> immer::box { + auto id = get_client_id(*client); + if (id == client_id_num) { + return immer::box( + PairedClient{.client_cert = client->client_cert, + .app_state_folder = new_folder.value_or(client->app_state_folder), + .settings = merged_settings}); + } + return client; + }) | + ranges::to(); + }); + + // Update the TOML file + auto toml_config = rfl::toml::load(cfg.config_source).value(); + + bool found = false; + for (auto &toml_client : toml_config.paired_clients) { + auto id = get_client_id(toml_client); + if (id == client_id_num) { + if (new_folder) { + toml_client.app_state_folder = *new_folder; + } + toml_client.settings = merged_settings; + found = true; + break; } + } + + if (!found) { + throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); + } - // Save back to file - rfl::toml::save(cfg.config_source, toml_config); + // Save back to file + rfl::toml::save(cfg.config_source, toml_config); } } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index 932bd715..5182f1ee 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -227,12 +227,12 @@ const static immer::array DISPLAY_CONFIGURATIONS = {{ }}; struct PartialClientSettings { - std::optional run_uid; - std::optional run_gid; - std::optional> controllers_override; - std::optional mouse_acceleration; - std::optional v_scroll_acceleration; - std::optional h_scroll_acceleration; + std::optional run_uid; + std::optional run_gid; + std::optional> controllers_override; + std::optional mouse_acceleration; + std::optional v_scroll_acceleration; + std::optional h_scroll_acceleration; }; } // namespace state \ No newline at end of file From 2fbd4191e4d0b4493568f293b5d57f9545f436f5 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 29 Jan 2025 19:05:12 +0000 Subject: [PATCH 15/20] fix: refactored update client settings method and API call --- src/moonlight-server/api/api.hpp | 11 ++- src/moonlight-server/api/endpoints.cpp | 56 +++++++------- src/moonlight-server/state/config.hpp | 10 ++- src/moonlight-server/state/configTOML.cpp | 75 +++++-------------- .../state/data-structures.hpp | 10 --- 5 files changed, 64 insertions(+), 98 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 4ea38b74..09cecc1c 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -45,10 +45,19 @@ struct PairedClientsResponse { std::vector clients; }; +struct PartialClientSettings { + std::optional run_uid; + std::optional run_gid; + std::optional> controllers_override; + std::optional mouse_acceleration; + std::optional v_scroll_acceleration; + std::optional h_scroll_acceleration; +}; + struct UpdateClientSettingsRequest { rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; - rfl::Description<"Client settings to update (only specified fields will be updated)", state::PartialClientSettings> + rfl::Description<"Client settings to update (only specified fields will be updated)", PartialClientSettings> settings; }; diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index b036e2bb..110cb92e 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -307,33 +307,39 @@ void UnixSocketServer::endpoint_RunnerStart(const wolf::api::HTTPRequest &req, s } void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std::shared_ptr socket) { - try { - auto payload_result = rfl::json::read(req.body); - if (!payload_result) { - auto res = GenericErrorResponse{.error = "Invalid request format"}; - send_http(socket, 400, rfl::json::write(res)); - return; - } - - const auto &payload = payload_result.value(); - - // Update the client settings - try { - state::update_client_settings(this->state_->app_state->config, - payload.client_id.value(), - payload.app_state_folder.value(), - payload.settings.value()); + auto payload_result = rfl::json::read(req.body); + if (!payload_result) { + auto res = GenericErrorResponse{.error = "Invalid request format"}; + send_http(socket, 400, rfl::json::write(res)); + return; + } - auto res = GenericSuccessResponse{.success = true}; - send_http(socket, 200, rfl::json::write(res)); - } catch (const std::runtime_error &e) { - auto res = GenericErrorResponse{.error = e.what()}; - send_http(socket, 404, rfl::json::write(res)); - } - } catch (const std::exception &e) { - auto res = GenericErrorResponse{.error = e.what()}; - send_http(socket, 500, rfl::json::write(res)); + const auto &payload = payload_result.value(); + auto current_client = state::get_client_by_id(this->state_->app_state->config, payload.client_id.value()); + if (!current_client) { + auto res = GenericErrorResponse{.error = "Client not found"}; + send_http(socket, 404, rfl::json::write(res)); + return; } + + // Edit only the settings that are being passed in the payload + auto current_settings = current_client->settings; + auto new_settings = payload.settings.get(); + auto merged_client = config::PairedClient{ + .app_state_folder = payload.app_state_folder.get().value_or(current_client->app_state_folder), + .settings = config::ClientSettings{ + .run_uid = new_settings.run_gid.value_or(current_settings.run_uid), + .run_gid = new_settings.run_gid.value_or(current_settings.run_gid), + .controllers_override = new_settings.controllers_override.value_or(current_settings.controllers_override), + .mouse_acceleration = new_settings.mouse_acceleration.value_or(current_settings.mouse_acceleration), + .v_scroll_acceleration = new_settings.v_scroll_acceleration.value_or(current_settings.v_scroll_acceleration), + .h_scroll_acceleration = new_settings.h_scroll_acceleration.value_or(current_settings.h_scroll_acceleration), + }}; + + update_client_settings(this->state_->app_state->config, std::stoull(payload.client_id.value()), merged_client); + + auto res = GenericSuccessResponse{.success = true}; + send_http(socket, 200, rfl::json::write(res)); } } // namespace wolf::api \ No newline at end of file diff --git a/src/moonlight-server/state/config.hpp b/src/moonlight-server/state/config.hpp index 57d276c1..e1c31145 100644 --- a/src/moonlight-server/state/config.hpp +++ b/src/moonlight-server/state/config.hpp @@ -152,8 +152,10 @@ static moonlight::control::pkts::CONTROLLER_TYPE get_controller_type(const Contr } std::optional get_client_by_id(const Config &cfg, const std::string &client_id); -void update_client_settings(const Config &cfg, - const std::string &client_id, - const std::optional &new_folder, - const PartialClientSettings &settings_update); + +/** + * Replaces the specified client_id with the updated_client + * Side effects: will save back the configuration to disk + */ +void update_client_settings(const Config &cfg, std::size_t client_id, const PairedClient &updated_client); } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index 6bc026d9..34921e99 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -305,71 +305,30 @@ void unpair(const Config &cfg, const PairedClient &client) { rfl::toml::save(cfg.config_source, tml); } -void update_client_settings(const Config &cfg, - const std::string &client_id, - const std::optional &new_folder, - const PartialClientSettings &settings_update) { - // Get existing client first - auto client = get_client_by_id(cfg, client_id); - if (!client) { - throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); - } +void update_client_settings(const Config &cfg, std::size_t client_id, const PairedClient &updated_client) { - // Merge new settings with existing ones - auto merged_settings = client->settings; - // Only update fields that are present in the update - if (settings_update.run_uid) - merged_settings.run_uid = *settings_update.run_uid; - if (settings_update.run_gid) - merged_settings.run_gid = *settings_update.run_gid; - if (settings_update.controllers_override) - merged_settings.controllers_override = *settings_update.controllers_override; - if (settings_update.mouse_acceleration) - merged_settings.mouse_acceleration = *settings_update.mouse_acceleration; - if (settings_update.v_scroll_acceleration) - merged_settings.v_scroll_acceleration = *settings_update.v_scroll_acceleration; - if (settings_update.h_scroll_acceleration) - merged_settings.h_scroll_acceleration = *settings_update.h_scroll_acceleration; - - // Update the in-memory config atomically - auto client_id_num = std::stoull(client_id); - cfg.paired_clients->update([&](const state::PairedClientList &paired_clients) { - return paired_clients | - ranges::views::transform([&](const immer::box &client) -> immer::box { - auto id = get_client_id(*client); - if (id == client_id_num) { - return immer::box( - PairedClient{.client_cert = client->client_cert, - .app_state_folder = new_folder.value_or(client->app_state_folder), - .settings = merged_settings}); - } - return client; - }) | - ranges::to(); + auto update_client_fn = [&](immer::box client) -> immer::box { + if (get_client_id(client) == client_id) { + return immer::box(updated_client); + } + return client; + }; + + cfg.paired_clients->update([&](const PairedClientList &paired_clients) { + return paired_clients | // + ranges::views::transform(update_client_fn) | // + ranges::to(); }); // Update the TOML file - auto toml_config = rfl::toml::load(cfg.config_source).value(); - - bool found = false; - for (auto &toml_client : toml_config.paired_clients) { - auto id = get_client_id(toml_client); - if (id == client_id_num) { - if (new_folder) { - toml_client.app_state_folder = *new_folder; - } - toml_client.settings = merged_settings; - found = true; - break; - } - } + auto tml = rfl::toml::load(cfg.config_source).value(); - if (!found) { - throw std::runtime_error(fmt::format("Client with id {} not found", client_id)); - } + tml.paired_clients = tml.paired_clients | // + ranges::views::transform(update_client_fn) | // + ranges::to>(); // Save back to file - rfl::toml::save(cfg.config_source, toml_config); + rfl::toml::save(cfg.config_source, tml); } } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index 5182f1ee..ed4e08b4 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -225,14 +225,4 @@ const static immer::array DISPLAY_CONFIGURATIONS = {{ {.width = 7680, .height = 4320, .refreshRate = 90}, {.width = 7680, .height = 4320, .refreshRate = 60}, }}; - -struct PartialClientSettings { - std::optional run_uid; - std::optional run_gid; - std::optional> controllers_override; - std::optional mouse_acceleration; - std::optional v_scroll_acceleration; - std::optional h_scroll_acceleration; -}; - } // namespace state \ No newline at end of file From 4d1655f8ac6a60e29a1e11365279dd2cfb050ac9 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 29 Jan 2025 19:25:55 +0000 Subject: [PATCH 16/20] fix: properly merge updated client, added unit tests for the new methods --- src/moonlight-server/api/endpoints.cpp | 1 + tests/testWolfAPI.cpp | 47 ++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index 110cb92e..f016dbcf 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -326,6 +326,7 @@ void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std auto current_settings = current_client->settings; auto new_settings = payload.settings.get(); auto merged_client = config::PairedClient{ + .client_cert = current_client->client_cert, // Immutable, changing this would mean a new client .app_state_folder = payload.app_state_folder.get().value_or(current_client->app_state_folder), .settings = config::ClientSettings{ .run_uid = new_settings.run_gid.value_or(current_settings.run_uid), diff --git a/tests/testWolfAPI.cpp b/tests/testWolfAPI.cpp index aacdd595..78da2f7b 100644 --- a/tests/testWolfAPI.cpp +++ b/tests/testWolfAPI.cpp @@ -3,10 +3,11 @@ #include #include #include +#include #include -using Catch::Matchers::Equals; using Catch::Matchers::ContainsSubstring; +using Catch::Matchers::Equals; using namespace wolf::api; using curl_ptr = std::unique_ptr; @@ -84,9 +85,14 @@ req(CURL *handle, TEST_CASE("Pair APIs", "[API]") { auto event_bus = std::make_shared(); auto running_sessions = std::make_shared>>(); - auto config = immer::box(state::load_or_default("config.test.toml", event_bus, running_sessions)); + auto config = state::load_or_default("config.test.toml", event_bus, running_sessions); + { // Avoid overriding the test config file (shared across multiple tests) + config.config_source = "config.test.EDITED.toml"; + auto tml = rfl::toml::load("config.test.toml").value(); + rfl::toml::save(config.config_source, tml); + } auto app_state = immer::box(state::AppState{ - .config = config, + .config = immer::box(config), .pairing_cache = std::make_shared>>(), .pairing_atom = std::make_shared>>>(), .event_bus = event_bus, @@ -109,9 +115,10 @@ TEST_CASE("Pair APIs", "[API]") { // Checkout the list of paired clients (there will be one in the test config file) response = req(curl.get(), HTTPMethod::GET, "http://localhost/api/v1/clients"); REQUIRE(response); - REQUIRE_THAT(response->second, - Equals("{\"success\":true,\"clients\":[{\"client_id\":10594003729173467913,\"app_state_folder\":\"some/" - "folder\"}]}")); + REQUIRE_THAT( + response->second, + Equals("{\"success\":true,\"clients\":[{\"client_id\":\"10594003729173467913\",\"app_state_folder\":\"some/" + "folder\"}]}")); auto pair_promise = std::make_shared>(); @@ -135,6 +142,34 @@ TEST_CASE("Pair APIs", "[API]") { REQUIRE(response); REQUIRE_THAT(response->second, Equals("{\"success\":true}")); REQUIRE(pair_promise->get_future().get() == "1234"); + + { // Test out changing client settings + REQUIRE_THAT(app_state->config.get().paired_clients->load().get()[0]->app_state_folder, Equals("some/folder")); + response = req(curl.get(), + HTTPMethod::POST, + "http://localhost/api/v1/clients/settings", + "{\"client_id\":\"10594003729173467913\",\"app_state_folder\":\"OVERRIDDEN\", \"settings\":{}}"); + REQUIRE(response); + REQUIRE_THAT(response->second, Equals("{\"success\":true}")); + REQUIRE_THAT(app_state->config.get().paired_clients->load().get()[0]->app_state_folder, Equals("OVERRIDDEN")); + + // Check back that we've correctly updated the config file + auto tml = rfl::toml::load(config.config_source).value(); + REQUIRE(tml.paired_clients[0].app_state_folder == "OVERRIDDEN"); + } + + { // Test out unpairing + response = req(curl.get(), + HTTPMethod::POST, + "http://localhost/api/v1/unpair/client", + "{\"client_id\":\"10594003729173467913\"}"); + REQUIRE(response); + REQUIRE_THAT(response->second, Equals("{\"success\":true}")); + + // Check back that we've correctly updated the config file + auto tml = rfl::toml::load(config.config_source).value(); + REQUIRE(tml.paired_clients.empty()); + } } TEST_CASE("APPs APIs", "[API]") { From 581f41031579122ca319d79b9cf5321ed2c3ced3 Mon Sep 17 00:00:00 2001 From: salty2011 Date: Thu, 30 Jan 2025 09:00:24 +0000 Subject: [PATCH 17/20] feat: changing client config update to allow settings to be optional. current behavious requires it to be specifcied with blank array --- src/moonlight-server/api/api.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 09cecc1c..4d15235a 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -57,7 +57,7 @@ struct PartialClientSettings { struct UpdateClientSettingsRequest { rfl::Description<"The client ID to identify the client (derived from certificate)", std::string> client_id; rfl::Description<"New app state folder path (optional)", std::optional> app_state_folder; - rfl::Description<"Client settings to update (only specified fields will be updated)", PartialClientSettings> + rfl::Description<"Client settings to update (only specified fields will be updated)", std::optional> settings; }; From 4f25091ef679aa0689eac8f64ffad43779619785 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 31 Jan 2025 16:22:14 +0000 Subject: [PATCH 18/20] fix: compilation error, exposing all client data in the APIs, correctly calling IP instead of PIN in pending requests fixes #166 --- src/moonlight-server/api/api.hpp | 8 +++++++- src/moonlight-server/api/endpoints.cpp | 11 ++++++----- tests/testWolfAPI.cpp | 17 ++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index 4d15235a..7aef1672 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -12,6 +12,11 @@ using namespace wolf::core; void start_server(immer::box app_state); +struct PendingPairClient { + std::string pair_secret; + rfl::Description<"The IP of the remote Moonlight client", std::string> client_ip; +}; + struct PairRequest { std::string pair_secret; rfl::Description<"The PIN created by the remote Moonlight client", std::string> pin; @@ -32,12 +37,13 @@ struct GenericErrorResponse { struct PendingPairRequestsResponse { bool success = true; - std::vector requests; + std::vector requests; }; struct PairedClient { std::string client_id; std::string app_state_folder; + config::ClientSettings settings = {}; }; struct PairedClientsResponse { diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index f016dbcf..72a2363f 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -15,9 +15,9 @@ void UnixSocketServer::endpoint_Events(const HTTPRequest &req, std::shared_ptr socket) { - auto requests = std::vector(); + auto requests = std::vector(); for (auto [secret, pair_request] : *(state_->app_state)->pairing_atom->load()) { - requests.push_back({.pair_secret = secret, .pin = pair_request->client_ip}); + requests.push_back({.pair_secret = secret, .client_ip = pair_request->client_ip}); } send_http(socket, 200, rfl::json::write(PendingPairRequestsResponse{.requests = requests})); } @@ -44,9 +44,10 @@ void UnixSocketServer::endpoint_Pair(const HTTPRequest &req, std::shared_ptr socket) { auto res = PairedClientsResponse{.success = true}; auto clients = state_->app_state->config->paired_clients->load(); - for (const auto &client : clients.get()) { + for (const config::PairedClient &client : clients.get()) { res.clients.push_back(PairedClient{.client_id = std::to_string(state::get_client_id(client)), - .app_state_folder = client->app_state_folder}); + .app_state_folder = client.app_state_folder, + .settings = client.settings}); } send_http(socket, 200, rfl::json::write(res)); } @@ -324,7 +325,7 @@ void UnixSocketServer::endpoint_UpdateClientSettings(const HTTPRequest &req, std // Edit only the settings that are being passed in the payload auto current_settings = current_client->settings; - auto new_settings = payload.settings.get(); + auto new_settings = payload.settings.get().value_or(PartialClientSettings{}); auto merged_client = config::PairedClient{ .client_cert = current_client->client_cert, // Immutable, changing this would mean a new client .app_state_folder = payload.app_state_folder.get().value_or(current_client->app_state_folder), diff --git a/tests/testWolfAPI.cpp b/tests/testWolfAPI.cpp index 78da2f7b..e7876eb5 100644 --- a/tests/testWolfAPI.cpp +++ b/tests/testWolfAPI.cpp @@ -115,10 +115,17 @@ TEST_CASE("Pair APIs", "[API]") { // Checkout the list of paired clients (there will be one in the test config file) response = req(curl.get(), HTTPMethod::GET, "http://localhost/api/v1/clients"); REQUIRE(response); - REQUIRE_THAT( - response->second, - Equals("{\"success\":true,\"clients\":[{\"client_id\":\"10594003729173467913\",\"app_state_folder\":\"some/" - "folder\"}]}")); + REQUIRE_THAT(response->second, + Equals("{\"success\":true,\"clients\":[" + "{\"client_id\":\"10594003729173467913\"," + "\"app_state_folder\":\"some/folder\"," + "\"settings\":{" + "\"run_uid\":1234," + "\"run_gid\":5678," + "\"controllers_override\":[\"PS\"]," + "\"mouse_acceleration\":2.5," + "\"v_scroll_acceleration\":1.5," + "\"h_scroll_acceleration\":10.199999809265137}}]}")); auto pair_promise = std::make_shared>(); @@ -132,7 +139,7 @@ TEST_CASE("Pair APIs", "[API]") { response = req(curl.get(), HTTPMethod::GET, "http://localhost/api/v1/pair/pending"); REQUIRE(response); REQUIRE_THAT(response->second, - Equals("{\"success\":true,\"requests\":[{\"pair_secret\":\"secret\",\"pin\":\"1234\"}]}")); + Equals("{\"success\":true,\"requests\":[{\"pair_secret\":\"secret\",\"client_ip\":\"1234\"}]}")); // Let's complete the pairing process response = req(curl.get(), From bfb7dd5e1a13c36491e3527c096394fed81ee5ed Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 31 Jan 2025 16:45:32 +0000 Subject: [PATCH 19/20] fix: avoid dangling pair requests fixes #167 --- src/moonlight-server/rest/servers.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/moonlight-server/rest/servers.cpp b/src/moonlight-server/rest/servers.cpp index 8562262e..8ebe44e1 100644 --- a/src/moonlight-server/rest/servers.cpp +++ b/src/moonlight-server/rest/servers.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include namespace HTTPServers { @@ -10,6 +11,7 @@ namespace HTTPServers { */ constexpr char const *pin_html = #include "html/pin.include.html" + ; namespace bt = boost::property_tree; @@ -70,10 +72,19 @@ void startServer(HttpServer *server, const immer::box state, in auto pair_handler = state->event_bus->register_handler>( [pairing_atom](const immer::box pair_sig) { - pairing_atom->update([&pair_sig](auto m) { + pairing_atom->update([&pair_sig](const immer::map> &m) { auto secret = crypto::str_to_hex(crypto::random(8)); logs::log(logs::info, "Insert pin at http://{}:47989/pin/#{}", pair_sig->host_ip, secret); - return m.set(secret, pair_sig); + // filter out any other (dangling) pair request from the same client + auto t_map = m.transient(); + for (auto [key, value] : m) { + if (value->client_ip == pair_sig->client_ip) { + t_map.erase(key); + } + } + // insert the new pair request + t_map.set(secret, pair_sig); + return t_map.persistent(); }); }); From 7a68f72f9ff8b1c7616e1d53ee26c7f363c4a9f4 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 31 Jan 2025 17:05:55 +0000 Subject: [PATCH 20/20] docs: updated API spec, removed dark mode --- docs/modules/dev/pages/api.adoc | 14 +- docs/modules/dev/partials/spec.json | 2390 +++++++++++++++------------ 2 files changed, 1301 insertions(+), 1103 deletions(-) diff --git a/docs/modules/dev/pages/api.adoc b/docs/modules/dev/pages/api.adoc index 9d9dc05b..9e5228e9 100644 --- a/docs/modules/dev/pages/api.adoc +++ b/docs/modules/dev/pages/api.adoc @@ -2,7 +2,8 @@ = Wolf API Wolf exposes a REST API that allows you to interact with the platform programmatically. + -The API can be accessed only via UNIX sockets, you can control the exact path by setting the `WOLF_SOCKET_PATH` environment variable. If you want to access the socket from outside the container, you should mount the socket to the host machine, ex: `-e WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock` and `-v /var/run/wolf:/var/run/wolf` will allow you to access the socket from the host machine at `/var/run/wolf/wolf.sock`. +The API can be accessed only via UNIX sockets, you can control the exact path by setting the `WOLF_SOCKET_PATH` environment variable. +If you want to access the socket from outside the container, you should mount the socket to the host machine, ex: `-e WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock` and `-v /var/run/wolf:/var/run/wolf` will allow you to access the socket from the host machine at `/var/run/wolf/wolf.sock`. You can test out the API using the `curl` command, for example, to get the OpenAPI specification you can run: @@ -64,11 +65,18 @@ curl localhost:8080/api/v1/openapi-schema + + ++++ \ No newline at end of file diff --git a/docs/modules/dev/partials/spec.json b/docs/modules/dev/partials/spec.json index f914646a..8b0347ec 100644 --- a/docs/modules/dev/partials/spec.json +++ b/docs/modules/dev/partials/spec.json @@ -1,1182 +1,1372 @@ { "openapi": "3.1.0", "info": { - "title": "Wolf API", - "description": "API for the Wolf server", - "version": "0.1" + "title": "Wolf API", + "description": "API for the Wolf server", + "version": "0.1" }, "servers": [ - { - "url": "http://localhost/", - "description": "Local development server" - } + { + "url": "http://localhost/", + "description": "Local development server" + } ], "paths": { - "/api/v1/apps": { - "get": { - "summary": "Get all apps", - "description": "This endpoint returns a list of all apps.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__AppListResponse" - } - } - }, - "description": "" - } + "/api/v1/apps": { + "get": { + "summary": "Get all apps", + "description": "This endpoint returns a list of all apps.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__AppListResponse" + } } + }, + "description": "" } - }, - "/api/v1/clients": { - "get": { - "summary": "Get paired clients", - "description": "This endpoint returns a list of all paired clients.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PairedClientsResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/clients": { + "get": { + "summary": "Get paired clients", + "description": "This endpoint returns a list of all paired clients.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PairedClientsResponse" + } } + }, + "description": "" } - }, - "/api/v1/events": { - "get": { - "summary": "Subscribe to events", - "description": "This endpoint allows clients to subscribe to events using SSE", - "responses": {} - } - }, - "/api/v1/openapi-schema": { - "get": { - "summary": "Return this OpenAPI schema as JSON", - "description": "", - "responses": {} + } + } + }, + "/api/v1/events": { + "get": { + "summary": "Subscribe to events", + "description": "This endpoint allows clients to subscribe to events using SSE", + "responses": {} + } + }, + "/api/v1/openapi-schema": { + "get": { + "summary": "Return this OpenAPI schema as JSON", + "description": "", + "responses": {} + } + }, + "/api/v1/pair/pending": { + "get": { + "summary": "Get pending pair requests", + "description": "This endpoint returns a list of Moonlight clients that are currently waiting to be paired.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PendingPairRequestsResponse" + } + } + }, + "description": "" } - }, - "/api/v1/pair/pending": { - "get": { - "summary": "Get pending pair requests", - "description": "This endpoint returns a list of Moonlight clients that are currently waiting to be paired.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PendingPairRequestsResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions": { + "get": { + "summary": "Get all stream sessions", + "description": "This endpoint returns a list of all active stream sessions.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionListResponse" + } } + }, + "description": "" } - }, - "/api/v1/sessions": { - "get": { - "summary": "Get all stream sessions", - "description": "This endpoint returns a list of all active stream sessions.", - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionListResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/apps/add": { + "post": { + "summary": "Add an app", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/apps/add": { - "post": { - "summary": "Add an app", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/apps/delete": { + "post": { + "summary": "Remove an app", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__AppDeleteRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } } + }, + "description": "" } - }, - "/api/v1/apps/delete": { - "post": { - "summary": "Remove an app", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__AppDeleteRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/clients/settings": { + "post": { + "summary": "Update client settings", + "description": "Update a client's settings including app state folder and client-specific settings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__UpdateClientSettingsRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/pair/client": { - "post": { - "summary": "Pair a client", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__PairRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/pair/client": { + "post": { + "summary": "Pair a client", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__PairRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/runners/start": { - "post": { - "summary": "Start a runner in a given session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__RunnerStartRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/runners/start": { + "post": { + "summary": "Start a runner in a given session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__RunnerStartRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } } + }, + "description": "" } - }, - "/api/v1/sessions/add": { - "post": { - "summary": "Create a new stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionCreated" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions/add": { + "post": { + "summary": "Create a new stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionCreated" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } } + }, + "description": "" } - }, - "/api/v1/sessions/input": { - "post": { - "summary": "Handle input for a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionHandleInputRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions/input": { + "post": { + "summary": "Handle input for a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionHandleInputRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } } + }, + "description": "" } - }, - "/api/v1/sessions/pause": { - "post": { - "summary": "Pause a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionPauseRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions/pause": { + "post": { + "summary": "Pause a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionPauseRequest" } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/sessions/start": { - "post": { - "summary": "Start a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionStartRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions/start": { + "post": { + "summary": "Start a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionStartRequest" } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/sessions/stop": { - "post": { - "summary": "Stop a stream session", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__StreamSessionStopRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/sessions/stop": { + "post": { + "summary": "Stop a stream session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__StreamSessionStopRequest" } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } - }, - "/api/v1/unpair/client": { - "post": { - "summary": "Unpair a client", - "description": "", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__UnpairClientRequest" - } - } - }, - "description": "", - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" - } - } - }, - "description": "" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" - } - } - }, - "description": "" - } + } + } + }, + "/api/v1/unpair/client": { + "post": { + "summary": "Unpair a client", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__UnpairClientRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" } + } } + } }, "components": { - "schemas": { - "rfl__Reflector_wolf__core__events__App___ReflType": { - "type": "object", - "properties": { - "av1_gst_pipeline": { - "type": "string" - }, - "h264_gst_pipeline": { - "type": "string" - }, - "hevc_gst_pipeline": { - "type": "string" - }, - "icon_png_path": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "string" - }, - "opus_gst_pipeline": { - "type": "string" - }, - "render_node": { - "type": "string" - }, - "runner": { - "anyOf": [ - { - "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" - } - ] - }, - "start_audio_server": { - "type": "boolean" - }, - "start_virtual_compositor": { - "type": "boolean" - }, - "support_hdr": { - "type": "boolean" - }, - "title": { - "type": "string" - } - }, - "required": [ - "av1_gst_pipeline", - "h264_gst_pipeline", - "hevc_gst_pipeline", - "id", - "opus_gst_pipeline", - "render_node", - "runner", - "start_audio_server", - "start_virtual_compositor", - "support_hdr", - "title" - ] + "schemas": { + "rfl__Reflector_wolf__core__events__App___ReflType": { + "type": "object", + "properties": { + "av1_gst_pipeline": { + "type": "string" }, - "rfl__Reflector_wolf__core__events__StreamSession___ReflType": { - "type": "object", - "properties": { - "app_id": { - "type": "string" - }, - "audio_channel_count": { - "type": "integer" - }, - "client_id": { - "type": "string" - }, - "client_ip": { - "type": "string" - }, - "client_settings": { - "$ref": "#/components/schemas/wolf__config__ClientSettings" - }, - "video_height": { - "type": "integer" - }, - "video_refresh_rate": { - "type": "integer" - }, - "video_width": { - "type": "integer" - } - }, - "required": [ - "app_id", - "audio_channel_count", - "client_id", - "client_ip", - "client_settings", - "video_height", - "video_refresh_rate", - "video_width" - ] + "h264_gst_pipeline": { + "type": "string" }, - "wolf__api__AppDeleteRequest": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ] + "hevc_gst_pipeline": { + "type": "string" }, - "wolf__api__AppListResponse": { - "type": "object", - "properties": { - "apps": { - "type": "array", - "items": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" - } - }, - "success": { - "type": "boolean" - } + "icon_png_path": { + "anyOf": [ + { + "type": "string" }, - "required": [ - "apps", - "success" - ] + { + "type": "null" + } + ] }, - "wolf__api__GenericErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "error", - "success" - ] + "id": { + "type": "string" }, - "wolf__api__GenericSuccessResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - }, - "required": [ - "success" - ] + "opus_gst_pipeline": { + "type": "string" }, - "wolf__api__PairRequest": { - "type": "object", - "properties": { - "pair_secret": { - "type": "string" - }, - "pin": { - "type": "string", - "description": "The PIN created by the remote Moonlight client" - } - }, - "required": [ - "pair_secret", - "pin" - ] + "render_node": { + "type": "string" }, - "wolf__api__PairedClient": { - "type": "object", - "properties": { - "app_state_folder": { - "type": "string" - }, - "client_id": { - "type": "integer" - } + "runner": { + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" }, - "required": [ - "app_state_folder", - "client_id" - ] - }, - "wolf__api__PairedClientsResponse": { - "type": "object", - "properties": { - "clients": { - "type": "array", - "items": { - "$ref": "#/components/schemas/wolf__api__PairedClient" - } - }, - "success": { - "type": "boolean" - } + { + "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" }, - "required": [ - "clients", - "success" - ] + { + "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + } + ] }, - "wolf__api__PendingPairRequestsResponse": { - "type": "object", - "properties": { - "requests": { - "type": "array", - "items": { - "$ref": "#/components/schemas/wolf__api__PairRequest" - } - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "requests", - "success" - ] + "start_audio_server": { + "type": "boolean" }, - "wolf__api__RunnerStartRequest": { - "type": "object", - "properties": { - "runner": { - "anyOf": [ - { - "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" - }, - { - "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" - } - ] - }, - "session_id": { - "type": "string" - }, - "stop_stream_when_over": { - "type": "boolean" - } - }, - "required": [ - "runner", - "session_id", - "stop_stream_when_over" - ] + "start_virtual_compositor": { + "type": "boolean" }, - "wolf__api__StreamSessionCreated": { - "type": "object", - "properties": { - "session_id": { - "type": "string" - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "session_id", - "success" - ] + "support_hdr": { + "type": "boolean" }, - "wolf__api__StreamSessionHandleInputRequest": { - "type": "object", - "properties": { - "input_packet_hex": { - "type": "string", - "description": "A HEX encoded Moonlight input packet, for the full format see: games-on-whales.github.io/wolf/stable/protocols/input-data.html" - }, - "session_id": { - "type": "string" - } - }, - "required": [ - "input_packet_hex", - "session_id" - ] + "title": { + "type": "string" + } + }, + "required": [ + "av1_gst_pipeline", + "h264_gst_pipeline", + "hevc_gst_pipeline", + "id", + "opus_gst_pipeline", + "render_node", + "runner", + "start_audio_server", + "start_virtual_compositor", + "support_hdr", + "title" + ] + }, + "rfl__Reflector_wolf__core__events__StreamSession___ReflType": { + "type": "object", + "properties": { + "app_id": { + "type": "string" }, - "wolf__api__StreamSessionListResponse": { - "type": "object", - "properties": { - "sessions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" - } - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "sessions", - "success" - ] + "audio_channel_count": { + "type": "integer" }, - "wolf__api__StreamSessionPauseRequest": { - "type": "object", - "properties": { - "session_id": { - "type": "string" - } - }, - "required": [ - "session_id" - ] + "client_id": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "client_settings": { + "$ref": "#/components/schemas/wolf__config__ClientSettings" }, - "wolf__api__StreamSessionStartRequest": { - "type": "object", - "properties": { - "audio_session": { - "$ref": "#/components/schemas/wolf__core__events__AudioSession" - }, - "session_id": { - "type": "string" - }, - "video_session": { - "$ref": "#/components/schemas/wolf__core__events__VideoSession" - } + "video_height": { + "type": "integer" + }, + "video_refresh_rate": { + "type": "integer" + }, + "video_width": { + "type": "integer" + } + }, + "required": [ + "app_id", + "audio_channel_count", + "client_id", + "client_ip", + "client_settings", + "video_height", + "video_refresh_rate", + "video_width" + ] + }, + "wolf__api__AppDeleteRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "wolf__api__AppListResponse": { + "type": "object", + "properties": { + "apps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__App___ReflType" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "apps", + "success" + ] + }, + "wolf__api__GenericErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "error", + "success" + ] + }, + "wolf__api__GenericSuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + }, + "wolf__api__PairRequest": { + "type": "object", + "properties": { + "pair_secret": { + "type": "string" + }, + "pin": { + "type": "string", + "description": "The PIN created by the remote Moonlight client" + } + }, + "required": [ + "pair_secret", + "pin" + ] + }, + "wolf__api__PairedClient": { + "type": "object", + "properties": { + "app_state_folder": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "settings": { + "$ref": "#/components/schemas/wolf__config__ClientSettings" + } + }, + "required": [ + "app_state_folder", + "client_id", + "settings" + ] + }, + "wolf__api__PairedClientsResponse": { + "type": "object", + "properties": { + "clients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/wolf__api__PairedClient" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "clients", + "success" + ] + }, + "wolf__api__PartialClientSettings": { + "type": "object", + "properties": { + "controllers_override": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "XBOX", + "PS", + "NINTENDO", + "AUTO" + ] + } }, - "required": [ - "audio_session", - "session_id", - "video_session" - ] + { + "type": "null" + } + ] }, - "wolf__api__StreamSessionStopRequest": { - "type": "object", - "properties": { - "session_id": { - "type": "string" - } + "h_scroll_acceleration": { + "anyOf": [ + { + "type": "number" }, - "required": [ - "session_id" - ] + { + "type": "null" + } + ] }, - "wolf__api__UnpairClientRequest": { - "type": "object", - "properties": { - "client_id": { - "type": "string", - "description": "The client ID to unpair" - } + "mouse_acceleration": { + "anyOf": [ + { + "type": "number" }, - "required": [ - "client_id" - ] + { + "type": "null" + } + ] }, - "wolf__config__AppCMD__tagged": { - "type": "object", - "properties": { - "run_cmd": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "process" - ] - } + "run_gid": { + "anyOf": [ + { + "type": "integer" }, - "required": [ - "run_cmd", - "type" - ] + { + "type": "null" + } + ] }, - "wolf__config__AppChildSession__tagged": { - "type": "object", - "properties": { - "parent_session_id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "child_session" - ] - } + "run_uid": { + "anyOf": [ + { + "type": "integer" }, - "required": [ - "parent_session_id", - "type" - ] + { + "type": "null" + } + ] }, - "wolf__config__AppDocker__tagged": { - "type": "object", - "properties": { - "base_create_json": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "devices": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "image": { - "type": "string" - }, - "mounts": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "docker" - ] - } + "v_scroll_acceleration": { + "anyOf": [ + { + "type": "number" }, - "required": [ - "devices", - "env", - "image", - "mounts", - "name", - "ports", - "type" - ] + { + "type": "null" + } + ] + } + }, + "required": [] + }, + "wolf__api__PendingPairClient": { + "type": "object", + "properties": { + "client_ip": { + "type": "string", + "description": "The IP of the remote Moonlight client" + }, + "pair_secret": { + "type": "string" + } + }, + "required": [ + "client_ip", + "pair_secret" + ] + }, + "wolf__api__PendingPairRequestsResponse": { + "type": "object", + "properties": { + "requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/wolf__api__PendingPairClient" + } }, - "wolf__config__ClientSettings": { - "type": "object", - "properties": { - "controllers_override": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "XBOX", - "PS", - "NINTENDO", - "AUTO" - ] - } - }, - "h_scroll_acceleration": { - "type": "number" - }, - "mouse_acceleration": { - "type": "number" - }, - "run_gid": { - "type": "integer" - }, - "run_uid": { - "type": "integer" - }, - "v_scroll_acceleration": { - "type": "number" - } + "success": { + "type": "boolean" + } + }, + "required": [ + "requests", + "success" + ] + }, + "wolf__api__RunnerStartRequest": { + "type": "object", + "properties": { + "runner": { + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" }, - "required": [ - "controllers_override", - "h_scroll_acceleration", - "mouse_acceleration", - "run_gid", - "run_uid", - "v_scroll_acceleration" - ] + { + "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + } + ] + }, + "session_id": { + "type": "string" + }, + "stop_stream_when_over": { + "type": "boolean" + } + }, + "required": [ + "runner", + "session_id", + "stop_stream_when_over" + ] + }, + "wolf__api__StreamSessionCreated": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "session_id", + "success" + ] + }, + "wolf__api__StreamSessionHandleInputRequest": { + "type": "object", + "properties": { + "input_packet_hex": { + "type": "string", + "description": "A HEX encoded Moonlight input packet, for the full format see: games-on-whales.github.io/wolf/stable/protocols/input-data.html" }, - "wolf__core__audio__AudioMode": { - "type": "object", - "properties": { - "bitrate": { - "type": "integer" - }, - "channels": { - "type": "integer" - }, - "coupled_streams": { - "type": "integer" - }, - "sample_rate": { - "type": "integer" - }, - "speakers": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "FRONT_LEFT", - "FRONT_RIGHT", - "FRONT_CENTER", - "LOW_FREQUENCY", - "BACK_LEFT", - "BACK_RIGHT", - "SIDE_LEFT", - "SIDE_RIGHT", - "MAX_SPEAKERS" - ] - } - }, - "streams": { - "type": "integer" - } + "session_id": { + "type": "string" + } + }, + "required": [ + "input_packet_hex", + "session_id" + ] + }, + "wolf__api__StreamSessionListResponse": { + "type": "object", + "properties": { + "sessions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/rfl__Reflector_wolf__core__events__StreamSession___ReflType" + } + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "sessions", + "success" + ] + }, + "wolf__api__StreamSessionPauseRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ] + }, + "wolf__api__StreamSessionStartRequest": { + "type": "object", + "properties": { + "audio_session": { + "$ref": "#/components/schemas/wolf__core__events__AudioSession" + }, + "session_id": { + "type": "string" + }, + "video_session": { + "$ref": "#/components/schemas/wolf__core__events__VideoSession" + } + }, + "required": [ + "audio_session", + "session_id", + "video_session" + ] + }, + "wolf__api__StreamSessionStopRequest": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + } + }, + "required": [ + "session_id" + ] + }, + "wolf__api__UnpairClientRequest": { + "type": "object", + "properties": { + "client_id": { + "type": "string", + "description": "The client ID to unpair" + } + }, + "required": [ + "client_id" + ] + }, + "wolf__api__UpdateClientSettingsRequest": { + "type": "object", + "properties": { + "app_state_folder": { + "description": "New app state folder path (optional)", + "anyOf": [ + { + "type": "string" }, - "required": [ - "bitrate", - "channels", - "coupled_streams", - "sample_rate", - "speakers", - "streams" - ] + { + "type": "null" + } + ] + }, + "client_id": { + "type": "string", + "description": "The client ID to identify the client (derived from certificate)" }, - "wolf__core__events__AudioSession": { - "type": "object", - "properties": { - "aes_iv": { - "type": "string" - }, - "aes_key": { - "type": "string" - }, - "audio_mode": { - "$ref": "#/components/schemas/wolf__core__audio__AudioMode" - }, - "client_ip": { - "type": "string" - }, - "encrypt_audio": { - "type": "boolean" - }, - "gst_pipeline": { - "type": "string" - }, - "packet_duration": { - "type": "integer" - }, - "port": { - "type": "integer" - }, - "session_id": { - "type": "integer" - }, - "wait_for_ping": { - "type": "boolean" - } + "settings": { + "description": "Client settings to update (only specified fields will be updated)", + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__api__PartialClientSettings" }, - "required": [ - "aes_iv", - "aes_key", - "audio_mode", - "client_ip", - "encrypt_audio", - "gst_pipeline", - "packet_duration", - "port", - "session_id", - "wait_for_ping" - ] + { + "type": "null" + } + ] + } + }, + "required": [ + "app_state_folder", + "client_id", + "settings" + ] + }, + "wolf__config__AppCMD__tagged": { + "type": "object", + "properties": { + "run_cmd": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "process" + ] + } + }, + "required": [ + "run_cmd", + "type" + ] + }, + "wolf__config__AppChildSession__tagged": { + "type": "object", + "properties": { + "parent_session_id": { + "type": "string" }, - "wolf__core__events__VideoSession": { - "type": "object", - "properties": { - "bitrate_kbps": { - "type": "integer" - }, - "client_ip": { - "type": "string" - }, - "color_range": { - "type": "string", - "enum": [ - "JPEG", - "MPEG" - ] - }, - "color_space": { - "type": "string", - "enum": [ - "BT601", - "BT709", - "BT2020" - ] - }, - "display_mode": { - "$ref": "#/components/schemas/wolf__core__virtual_display__DisplayMode" - }, - "fec_percentage": { - "type": "integer" - }, - "frames_with_invalid_ref_threshold": { - "type": "integer" - }, - "gst_pipeline": { - "type": "string" - }, - "min_required_fec_packets": { - "type": "integer" - }, - "packet_size": { - "type": "integer" - }, - "port": { - "type": "integer" - }, - "session_id": { - "type": "integer" - }, - "slices_per_frame": { - "type": "integer" - }, - "timeout_ms": { - "type": "integer" - }, - "wait_for_ping": { - "type": "boolean" - } + "type": { + "type": "string", + "enum": [ + "child_session" + ] + } + }, + "required": [ + "parent_session_id", + "type" + ] + }, + "wolf__config__AppDocker__tagged": { + "type": "object", + "properties": { + "base_create_json": { + "anyOf": [ + { + "type": "string" }, - "required": [ - "bitrate_kbps", - "client_ip", - "color_range", - "color_space", - "display_mode", - "fec_percentage", - "frames_with_invalid_ref_threshold", - "gst_pipeline", - "min_required_fec_packets", - "packet_size", - "port", - "session_id", - "slices_per_frame", - "timeout_ms", - "wait_for_ping" + { + "type": "null" + } + ] + }, + "devices": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "image": { + "type": "string" + }, + "mounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "docker" + ] + } + }, + "required": [ + "devices", + "env", + "image", + "mounts", + "name", + "ports", + "type" + ] + }, + "wolf__config__ClientSettings": { + "type": "object", + "properties": { + "controllers_override": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "XBOX", + "PS", + "NINTENDO", + "AUTO" ] + } }, - "wolf__core__virtual_display__DisplayMode": { - "type": "object", - "properties": { - "height": { - "type": "integer" - }, - "refreshRate": { - "type": "integer" - }, - "width": { - "type": "integer" - } - }, - "required": [ - "height", - "refreshRate", - "width" + "h_scroll_acceleration": { + "type": "number" + }, + "mouse_acceleration": { + "type": "number" + }, + "run_gid": { + "type": "integer" + }, + "run_uid": { + "type": "integer" + }, + "v_scroll_acceleration": { + "type": "number" + } + }, + "required": [ + "controllers_override", + "h_scroll_acceleration", + "mouse_acceleration", + "run_gid", + "run_uid", + "v_scroll_acceleration" + ] + }, + "wolf__core__audio__AudioMode": { + "type": "object", + "properties": { + "bitrate": { + "type": "integer" + }, + "channels": { + "type": "integer" + }, + "coupled_streams": { + "type": "integer" + }, + "sample_rate": { + "type": "integer" + }, + "speakers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "FRONT_LEFT", + "FRONT_RIGHT", + "FRONT_CENTER", + "LOW_FREQUENCY", + "BACK_LEFT", + "BACK_RIGHT", + "SIDE_LEFT", + "SIDE_RIGHT", + "MAX_SPEAKERS" ] + } + }, + "streams": { + "type": "integer" + } + }, + "required": [ + "bitrate", + "channels", + "coupled_streams", + "sample_rate", + "speakers", + "streams" + ] + }, + "wolf__core__events__AudioSession": { + "type": "object", + "properties": { + "aes_iv": { + "type": "string" + }, + "aes_key": { + "type": "string" + }, + "audio_mode": { + "$ref": "#/components/schemas/wolf__core__audio__AudioMode" + }, + "client_ip": { + "type": "string" + }, + "encrypt_audio": { + "type": "boolean" + }, + "gst_pipeline": { + "type": "string" + }, + "packet_duration": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "session_id": { + "type": "integer" + }, + "wait_for_ping": { + "type": "boolean" + } + }, + "required": [ + "aes_iv", + "aes_key", + "audio_mode", + "client_ip", + "encrypt_audio", + "gst_pipeline", + "packet_duration", + "port", + "session_id", + "wait_for_ping" + ] + }, + "wolf__core__events__VideoSession": { + "type": "object", + "properties": { + "bitrate_kbps": { + "type": "integer" + }, + "client_ip": { + "type": "string" + }, + "color_range": { + "type": "string", + "enum": [ + "JPEG", + "MPEG" + ] + }, + "color_space": { + "type": "string", + "enum": [ + "BT601", + "BT709", + "BT2020" + ] + }, + "display_mode": { + "$ref": "#/components/schemas/wolf__core__virtual_display__DisplayMode" + }, + "fec_percentage": { + "type": "integer" + }, + "frames_with_invalid_ref_threshold": { + "type": "integer" + }, + "gst_pipeline": { + "type": "string" + }, + "min_required_fec_packets": { + "type": "integer" + }, + "packet_size": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "session_id": { + "type": "integer" + }, + "slices_per_frame": { + "type": "integer" + }, + "timeout_ms": { + "type": "integer" + }, + "wait_for_ping": { + "type": "boolean" + } + }, + "required": [ + "bitrate_kbps", + "client_ip", + "color_range", + "color_space", + "display_mode", + "fec_percentage", + "frames_with_invalid_ref_threshold", + "gst_pipeline", + "min_required_fec_packets", + "packet_size", + "port", + "session_id", + "slices_per_frame", + "timeout_ms", + "wait_for_ping" + ] + }, + "wolf__core__virtual_display__DisplayMode": { + "type": "object", + "properties": { + "height": { + "type": "integer" + }, + "refreshRate": { + "type": "integer" + }, + "width": { + "type": "integer" } + }, + "required": [ + "height", + "refreshRate", + "width" + ] } + } } } \ No newline at end of file