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