#include "OrcaCloudServiceAgent.hpp" #include "Http.hpp" #include "libslic3r/Utils.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r/AppConfig.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #endif #if defined(__APPLE__) #include #include #endif using json = nlohmann::json; namespace Slic3r { namespace { constexpr const char* ORCA_DEFAULT_API_URL = "api.orcaslicer.com"; constexpr const char* ORCA_DEFAULT_AUTH_URL = "https://auth.orcaslicer.com"; constexpr const char* ORCA_DEFAULT_CLOUD_URL = "https://cloud.orcaslicer.com"; // Orca: This is a public key with no secret, used to identify the client application to the backend. constexpr const char* ORCA_DEFAULT_PUB_KEY = "sb_publishable_lvVe_whOi80SU9BPSxM1kA_tbt9AbR_"; constexpr const char* ORCA_HEALTH_PATH = "/api/v1/health"; constexpr const char* ORCA_SYNC_PULL_PATH = "/api/v1/sync/pull"; constexpr const char* ORCA_SYNC_PUSH_PATH = "/api/v1/sync/push"; constexpr const char* ORCA_SYNC_FORCE_PUSH_PATH = "/api/v1/sync/force-push"; constexpr const char* ORCA_SYNC_DELETE_PATH = "/api/v1/sync/delete"; constexpr const char* ORCA_PROFILES_PATH = "/api/v1/sync/profiles"; constexpr const char* ORCA_SUBSCRIPTIONS_PATH = "/api/v1/subscriptions"; constexpr const char* ORCA_SYNC_STATE_FILE = "sync_state"; constexpr const char* ORCA_SYNC_PROFILE_TABLE = "profiles"; constexpr size_t ORCA_SYNC_MAX_PAYLOAD_SIZE = 1048576; // 1MB size limit constexpr const char* ORCA_CLOUD_LOGIN_PATH = "/orcaslicer-login"; constexpr const char* CONFIG_ORCA_API_URL = "orca_api_url"; constexpr const char* CONFIG_ORCA_AUTH_URL = "orca_auth_url"; constexpr const char* CONFIG_ORCA_CLOUD_URL = "orca_cloud_url"; constexpr const char* CONFIG_ORCA_PUB_KEY = "orca_pub_key"; constexpr const char* SECRET_STORE_SERVICE = "OrcaSlicer/Auth"; constexpr const char* SECRET_STORE_USER = "orca_refresh_token"; constexpr std::chrono::seconds TOKEN_REFRESH_SKEW{900}; // 15 minutes // Return a JSON field only when it is present as a string. Missing or non-string values normalize to empty. std::string get_json_string_field(const json& j, const std::string& key) { if (j.contains(key) && j[key].is_string()) { return j[key].get(); } return ""; } // Resolve the human-facing UI label from provider metadata. std::string resolve_display_name( const std::string& display_name, const std::string& nickname, const std::string& full_name, const std::string& name, const std::string& username) { // Providers and payload shapes do not all use the same display-name field. // Fallback sequence: display_name -> nickname -> full_name -> name if (!display_name.empty()) return display_name; if (!nickname.empty()) return nickname; if (!full_name.empty()) return full_name; if (!name.empty()) return name; return username; } std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "") { if (name.empty()) { return ""; } // Mix user_id into the hashed input so two different users generating a setting_id // for an identically-named preset get distinct UUIDs. Without this, the cloud's ID // space collides across accounts and the second user's create gets HTTP 409 with // server_profile=null on every sync (the foreign owner's record is not exposed). static const boost::uuids::uuid orca_namespace = boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479"); boost::uuids::name_generator_sha1 gen(orca_namespace); boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name); return boost::uuids::to_string(id); } std::string base64url_encode(const std::vector& data) { std::string out; out.resize(boost::beast::detail::base64::encoded_size(data.size())); out.resize(boost::beast::detail::base64::encode(out.data(), data.data(), data.size())); std::replace(out.begin(), out.end(), '+', '-'); std::replace(out.begin(), out.end(), '/', '_'); out.erase(std::remove(out.begin(), out.end(), '='), out.end()); return out; } bool base64url_decode(const std::string& input, std::vector& out) { std::string padded = input; while (padded.size() % 4 != 0) padded.push_back('='); std::string normalized = padded; std::replace(normalized.begin(), normalized.end(), '-', '+'); std::replace(normalized.begin(), normalized.end(), '_', '/'); out.resize(boost::beast::detail::base64::decoded_size(normalized.size())); auto res = boost::beast::detail::base64::decode(out.data(), normalized.data(), normalized.size()); if (!res.second) return false; out.resize(res.first); return true; } std::vector random_bytes(size_t len) { std::vector bytes(len); if (RAND_bytes(bytes.data(), static_cast(len)) != 1) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dist(0, 255); for (auto& b : bytes) b = static_cast(dist(gen)); } return bytes; } std::string generate_code_verifier() { constexpr int PKCE_VERIFIER_BYTES = 32; auto bytes = random_bytes(PKCE_VERIFIER_BYTES); return base64url_encode(bytes); } std::string generate_state_token() { auto bytes = random_bytes(16); std::stringstream ss; for (auto b : bytes) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); } return ss.str(); } std::string sha256_base64url(const std::string& input) { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast(input.data()), input.size(), hash); std::vector hash_vec(hash, hash + sizeof(hash)); return base64url_encode(hash_vec); } std::string os_machine_id() { // Orca: OS-level identifiers that live outside data_dir, so a copied data_dir // on another machine yields a different key and the stored refresh // token silently fails to decrypt (forcing a normal sign-in). #if defined(__linux__) std::ifstream f("/etc/machine-id"); std::string id; if (f.good()) { std::getline(f, id); } if (!id.empty()) return id; #elif defined(_WIN32) // HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid is a per-install // identifier that isn't copied along with the user's data_dir. HKEY hkey = nullptr; if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ | KEY_WOW64_64KEY, &hkey) == ERROR_SUCCESS) { wchar_t buffer[128] = {0}; DWORD buffer_bytes = sizeof(buffer); DWORD type = 0; LONG status = RegQueryValueExW(hkey, L"MachineGuid", nullptr, &type, reinterpret_cast(buffer), &buffer_bytes); RegCloseKey(hkey); if (status == ERROR_SUCCESS && type == REG_SZ && buffer_bytes > sizeof(wchar_t)) { const size_t wlen = (buffer_bytes / sizeof(wchar_t)) - 1; // strip trailing NUL return wxString(buffer, wlen).ToStdString(); } } #elif defined(__APPLE__) // gethostuuid() returns the hardware-tied host UUID (same value as // "Hardware UUID" in System Information). It persists across OS // reinstalls and kernel updates. Do NOT use sysctl kern.uuid — that // is the running kernel image's build UUID (from kernel_uuid_string // in XNU), which rotates on every macOS/kernel update and would // sign users out on every OS update. uuid_t host_id; struct timespec wait = {0, 0}; if (gethostuuid(host_id, &wait) == 0) { uuid_string_t str; uuid_unparse(host_id, str); return std::string(str); } #endif // Last resort: OS hostname. return wxGetHostName().ToStdString(); } std::string get_encryption_key() { // Bind the encryption key to both the machine and the OS user. A data_dir // copied to another machine changes the machine half; a data_dir shared // between OS users on the same machine changes the user half. Either // difference makes the stored token fail to decrypt and forces a sign-in. return os_machine_id() + ":" + wxGetUserId().ToStdString(); } std::vector sha256_bytes(const std::string& input) { std::vector out(SHA256_DIGEST_LENGTH, 0); SHA256(reinterpret_cast(input.data()), input.size(), out.data()); return out; } std::string hmac_sha256_hex(const std::string& data, const std::vector& key) { unsigned int len = 0; unsigned char result[EVP_MAX_MD_SIZE]; if (HMAC(EVP_sha256(), key.data(), static_cast(key.size()), reinterpret_cast(data.data()), data.size(), result, &len) == nullptr) { return {}; } std::ostringstream oss; oss << std::hex << std::setfill('0'); for (unsigned int i = 0; i < len; ++i) { oss << std::setw(2) << static_cast(result[i]); } return oss.str(); } bool is_port_available(int port) { if (port <= 0 || port > 65535) return false; using boost::asio::ip::tcp; boost::asio::io_context ctx; boost::system::error_code ec; tcp::acceptor acceptor(ctx); tcp::endpoint endpoint(tcp::v4(), static_cast(port)); acceptor.open(endpoint.protocol(), ec); if (ec) return false; acceptor.set_option(tcp::acceptor::reuse_address(true), ec); if (ec) return false; acceptor.bind(endpoint, ec); if (ec) return false; acceptor.close(ec); return true; } int choose_loopback_port() { int base_port = auth_constants::LOOPBACK_PORT; if (const char* env_port = std::getenv("ORCA_LOOPBACK_PORT")) { try { int parsed = std::stoi(env_port); if (parsed > 0 && parsed <= 65535) { base_port = parsed; } } catch (...) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: invalid ORCA_LOOPBACK_PORT value, falling back to default"; } } std::vector candidates = {base_port, base_port + 1, base_port + 2}; for (int port : candidates) { if (is_port_available(port)) return port; } return base_port; } bool aes256gcm_encrypt(const std::string& plaintext, const std::vector& key, std::string& out_b64) { const int iv_len = 12; auto iv = random_bytes(iv_len); EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); if (!ctx) return false; bool ok = true; int len = 0; std::vector ciphertext(plaintext.size()); std::vector tag(16); if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) ok = false; if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr) != 1) ok = false; if (ok && EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) ok = false; if (ok && EVP_EncryptUpdate(ctx, ciphertext.data(), &len, reinterpret_cast(plaintext.data()), plaintext.size()) != 1) ok = false; int ciphertext_len = len; if (ok && EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len) != 1) ok = false; ciphertext_len += len; if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, tag.size(), tag.data()) != 1) ok = false; EVP_CIPHER_CTX_free(ctx); if (!ok) return false; ciphertext.resize(ciphertext_len); std::vector payload; payload.reserve(iv.size() + tag.size() + ciphertext.size()); payload.insert(payload.end(), iv.begin(), iv.end()); payload.insert(payload.end(), tag.begin(), tag.end()); payload.insert(payload.end(), ciphertext.begin(), ciphertext.end()); out_b64 = base64url_encode(payload); return true; } bool aes256gcm_decrypt(const std::string& b64_payload, const std::vector& key, std::string& plaintext) { std::vector payload; if (!base64url_decode(b64_payload, payload)) return false; if (payload.size() < 12 + 16) return false; const size_t iv_len = 12; const size_t tag_len = 16; std::vector iv(payload.begin(), payload.begin() + iv_len); std::vector tag(payload.begin() + iv_len, payload.begin() + iv_len + tag_len); std::vector ciphertext(payload.begin() + iv_len + tag_len, payload.end()); EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); if (!ctx) return false; bool ok = true; int len = 0; std::vector plain(ciphertext.size()); if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) ok = false; if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr) != 1) ok = false; if (ok && EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) ok = false; if (ok && EVP_DecryptUpdate(ctx, plain.data(), &len, ciphertext.data(), ciphertext.size()) != 1) ok = false; int plain_len = len; if (ok && EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), tag.data()) != 1) ok = false; if (ok && EVP_DecryptFinal_ex(ctx, plain.data() + len, &len) != 1) ok = false; plain_len += len; EVP_CIPHER_CTX_free(ctx); if (!ok) return false; plain.resize(plain_len); plaintext.assign(reinterpret_cast(plain.data()), plain.size()); return true; } } // namespace // ============================================================================ // Constructor / Destructor // ============================================================================ OrcaCloudServiceAgent::OrcaCloudServiceAgent(std::string log_dir) : log_dir(std::move(log_dir)) , api_base_url(ORCA_DEFAULT_API_URL) , auth_base_url(ORCA_DEFAULT_AUTH_URL) , cloud_base_url(ORCA_DEFAULT_CLOUD_URL) { auth_headers["apikey"] = ORCA_DEFAULT_PUB_KEY; pkce_bundle.loopback_port = choose_loopback_port(); update_redirect_uri(); regenerate_pkce(); compute_fallback_path(); } OrcaCloudServiceAgent::~OrcaCloudServiceAgent() { if (refresh_thread.joinable()) { refresh_thread.join(); } } void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config) { if (!app_config) return; // Read token storage preference m_use_encrypted_token_file = app_config->get_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE); std::string api_url = app_config->get(CONFIG_ORCA_API_URL); if (!api_url.empty()) { api_base_url = api_url; } std::string auth_url = app_config->get(CONFIG_ORCA_AUTH_URL); if (!auth_url.empty()) { auth_base_url = auth_url; } std::string cloud_url = app_config->get(CONFIG_ORCA_CLOUD_URL); if (!cloud_url.empty()) { cloud_base_url = cloud_url; } std::string pub_key = app_config->get(CONFIG_ORCA_PUB_KEY); if (!pub_key.empty()) { auth_headers["apikey"] = pub_key; } } void OrcaCloudServiceAgent::set_api_base_url(const std::string& url) { api_base_url = url; } void OrcaCloudServiceAgent::set_auth_base_url(const std::string& url) { auth_base_url = url; } void OrcaCloudServiceAgent::set_cloud_base_url(const std::string& url) { cloud_base_url = url; } void OrcaCloudServiceAgent::set_use_encrypted_token_file(bool use) { m_use_encrypted_token_file = use; } bool OrcaCloudServiceAgent::get_use_encrypted_token_file() const { return m_use_encrypted_token_file; } // ============================================================================ // ICloudServiceAgent - Lifecycle Methods // ============================================================================ int OrcaCloudServiceAgent::init_log() { return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_config_dir(std::string cfg_dir) { config_dir = cfg_dir; wxFileName fallback(wxString::FromUTF8(cfg_dir.c_str()), "orca_refresh_token.sec"); fallback.Normalize(); refresh_fallback_path = fallback.GetFullPath().ToStdString(); return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_cert_file(std::string folder, std::string filename) { // Not used by OrcaCloudServiceAgent (OAuth doesn't need client certs) (void) folder; (void) filename; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_country_code(std::string code) { country_code = code; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::start() { regenerate_pkce(); // Attempt silent sign-in from stored refresh token std::string stored_refresh; if (load_refresh_token(stored_refresh) && !stored_refresh.empty()) { refresh_now(stored_refresh, "refresh token", false); } return BAMBU_NETWORK_SUCCESS; } // ============================================================================ // ICloudServiceAgent - User Session Management // ============================================================================ bool OrcaCloudServiceAgent::exchange_auth_code(const std::string& auth_code, const std::string& state, std::string& session_payload) { const auto expected_state = pkce_bundle.state; if (expected_state.empty()) { BOOST_LOG_TRIVIAL(warning) << "[auth] event=code_exchange result=failure reason=no_expected_state"; return false; } if (state != expected_state) { BOOST_LOG_TRIVIAL(warning) << "[auth] event=code_exchange result=failure reason=state_mismatch"; return false; } std::string code_verifier; { std::lock_guard lock(state_mutex); code_verifier = pkce_bundle.verifier; } // Exchange the Supabase PKCE code with the token endpoint. // With redirect_to, GoTrue handles Google OAuth internally and redirects to localhost // with a Supabase-generated auth_code (not a raw Google code). The grant_type goes // in the URL query string (same pattern as the refresh_token flow). const std::string token_url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=pkce"; json body_j; body_j["auth_code"] = auth_code; body_j["code_verifier"] = code_verifier; std::string response; unsigned int http_code = 0; const bool success = http_post_token(body_j.dump(), &response, &http_code, token_url); if (!success || http_code >= 400) { BOOST_LOG_TRIVIAL(error) << "[auth] event=code_exchange result=failure http_code=" << http_code << " body=" << response; return false; } session_payload = response; BOOST_LOG_TRIVIAL(info) << "[auth] event=code_exchange result=success"; return true; } int OrcaCloudServiceAgent::change_user(std::string user_info) { try { auto tree = json::parse(user_info); auto safe_str = [](const json& j, const std::string& key) -> std::string { if (j.contains(key) && j[key].is_string()) return j[key].get(); return ""; }; // Check if this is a WebView login message (PKCE flow completion) std::string command = safe_str(tree, "command"); if (command == "user_login") { if (!tree.contains("data") || !tree["data"].is_object()) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: WebView login payload missing data field"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } const auto& data = tree["data"]; std::string state = safe_str(data, "state"); // Check for auth code (PKCE authorization code flow) std::string auth_code = safe_str(data, "code"); if (!auth_code.empty()) { std::string session_payload; if (!exchange_auth_code(auth_code, state, session_payload)) { return BAMBU_NETWORK_ERR_INVALID_HANDLE; } // Recursively process the session payload (contains access_token, user, etc.) return change_user(session_payload); } // Validate PKCE state const auto expected_state = pkce_bundle.state; if (!expected_state.empty() && state != expected_state) { BOOST_LOG_TRIVIAL(warning) << "[auth] event=login result=failure reason=state_mismatch"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } // Direct token flow (tokens already obtained by WebView) bool success = set_user_session(data); BOOST_LOG_TRIVIAL(info) << "[auth] event=login result=" << (success ? "success" : "failure") << " source=webview"; return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; } // Orca cloud session payload (default flow) const json* session_node = nullptr; if (tree.contains("data") && tree["data"].is_object()) { const auto& data = tree["data"]; if (data.contains("session") && data["session"].is_object()) { session_node = &data["session"]; } else if (data.contains("access_token") || data.contains("token")) { session_node = &data; } } if (!session_node) { if (tree.contains("session") && tree["session"].is_object()) { session_node = &tree["session"]; } else if (tree.contains("access_token") || tree.contains("token")) { session_node = &tree; } } if (session_node) { return set_user_session(*session_node) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; } BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: Username/password login is disabled. Use the Orca cloud PKCE flow."; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: change_user exception - " << e.what(); return BAMBU_NETWORK_ERR_INVALID_RESULT; } } bool OrcaCloudServiceAgent::is_user_login() { std::lock_guard lock(session_mutex); return session.logged_in; } int OrcaCloudServiceAgent::user_logout(bool request) { // Send logout request to backend if requested if (request) { std::string token; std::string refresh_copy; { std::lock_guard lock(session_mutex); token = session.access_token; refresh_copy = session.refresh_token; } if (!token.empty()) { std::string response; unsigned int http_code = 0; json logout_req = json::object(); if (!refresh_copy.empty()) { logout_req["refresh_token"] = refresh_copy; } int result = http_post_auth(auth_constants::LOGOUT_PATH, logout_req.dump(), &response, &http_code) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_INVALID_HANDLE; if (result != BAMBU_NETWORK_SUCCESS || http_code >= 400) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: Orca cloud logout request failed - http_code=" << http_code; } } } clear_session(); return BAMBU_NETWORK_SUCCESS; } std::string OrcaCloudServiceAgent::get_user_id() { std::lock_guard lock(session_mutex); return session.user_id; } std::string OrcaCloudServiceAgent::get_user_name() { std::lock_guard lock(session_mutex); return session.user_name; } std::string OrcaCloudServiceAgent::get_user_avatar() { std::lock_guard lock(session_mutex); return session.user_avatar; } std::string OrcaCloudServiceAgent::get_user_nickname() { std::lock_guard lock(session_mutex); return session.user_nickname; } // ============================================================================ // ICloudServiceAgent - Login UI Support // ============================================================================ std::string OrcaCloudServiceAgent::build_login_cmd() { // When already signed in, emit the homepage payload so the web UI // can flip to the logged-in state without re-opening the login flow. if (is_user_login()) { std::string display_name = get_user_nickname(); if (display_name.empty()) { display_name = "unknown name"; } json cmd; cmd["command"] = "orca_userlogin"; cmd["data"]["name"] = display_name; cmd["data"]["avatar"] = get_user_avatar(); return cmd.dump(); } update_redirect_uri(); regenerate_pkce(); const auto bundle = pkce(); json tree; tree["action"] = "login_config"; tree["backend_url"] = auth_base_url; // Include API key for direct Supabase calls from JavaScript { std::lock_guard lock(state_mutex); auto it = auth_headers.find("apikey"); if (it != auth_headers.end()) { tree["apikey"] = it->second; } } tree["pkce"]["code_challenge"] = bundle.challenge; tree["pkce"]["code_challenge_method"] = "S256"; tree["pkce"]["state"] = bundle.state; tree["pkce"]["redirect_uri"] = bundle.redirect; tree["pkce"]["code_verifier"] = bundle.verifier; tree["pkce"]["loopback_port"] = bundle.loopback_port; return tree.dump(); } std::string OrcaCloudServiceAgent::build_logout_cmd() { return json{{"command", "orca_useroffline"}}.dump(); } std::string OrcaCloudServiceAgent::build_login_info() { json tree; { std::lock_guard lock(session_mutex); tree["user_id"] = session.user_id; tree["user_name"] = session.user_name; tree["nickname"] = session.user_nickname; tree["avatar"] = session.user_avatar; tree["logged_in"] = session.logged_in; } // Do not expose tokens to the WebView tree["access_token"] = ""; tree["refresh_token"] = ""; tree["backend_url"] = api_base_url; tree["auth_url"] = auth_base_url; return tree.dump(); } // ============================================================================ // ICloudServiceAgent - Token Access // ============================================================================ std::string OrcaCloudServiceAgent::get_access_token() const { std::lock_guard lock(session_mutex); return session.access_token; } std::string OrcaCloudServiceAgent::get_refresh_token() const { std::lock_guard lock(session_mutex); return session.refresh_token; } bool OrcaCloudServiceAgent::ensure_token_fresh(const std::string& reason) { return refresh_if_expiring(TOKEN_REFRESH_SKEW, reason); } // ============================================================================ // ICloudServiceAgent - Server Connectivity // ============================================================================ int OrcaCloudServiceAgent::connect_server() { std::string response; unsigned int http_code = 0; int result = http_get(ORCA_HEALTH_PATH, &response, &http_code); bool connected = (result == BAMBU_NETWORK_SUCCESS && http_code >= 200 && http_code < 300); { std::lock_guard lock(state_mutex); is_connected = connected; } invoke_server_connected_callback(connected ? 0 : -1, http_code); return connected ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECTION_TO_SERVER_FAILED; } bool OrcaCloudServiceAgent::is_server_connected() { std::lock_guard lock(state_mutex); return is_connected; } int OrcaCloudServiceAgent::refresh_connection() { return connect_server(); } int OrcaCloudServiceAgent::start_subscribe(std::string module) { (void) module; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::stop_subscribe(std::string module) { (void) module; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::add_subscribe(std::vector dev_list) { (void) dev_list; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::del_subscribe(std::vector dev_list) { (void) dev_list; return BAMBU_NETWORK_SUCCESS; } void OrcaCloudServiceAgent::enable_multi_machine(bool enable) { std::lock_guard lock(state_mutex); multi_machine_enabled = enable; } // ============================================================================ // ICloudServiceAgent - Settings Synchronization // ============================================================================ int OrcaCloudServiceAgent::get_user_presets(std::map>* user_presets) { if (!user_presets) return BAMBU_NETWORK_ERR_INVALID_HANDLE; if (!is_user_login()) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: Not logged in"; return BAMBU_NETWORK_ERR_INVALID_HANDLE; } bool success = false; std::string error_msg; int http_code_out = 0; SyncState saved_state; { std::lock_guard lock(state_mutex); saved_state = sync_state; sync_state = SyncState{}; } auto on_success = [&](const SyncPullResponse& resp) { // Get current user_id for all presets (required by load_user_preset) std::string current_user_id = get_user_id(); for (const auto& upsert : resp.upserts) { // Parse JSON content into key-value pairs using helper std::map value_map; json_to_map(upsert.content.dump(), value_map); // Add metadata from top-level sync response if not already in content // These are required by PresetCollection::load_user_preset if (value_map.find(BBL_JSON_KEY_SETTING_ID) == value_map.end()) { value_map[BBL_JSON_KEY_SETTING_ID] = upsert.id; } if (value_map.find(BBL_JSON_KEY_USER_ID) == value_map.end()) { value_map[BBL_JSON_KEY_USER_ID] = current_user_id; } if (value_map.find(ORCA_JSON_KEY_UPDATE_TIME) == value_map.end()) { value_map[ORCA_JSON_KEY_UPDATE_TIME] = std::to_string(upsert.updated_time); } // Use preset name from content or fallback to upsert.name or upsert.id std::string preset_name = upsert.content.value(BBL_JSON_KEY_NAME, ""); if (preset_name.empty()) { preset_name = upsert.name.empty() ? upsert.id : upsert.name; } // Store as: user_presets[preset_name][key] = value // This matches the format expected by PresetBundle::load_user_presets (*user_presets)[preset_name] = value_map; } if (resp.next_cursor != 0) { std::lock_guard lock(state_mutex); sync_state.last_sync_timestamp = resp.next_cursor; save_sync_state(); } else { std::lock_guard lock(state_mutex); sync_state = saved_state; } success = true; }; auto on_error = [&](int code, const std::string& err) { http_code_out = code; error_msg = err; success = false; }; sync_pull(on_success, on_error); if (!success) { std::lock_guard lock(state_mutex); sync_state = saved_state; } return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; } std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map* values_map, unsigned int* http_code) { std::string new_id = generate_uuid_for_setting_id(name, get_user_id()); if (new_id.empty()) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: request_setting_id failed - name is empty"; return ""; } nlohmann::json content; content["name"] = name; content["type"] = IOT_PRINT_TYPE_STRING; // Default type if (values_map && !values_map->empty()) { for (const auto& pair : *values_map) { // Skip updated_time - it's metadata, not content if (pair.first == IOT_JSON_KEY_UPDATED_TIME) continue; content[pair.first] = pair.second; } } // Use sync_push to create the profile (no original_updated_time for new profiles per spec) auto result = sync_push(new_id, name, content, ""); if (http_code) *http_code = result.http_code; if (result.success) { if (values_map && result.new_updated_time != 0) { (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time); } return new_id; } BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: request_setting_id failed - " << result.error_message << " - http code: " << result.http_code; return ""; } int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map* values_map, unsigned int* http_code, bool force) { // Extract original_updated_time for Optimistic Concurrency Control // If present, server will verify version before update. If absent, treated as insert. std::string original_updated_time; if (values_map) { auto it = values_map->find(IOT_JSON_KEY_UPDATED_TIME); if (it != values_map->end()) { original_updated_time = it->second; } } // Build content JSON nlohmann::json content; content["name"] = name; if (values_map && !values_map->empty()) { for (const auto& pair : *values_map) { // Skip updated_time - it's used for OCC, not as content if (pair.first == IOT_JSON_KEY_UPDATED_TIME) continue; content[pair.first] = pair.second; } } auto result = sync_push(setting_id, name, content, original_updated_time, force); if (http_code) *http_code = result.http_code; if (result.success) { if (values_map && result.new_updated_time != 0) { (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time); } return BAMBU_NETWORK_SUCCESS; } if (result.http_code == 409) { // Conflict - update values_map with server version if (values_map && result.server_version.updated_time != 0) { (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.server_version.updated_time); } BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: put_setting conflict - server_updated_time=" << result.server_version.updated_time; } BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: put_setting failed - " << result.error_message; return BAMBU_NETWORK_ERR_PUT_SETTING_FAILED; } int OrcaCloudServiceAgent::get_setting_list(std::string bundle_version, ProgressFn pro_fn, WasCancelledFn cancel_fn) { return get_setting_list2(bundle_version, nullptr, pro_fn, cancel_fn); } int OrcaCloudServiceAgent::get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn, WasCancelledFn cancel_fn) { (void) bundle_version; bool success = false; int error_code = 0; auto on_success = [&](const SyncPullResponse& resp) { int total = static_cast(resp.upserts.size() + resp.deletes.size()); int processed = 0; bool cancelled = false; for (const auto& upsert : resp.upserts) { if (cancel_fn && cancel_fn()) { cancelled = true; break; } if (chk_fn) { std::map info; info[IOT_JSON_KEY_SETTING_ID] = upsert.id; info[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(upsert.updated_time); if (upsert.content.is_object()) { for (auto& [key, value] : upsert.content.items()) { if (value.is_string()) { info[key] = value.get(); } else { info[key] = value.dump(); } } } if (!info.count(IOT_JSON_KEY_NAME) && !upsert.name.empty()) { info[IOT_JSON_KEY_NAME] = upsert.name; } if (!info.count(IOT_JSON_KEY_TYPE)) { info[IOT_JSON_KEY_TYPE] = IOT_PRINT_TYPE_STRING; } chk_fn(info); } if (pro_fn) { int progress = total > 0 ? (processed * 100 / total) : 100; pro_fn(progress); } processed++; } if (!cancelled) { for (const auto& deleted_id : resp.deletes) { if (cancel_fn && cancel_fn()) { cancelled = true; break; } if (chk_fn) { std::map info; info[IOT_JSON_KEY_SETTING_ID] = deleted_id; info["deleted"] = "true"; chk_fn(info); } if (pro_fn) { int progress = total > 0 ? (processed * 100 / total) : 100; pro_fn(progress); } processed++; } } if (!cancelled && resp.next_cursor != 0) { std::lock_guard lock(state_mutex); sync_state.last_sync_timestamp = resp.next_cursor; save_sync_state(); } if (pro_fn) { pro_fn(100); } success = !cancelled; }; auto on_error = [&](int code, const std::string& err) { error_code = code; success = false; }; sync_pull(on_success, on_error); return success ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; } int OrcaCloudServiceAgent::delete_setting(std::string setting_id) { std::string path = std::string(ORCA_SYNC_DELETE_PATH) + "?resource=" + ORCA_SYNC_PROFILE_TABLE + "&id=" + setting_id; std::string response; unsigned int http_code = 0; int result = http_delete(path, &response, &http_code); // Treat 204 as success: the setting is already gone from cloud, which is our goal. if (http_code == 204) { return BAMBU_NETWORK_SUCCESS; } if (result != BAMBU_NETWORK_SUCCESS || http_code >= 400) { return BAMBU_NETWORK_ERR_DEL_SETTING_FAILED; } return BAMBU_NETWORK_SUCCESS; } // ============================================================================ // Sync Protocol Implementation // ============================================================================ int OrcaCloudServiceAgent::sync_pull( std::function on_success, std::function on_error) { std::string path = ORCA_SYNC_PULL_PATH; if (sync_state.last_sync_timestamp != 0) { path += "?cursor=" + std::to_string(sync_state.last_sync_timestamp); } std::string response; unsigned int http_code = 0; int result = http_get(path, &response, &http_code); // Handle 410 Gone - cursor too old, need full resync if (http_code == 410) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: sync_pull returned 410 Gone - cursor too old, triggering full resync"; clear_sync_state(); // Retry without cursor path = ORCA_SYNC_PULL_PATH; result = http_get(path, &response, &http_code); } if (result != BAMBU_NETWORK_SUCCESS || (http_code != 200 && http_code != 304)) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: sync_pull failed - http_code=" << http_code << " - path=" << path; if (on_error) on_error(http_code, response); return BAMBU_NETWORK_ERR_GET_SETTING_LIST_FAILED; } if (http_code == 304) { if (on_success) { SyncPullResponse empty_response; on_success(empty_response); } return BAMBU_NETWORK_SUCCESS; } try { auto json = nlohmann::json::parse(response); SyncPullResponse resp; resp.next_cursor = json.value("next_cursor", 0); if (json.contains("upserts") && json["upserts"].is_array()) { for (const auto& item : json["upserts"]) { ProfileUpsert upsert; upsert.id = item.value("id", ""); upsert.name = item.value("name", ""); upsert.updated_time = item.value(ORCA_JSON_KEY_UPDATE_TIME, 0); upsert.created_time = item.value(ORCA_JSON_KEY_CREATED_TIME, 0); if (item.contains("content")) { upsert.content = item["content"]; } resp.upserts.push_back(upsert); } } if (json.contains("deletes") && json["deletes"].is_array()) { for (const auto& item : json["deletes"]) { resp.deletes.push_back(item.get()); } } if (on_success) on_success(resp); return BAMBU_NETWORK_SUCCESS; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: sync_pull parse error - " << e.what(); if (on_error) on_error(http_code, e.what()); return BAMBU_NETWORK_ERR_INVALID_RESULT; } } SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id, const std::string& name, const nlohmann::json& content, const std::string& original_updated_time, bool force) { SyncPushResult result; result.success = false; result.http_code = 0; result.server_deleted = false; nlohmann::json body; body["id"] = profile_id; body["name"] = name; body["content"] = content; if (!original_updated_time.empty()) { body["original_updated_time"] = original_updated_time; } // Validate payload size before upload std::string body_str = body.dump(); if (body_str.size() > ORCA_SYNC_MAX_PAYLOAD_SIZE) { result.http_code = 413; // HTTP 413 Payload Too Large result.success = false; result.error_message = "Preset content exceeds 1MB size limit (actual: " + std::to_string(body_str.size()) + " bytes)"; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: sync_push payload too large - " << "size=" << body_str.size() << " bytes, " << "limit=" << ORCA_SYNC_MAX_PAYLOAD_SIZE << " bytes, " << "profile_id=" << profile_id; return result; } std::string response; unsigned int http_code = 0; int http_result = http_post(force ? ORCA_SYNC_FORCE_PUSH_PATH : ORCA_SYNC_PUSH_PATH, body_str, &response, &http_code); result.http_code = http_code; if (http_code == 409) { // Conflict - parse server version try { auto json = nlohmann::json::parse(response); if (json.is_null()) { result.server_deleted = true; } else { auto& profile_data = json["server_profile"]; result.server_version.id = profile_data.value("id", ""); result.server_version.name = profile_data.value("name", ""); result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0); } } catch (...) {} result.error_message = response; return result; } if (http_code != 200) { result.error_message = response; return result; } // Success try { auto json = nlohmann::json::parse(response); result.new_updated_time = json.value(ORCA_JSON_KEY_UPDATE_TIME, 0); if (result.new_updated_time != 0) { result.success = true; } else { result.error_message = "Server response missing required updated_time timestamp"; } } catch (const std::exception& e) { result.error_message = e.what(); } return result; } // ============================================================================ // Sync State Management // ============================================================================ void OrcaCloudServiceAgent::load_sync_state() { std::lock_guard lock(state_mutex); if (sync_state_path.empty()) return; try { std::ifstream ifs(sync_state_path); if (ifs.good()) { std::string line; if (std::getline(ifs, line)) { sync_state.last_sync_timestamp = std::stoll(line); } } } catch (...) { sync_state.last_sync_timestamp = 0; } } void OrcaCloudServiceAgent::save_sync_state() { std::lock_guard lock(state_mutex); if (sync_state_path.empty()) return; try { std::string tmp_path = sync_state_path + ".tmp"; std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc); if (ofs.good()) { ofs << std::to_string(sync_state.last_sync_timestamp); ofs.close(); boost::filesystem::rename(tmp_path, sync_state_path); } } catch (...) {} } void OrcaCloudServiceAgent::clear_sync_state() { std::lock_guard lock(state_mutex); sync_state = SyncState{}; if (!sync_state_path.empty() && boost::filesystem::exists(sync_state_path)) { boost::filesystem::remove(sync_state_path); } } // ============================================================================ // Auth - PKCE and Session Management // ============================================================================ void OrcaCloudServiceAgent::set_session_handler(SessionHandler handler) { session_handler = std::move(handler); } void OrcaCloudServiceAgent::set_on_login_complete_handler(OnLoginCompleteHandler handler) { on_login_complete_handler = std::move(handler); } const OrcaCloudServiceAgent::PkceBundle& OrcaCloudServiceAgent::pkce() { if (pkce_bundle.verifier.empty() || pkce_bundle.challenge.empty() || pkce_bundle.state.empty()) { regenerate_pkce(); } return pkce_bundle; } void OrcaCloudServiceAgent::regenerate_pkce() { pkce_bundle.verifier = generate_code_verifier(); pkce_bundle.challenge = sha256_base64url(pkce_bundle.verifier); pkce_bundle.state = generate_state_token(); if (pkce_bundle.redirect.empty()) { pkce_bundle.redirect = "http://localhost:" + std::to_string(pkce_bundle.loopback_port) + auth_constants::LOOPBACK_PATH; } } void OrcaCloudServiceAgent::update_redirect_uri() { int selected_port = choose_loopback_port(); pkce_bundle.loopback_port = selected_port; pkce_bundle.redirect = "http://localhost:" + std::to_string(selected_port) + auth_constants::LOOPBACK_PATH; } // ============================================================================ // Auth - Token Persistence // ============================================================================ void OrcaCloudServiceAgent::persist_refresh_token(const std::string& token) { if (token.empty()) { clear_refresh_token(); return; } bool stored = false; if (m_use_encrypted_token_file) { // Use encrypted file only auto key = sha256_bytes(get_encryption_key()); if (key.empty()) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot derive key for refresh-token file storage"; return; } std::string payload; if (!aes256gcm_encrypt(token, key, payload)) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to encrypt refresh token for file storage"; return; } std::string signed_payload = payload; if (auto mac = hmac_sha256_hex(payload, key); !mac.empty()) { signed_payload = "v2:" + mac + ":" + payload; } compute_fallback_path(); wxFileName path(wxString::FromUTF8(refresh_fallback_path.c_str())); path.Normalize(); if (!wxFileName::DirExists(path.GetPath())) { wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL); } const std::string tmp_path = refresh_fallback_path + ".tmp"; std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary); if (ofs.good()) { ofs << signed_payload; ofs.flush(); ofs.close(); if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(refresh_fallback_path.c_str()), true)) { stored = true; } else { wxRemoveFile(wxString::FromUTF8(tmp_path.c_str())); BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace refresh-token file"; } } else { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open refresh-token file for write - " << refresh_fallback_path; } } else { // Use wxSecretStore only wxSecretStore store = wxSecretStore::GetDefault(); if (store.IsOk()) { wxSecretValue secret(wxString::FromUTF8(token.c_str())); if (store.Save(SECRET_STORE_SERVICE, SECRET_STORE_USER, secret)) { stored = true; } else { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain save failed"; } } else { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: System Keychain not available"; } } (void) stored; } bool OrcaCloudServiceAgent::load_refresh_token(std::string& out_token) { out_token.clear(); if (m_use_encrypted_token_file) { // Load from encrypted file only compute_fallback_path(); if (wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) { std::ifstream ifs(refresh_fallback_path, std::ios::binary); std::string payload((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); auto key = sha256_bytes(get_encryption_key()); std::string plain; if (!key.empty()) { std::string encoded_payload = payload; bool integrity_ok = true; if (payload.rfind("v2:", 0) == 0) { auto delim = payload.find(':', 3); if (delim == std::string::npos) { BOOST_LOG_TRIVIAL(warning) << "payload missing delim ':'."; integrity_ok = false; } else { std::string stored_hmac = payload.substr(3, delim - 3); std::string lower_stored = stored_hmac; std::transform(lower_stored.begin(), lower_stored.end(), lower_stored.begin(), ::tolower); encoded_payload = payload.substr(delim + 1); std::string computed_hmac = hmac_sha256_hex(encoded_payload, key); std::transform(computed_hmac.begin(), computed_hmac.end(), computed_hmac.begin(), ::tolower); if (computed_hmac.empty() || computed_hmac != lower_stored) { integrity_ok = false; BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: refresh token integrity check failed (HMAC mismatch)"; } } } if (integrity_ok && aes256gcm_decrypt(encoded_payload, key, plain) && !plain.empty()) { out_token = plain; // Upgrade legacy payloads to signed format if (payload.rfind("v2:", 0) != 0) { persist_refresh_token(out_token); } return true; } } } } else { wxSecretStore store = wxSecretStore::GetDefault(); if (store.IsOk()) { wxString username; wxSecretValue secret; if (store.Load(SECRET_STORE_SERVICE, username, secret) && secret.IsOk()) { out_token.assign(static_cast(secret.GetData()), secret.GetSize()); if (!out_token.empty()) { return true; } } } } return false; } void OrcaCloudServiceAgent::clear_refresh_token() { wxSecretStore store = wxSecretStore::GetDefault(); if (store.IsOk()) { store.Delete(SECRET_STORE_SERVICE); } compute_fallback_path(); if (!refresh_fallback_path.empty() && wxFileExists(wxString::FromUTF8(refresh_fallback_path.c_str()))) { wxRemoveFile(wxString::FromUTF8(refresh_fallback_path.c_str())); } } // ============================================================================ // Auth - Token Refresh // ============================================================================ bool OrcaCloudServiceAgent::should_refresh_locked(std::chrono::seconds skew) const { if (!session.logged_in) return false; if (session.expires_at.time_since_epoch().count() == 0) return true; auto now = std::chrono::system_clock::now(); return (session.expires_at - now) <= skew; } bool OrcaCloudServiceAgent::decode_jwt_expiry(const std::string& token, std::chrono::system_clock::time_point& out_tp) { out_tp = {}; if (token.empty()) return false; auto first = token.find('.'); auto second = token.find('.', first == std::string::npos ? 0 : first + 1); if (first == std::string::npos || second == std::string::npos) return false; std::string payload_b64 = token.substr(first + 1, second - first - 1); std::vector payload_bytes; if (!base64url_decode(payload_b64, payload_bytes)) return false; std::string payload_str(payload_bytes.begin(), payload_bytes.end()); try { auto payload = json::parse(payload_str); if (payload.contains("exp") && payload["exp"].is_number()) { out_tp = std::chrono::system_clock::time_point{ std::chrono::seconds(payload["exp"].get()) }; return true; } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: failed to decode JWT exp - " << e.what(); } return false; } RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_token, const std::string& reason, bool async) { if (refresh_token.empty()) return RefreshResult::AuthRejected; // nothing to refresh bool expected = false; if (!refresh_running.compare_exchange_strong(expected, true)) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: refresh already running, skip (reason=" << reason << ")"; // Another refresh is already in flight. Treat as transient so we keep the session // instead of logging out: that in-flight refresh surfaces its own Success/AuthRejected, // so a genuine rejection is only deferred to the next request, never lost. return RefreshResult::Transient; } auto worker = [this, refresh_token, reason]() { (void) reason; RefreshResult r = refresh_session_with_token(refresh_token); refresh_running.store(false); return r; }; if (async) { if (refresh_thread.joinable()) { refresh_thread.join(); } refresh_thread = std::thread([worker]() { worker(); }); // Fire-and-forget: the outcome isn't known yet and no current caller consumes it. // Return Transient (indeterminate) rather than implying a completed, successful refresh. return RefreshResult::Transient; } return worker(); } RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async) { std::string refresh_token = get_refresh_token(); if (refresh_token.empty()) { load_refresh_token(refresh_token); } if (refresh_token.empty()) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")"; return RefreshResult::AuthRejected; // no persisted token: nothing to preserve } return refresh_now(refresh_token, reason, async); } bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason) { bool needs_refresh = false; { std::lock_guard lock(session_mutex); needs_refresh = should_refresh_locked(skew); } if (!needs_refresh) return true; if (refresh_from_storage(reason, false) == RefreshResult::Success) return true; std::this_thread::sleep_for(std::chrono::milliseconds(750)); return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success; } // Maps a token-refresh HTTP outcome to a RefreshResult. http_code == 0 means the // server could not be reached; session_established is only meaningful for a 2xx body. static RefreshResult classify_refresh_result(unsigned http_code, bool session_established) { if (http_code == 0) return RefreshResult::Transient; // no response: network/transport failure if (http_code == 400 || http_code == 401 || http_code == 403) return RefreshResult::AuthRejected; // refresh token rejected if (http_code >= 400) return RefreshResult::Transient; // rate-limit (429), server error (5xx) or other 4xx: keep the session return session_established ? RefreshResult::Success // 2xx with a usable session : RefreshResult::Transient; // 2xx but unusable body } RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token) { std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}"; std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token"; std::string response; unsigned int http_code = 0; // http_post_token sets http_code to 0 when the server could not be reached. http_post_token(body, &response, &http_code, url); bool established = false; if (http_code >= 200 && http_code < 300) { if (session_handler) { established = session_handler(response); } else { // No session handler set - parse the token response directly and establish the // session, so OrcaCloudServiceAgent is self-contained without external setup. try { established = set_user_session(json::parse(response)); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what(); } } } else { std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response; BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code << ", response_body=" << truncated_response; } return classify_refresh_result(http_code, established); } // ============================================================================ // Auth - Session State Helpers // ============================================================================ bool OrcaCloudServiceAgent::set_user_session(const std::string& token, const std::string& user_id, const std::string& username, const std::string& nickname, const std::string& avatar, const std::string& refresh_token) { std::chrono::system_clock::time_point exp_tp{}; decode_jwt_expiry(token, exp_tp); { std::lock_guard lock(session_mutex); session.access_token = token; session.refresh_token = refresh_token; session.user_id = user_id; session.user_name = username; session.user_nickname = nickname; session.user_avatar = avatar; session.expires_at = exp_tp; session.logged_in = true; } if (!refresh_token.empty()) { persist_refresh_token(refresh_token); } // Set per-user sync state path if (!config_dir.empty() && !user_id.empty()) { boost::filesystem::path user_dir = boost::filesystem::path(config_dir) / "user" / user_id; if (!boost::filesystem::exists(user_dir)) { boost::filesystem::create_directories(user_dir); } sync_state_path = (user_dir / ORCA_SYNC_STATE_FILE).string(); load_sync_state(); } BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: set_user_session - user_id=" << user_id << ", username=" << username; return true; } bool OrcaCloudServiceAgent::set_user_session(const json& session_json, bool notify_login) { std::string access_token = get_json_string_field(session_json, "access_token"); if (access_token.empty()) { access_token = get_json_string_field(session_json, "token"); } std::string refresh_token = get_json_string_field(session_json, "refresh_token"); std::string user_id, username, nickname, avatar; if (session_json.contains("user") && session_json["user"].is_object()) { // Nested format (Orca cloud / GoTrue response) const auto& user = session_json["user"]; user_id = get_json_string_field(user, "id"); if (user.contains("user_metadata") && user["user_metadata"].is_object()) { const auto& meta = user["user_metadata"]; username = get_json_string_field(meta, "username"); // Orca Cloud's unique username // Orca Cloud's primary display name field is display_name. // Fallback to different names from different providers if display_name is not set. nickname = resolve_display_name( get_json_string_field(meta, "display_name"), get_json_string_field(meta, "nickname"), get_json_string_field(meta, "full_name"), get_json_string_field(meta, "name"), username); avatar = get_json_string_field(meta, "avatar_url"); } } else { // Flat format (WebView direct token flow) user_id = get_json_string_field(session_json, "user_id"); username = get_json_string_field(session_json, "username"); nickname = resolve_display_name( get_json_string_field(session_json, "display_name"), get_json_string_field(session_json, "nickname"), get_json_string_field(session_json, "full_name"), get_json_string_field(session_json, "name"), username); avatar = get_json_string_field(session_json, "avatar"); } if (access_token.empty() || user_id.empty()) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: session payload missing access_token or user id"; return false; } bool success = set_user_session(access_token, user_id, username, nickname, avatar, refresh_token); if (success && notify_login && on_login_complete_handler) { on_login_complete_handler(true, user_id); } return success; } void OrcaCloudServiceAgent::clear_session() { { std::lock_guard lock(session_mutex); session = SessionInfo{}; } clear_refresh_token(); } // ============================================================================ // HTTP Helpers // ============================================================================ RefreshResult OrcaCloudServiceAgent::attempt_refresh_after_unauthorized(const std::string& reason) { RefreshResult r = refresh_from_storage(reason, false); if (r != RefreshResult::Transient) return r; // Success or AuthRejected: decided, no retry // Only a transient (network/server) failure is worth retrying. std::this_thread::sleep_for(std::chrono::milliseconds(500)); r = refresh_from_storage(reason + "_retry", false); if (r == RefreshResult::Transient) BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=transient source=" << reason << " action=keep_session"; else if (r == RefreshResult::AuthRejected) BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh result=rejected source=" << reason << " action=logout"; return r; } std::map OrcaCloudServiceAgent::data_headers() { std::scoped_lock lock(state_mutex, headers_mutex); auto headers = auth_headers; for (const auto& pair : extra_headers) { headers[pair.first] = pair.second; } return headers; } bool OrcaCloudServiceAgent::resolve_unauthorized(HttpResult& res, const std::function& perform, const std::string& reason) { if (res.status != 401) return false; RefreshResult rr = attempt_refresh_after_unauthorized(reason); if (rr == RefreshResult::Success) { res = perform(); // refreshed: retry the original request with the new token return false; } // Transient (no connection / 5xx / 429 / ambiguous): keep the session and token, // suppress the auth error so the GUI does not log the user out. // AuthRejected (refresh token genuinely rejected): let the 401 surface -> logout. return rr == RefreshResult::Transient; } int OrcaCloudServiceAgent::http_get(const std::string& path, std::string* response_body, unsigned int* http_code) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: GET " << url; if (!ensure_token_fresh("http_get_" + path)) BOOST_LOG_TRIVIAL(warning) << "ensure_token_fresh returned false"; auto perform = [&]() { HttpResult result; try { auto http = Http::get(url); http.tls_verify(true); std::string token = get_access_token(); auto headers = data_headers(); for (const auto& pair : headers) { http.header(pair.first, pair.second); } if (!token.empty()) { http.header("Authorization", "Bearer " + token); } http.on_complete([&](std::string body, unsigned resp_status) { result.success = true; result.status = resp_status; result.body = body; }) .on_error([&](std::string body, std::string error, unsigned resp_status) { result.success = false; result.status = resp_status == 0 ? 404 : resp_status; result.body = body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; }) .timeout_max(30) .perform_sync(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_get exception - " << e.what(); } return result; }; HttpResult res = perform(); bool suppress = resolve_unauthorized(res, perform, "http_get_" + path); if (response_body) *response_body = res.body; if (http_code) *http_code = res.status; if (!suppress && (!res.success || res.status >= 400)) { invoke_http_error_callback(res.status, res.body); } return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; ensure_token_fresh("http_post_" + path); auto perform = [&]() { HttpResult result; try { auto http = Http::post(url); http.tls_verify(true); std::string token = get_access_token(); auto headers = data_headers(); for (const auto& pair : headers) { http.header(pair.first, pair.second); } if (!token.empty()) { http.header("Authorization", "Bearer " + token); } http.header("Content-Type", "application/json"); http.set_post_body(body); http.on_complete([&](std::string resp_body, unsigned resp_status) { result.success = true; result.status = resp_status; result.body = resp_body; }) .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { result.success = false; result.status = resp_status == 0 ? 404 : resp_status; result.body = resp_body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; }) .timeout_max(30) .perform_sync(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post exception - " << e.what(); } return result; }; HttpResult res = perform(); bool suppress = resolve_unauthorized(res, perform, "http_post_" + path); if (response_body) *response_body = res.body; if (http_code) *http_code = res.status; if (!suppress && (!res.success || res.status >= 400)) { invoke_http_error_callback(res.status, res.body); } return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } int OrcaCloudServiceAgent::http_put(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: PUT " << url; ensure_token_fresh("http_put_" + path); auto perform = [&]() { HttpResult result; try { auto http = Http::put(url); http.tls_verify(true); std::string token = get_access_token(); auto headers = data_headers(); for (const auto& pair : headers) { http.header(pair.first, pair.second); } if (!token.empty()) { http.header("Authorization", "Bearer " + token); } http.header("Content-Type", "application/json"); http.set_post_body(body); http.on_complete([&](std::string resp_body, unsigned resp_status) { result.success = true; result.status = resp_status; result.body = resp_body; }) .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { result.success = false; result.status = resp_status == 0 ? 404 : resp_status; result.body = resp_body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; }) .timeout_max(30) .perform_sync(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_put exception - " << e.what(); } return result; }; HttpResult res = perform(); bool suppress = resolve_unauthorized(res, perform, "http_put_" + path); if (response_body) *response_body = res.body; if (http_code) *http_code = res.status; if (!suppress && (!res.success || res.status >= 400)) { invoke_http_error_callback(res.status, res.body); } return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } int OrcaCloudServiceAgent::http_delete(const std::string& path, std::string* response_body, unsigned int* http_code) { std::string url = api_base_url + path; BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: DELETE " << url; ensure_token_fresh("http_delete_" + path); auto perform = [&]() { HttpResult result; try { auto http = Http::del(url); http.tls_verify(true); std::string token = get_access_token(); auto headers = data_headers(); for (const auto& pair : headers) { http.header(pair.first, pair.second); } if (!token.empty()) { http.header("Authorization", "Bearer " + token); } http.on_complete([&](std::string resp_body, unsigned resp_status) { result.success = true; result.status = resp_status; result.body = resp_body; }) .on_error([&](std::string resp_body, std::string error, unsigned resp_status) { result.success = false; result.status = resp_status == 0 ? 404 : resp_status; result.body = resp_body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; }) .timeout_max(30) .perform_sync(); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_delete exception - " << e.what(); } return result; }; HttpResult res = perform(); bool suppress = resolve_unauthorized(res, perform, "http_delete_" + path); if (response_body) *response_body = res.body; if (http_code) *http_code = res.status; if (!suppress && (!res.success || res.status >= 400)) { invoke_http_error_callback(res.status, res.body); } return (res.success && !suppress) ? BAMBU_NETWORK_SUCCESS : BAMBU_NETWORK_ERR_CONNECT_FAILED; } bool OrcaCloudServiceAgent::http_post_token(const std::string& body, std::string* response_body, unsigned int* http_code, const std::string& custom_url) { std::map headers_copy; std::string url; { std::lock_guard lock(headers_mutex); url = custom_url.empty() ? (auth_base_url + auth_constants::TOKEN_PATH) : custom_url; headers_copy = extra_headers; } // Add auth headers { std::lock_guard lock(state_mutex); for (const auto& pair : auth_headers) { headers_copy[pair.first] = pair.second; } } BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST " << url; bool has_apikey = false; for (const auto& pair : headers_copy) { if (pair.first == "apikey") has_apikey = true; } if (!has_apikey) { BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: http_post_token - apikey header MISSING! Token request will likely fail."; } try { auto http = Http::post(url); http.tls_verify(true); for (const auto& pair : headers_copy) { http.header(pair.first, pair.second); } http.remove_header("Authorization"); http.remove_header("Content-Type"); http.header("Content-Type", "application/json"); http.set_post_body(body); bool success = false; unsigned int status = 0; std::string resp_body; http.on_complete([&](std::string body, unsigned resp_status) { success = true; status = resp_status; resp_body = body; }) .on_error([&](std::string body, std::string error, unsigned resp_status) { success = false; status = resp_status; // keep 0 for "no response" so the refresh classifier sees a transport failure resp_body = body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error; }) .timeout_max(30) .perform_sync(); if (response_body) *response_body = resp_body; if (http_code) *http_code = status; return success; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post_token exception - " << e.what(); if (http_code) *http_code = 0; return false; } } bool OrcaCloudServiceAgent::http_post_auth(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code) { std::string url = auth_base_url + path + "?scope=local"; std::string token; std::map headers_copy; { std::lock_guard lock(session_mutex); token = session.access_token; } { std::lock_guard lock(headers_mutex); headers_copy = extra_headers; } { std::lock_guard lock(state_mutex); for (const auto& pair : auth_headers) { headers_copy[pair.first] = pair.second; } } BOOST_LOG_TRIVIAL(trace) << "OrcaCloudServiceAgent: POST (auth) " << url; try { auto http = Http::post(url); http.tls_verify(true); for (const auto& pair : headers_copy) { http.header(pair.first, pair.second); } if (!token.empty()) { http.header("Authorization", "Bearer " + token); } http.remove_header("Content-Type"); http.header("Content-Type", "application/json"); http.set_post_body(body); bool success = false; unsigned int status = 0; std::string resp_body; http.on_complete([&](std::string body, unsigned resp_status) { success = true; status = resp_status; resp_body = body; }) .on_error([&](std::string body, std::string error, unsigned resp_status) { success = false; status = resp_status == 0 ? 404 : resp_status; resp_body = body; BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP (auth) error - " << error; }) .timeout_max(30) .perform_sync(); if (response_body) *response_body = resp_body; if (http_code) *http_code = status; return success; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: http_post_auth exception - " << e.what(); if (http_code) *http_code = 0; return false; } } void OrcaCloudServiceAgent::compute_fallback_path() { if (!refresh_fallback_path.empty()) return; wxFileName fallback(wxStandardPaths::Get().GetUserDataDir(), "orca_refresh_token.sec"); fallback.Normalize(); refresh_fallback_path = fallback.GetFullPath().ToStdString(); } // ============================================================================ // JSON Helpers // ============================================================================ std::string OrcaCloudServiceAgent::map_to_json(const std::map& map) { nlohmann::json j; for (const auto& pair : map) { j[pair.first] = pair.second; } return j.dump(); } void OrcaCloudServiceAgent::json_to_map(const std::string& json, std::map& map) { try { auto j = nlohmann::json::parse(json); for (auto it = j.begin(); it != j.end(); ++it) { if (it.value().is_string()) { map[it.key()] = it.value().get(); } else { map[it.key()] = it.value().dump(); } } } catch (...) {} } // ============================================================================ // Callback Invocation // ============================================================================ void OrcaCloudServiceAgent::invoke_server_connected_callback(int return_code, int reason_code) { AppOnServerConnectedFn callback; QueueOnMainFn queue_fn; { std::lock_guard lock(state_mutex); callback = on_server_connected_fn; queue_fn = queue_on_main_fn; } if (callback) { CloudEvent event{ORCA_CLOUD_PROVIDER}; if (queue_fn) { queue_fn([callback, event, return_code, reason_code]() { callback(event, return_code, reason_code); }); } else { callback(event, return_code, reason_code); } } } void OrcaCloudServiceAgent::invoke_http_error_callback(unsigned http_code, const std::string& http_body) { AppOnHttpErrorFn callback; QueueOnMainFn queue_fn; { std::lock_guard lock(state_mutex); callback = on_http_error_fn; queue_fn = queue_on_main_fn; } if (callback) { CloudEvent event{ORCA_CLOUD_PROVIDER}; if (queue_fn) { queue_fn([callback, event, http_code, http_body]() { callback(event, http_code, http_body); }); } else { callback(event, http_code, http_body); } } } // ============================================================================ // Callback Registration // ============================================================================ int OrcaCloudServiceAgent::set_on_server_connected_fn(AppOnServerConnectedFn fn) { std::lock_guard lock(state_mutex); on_server_connected_fn = fn; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_on_http_error_fn(AppOnHttpErrorFn fn) { std::lock_guard lock(state_mutex); on_http_error_fn = fn; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_get_country_code_fn(GetCountryCodeFn fn) { std::lock_guard lock(state_mutex); get_country_code_fn = fn; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::set_queue_on_main_fn(QueueOnMainFn fn) { std::lock_guard lock(state_mutex); queue_on_main_fn = fn; return BAMBU_NETWORK_SUCCESS; } // ============================================================================ // Stub Implementations (Cloud Services, Model Mall, Analytics, Ratings) // ============================================================================ int OrcaCloudServiceAgent::get_my_message(int type, int after, int limit, unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_my_message (stub)"; if (http_code) *http_code = 200; if (http_body) *http_body = "[]"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::check_user_task_report(int* task_id, bool* printable) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: check_user_task_report (stub)"; if (task_id) *task_id = 0; if (printable) *printable = false; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_user_print_info(unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_print_info (stub)"; if (http_code) *http_code = 200; if (http_body) *http_body = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_user_tasks(TaskQueryParams params, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_tasks (stub)"; if (http_body) *http_body = "[]"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_printer_firmware(std::string dev_id, unsigned* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_printer_firmware (stub)"; if (http_code) *http_code = 200; if (http_body) *http_body = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_task_plate_index(std::string task_id, int* plate_index) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_task_plate_index (stub)"; if (plate_index) *plate_index = 0; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_user_info(int* identifier) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_user_info (stub)"; if (identifier) *identifier = 0; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_subtask_info(std::string subtask_id, std::string* task_json, unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_subtask_info (stub)"; if (task_json) *task_json = "{}"; if (http_code) *http_code = 200; if (http_body) *http_body = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_slice_info(std::string project_id, std::string profile_id, int plate_index, std::string* slice_json) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_slice_info (stub)"; if (slice_json) *slice_json = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::query_bind_status(std::vector query_list, unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: query_bind_status (stub)"; if (http_code) *http_code = 200; if (http_body) *http_body = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::modify_printer_name(std::string dev_id, std::string dev_name) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: modify_printer_name (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_camera_url(std::string dev_id, std::function callback) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_camera_url (stub)"; if (callback) callback(""); return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_design_staffpick(int offset, int limit, std::function callback) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_design_staffpick (stub)"; if (callback) callback("[]"); return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::start_publish(PublishParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, std::string* out) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: start_publish (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_model_publish_url(std::string* url) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_publish_url (stub)"; if (url) *url = ""; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_subtask(BBLModelTask* task, OnGetSubTaskFn getsub_fn) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_subtask (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_model_mall_home_url(std::string* url) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_home_url (stub)"; if (url) *url = ""; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_model_mall_detail_url(std::string* url, std::string id) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_detail_url (stub)"; if (url) *url = ""; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_my_profile(std::string token, unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_my_profile (stub)"; if (http_code) *http_code = 200; if (http_body) *http_body = "{}"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_my_token(std::string ticket, unsigned int* http_code, std::string* http_body) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_my_token (stub) - Orca cloud uses code-based OAuth, not tickets"; if (http_code) *http_code = 0; if (http_body) *http_body = ""; return -1; } int OrcaCloudServiceAgent::track_enable(bool enable) { std::lock_guard lock(state_mutex); enable_track = enable; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::track_remove_files() { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_remove_files (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::track_event(std::string evt_key, std::string content) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_event (stub) - " << evt_key; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::track_header(std::string header) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_header (stub)"; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::track_update_property(std::string name, std::string value, std::string type) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_update_property (stub) - " << name; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::track_get_property(std::string name, std::string& value, std::string type) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: track_get_property (stub) - " << name; value = ""; return BAMBU_NETWORK_SUCCESS; } bool OrcaCloudServiceAgent::get_track_enable() { std::lock_guard lock(state_mutex); return enable_track; } int OrcaCloudServiceAgent::put_model_mall_rating(int design_id, int score, std::string content, std::vector images, unsigned int& http_code, std::string& http_error) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: put_model_mall_rating (stub)"; http_code = 200; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_oss_config(std::string& config, std::string country_code, unsigned int& http_code, std::string& http_error) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_oss_config (stub)"; config = "{}"; http_code = 200; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::put_rating_picture_oss(std::string& config, std::string& pic_oss_path, std::string model_id, int profile_id, unsigned int& http_code, std::string& http_error) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: put_rating_picture_oss (stub)"; http_code = 200; return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_model_mall_rating_result(int job_id, std::string& rating_result, unsigned int& http_code, std::string& http_error) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_model_mall_rating_result (stub)"; rating_result = "{}"; http_code = 200; return BAMBU_NETWORK_SUCCESS; } std::string OrcaCloudServiceAgent::get_cloud_service_host() { return api_base_url; } std::string OrcaCloudServiceAgent::get_cloud_login_url(const std::string& language) { std::string url = cloud_base_url + ORCA_CLOUD_LOGIN_PATH; if (!language.empty()) { url += "?lang=" + language; } return url; } int OrcaCloudServiceAgent::get_mw_user_preference(std::function callback) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_preference (stub)"; if (callback) callback("{}"); return BAMBU_NETWORK_SUCCESS; } int OrcaCloudServiceAgent::get_mw_user_4ulist(int seed, int limit, std::function callback) { BOOST_LOG_TRIVIAL(debug) << "OrcaCloudServiceAgent: get_mw_user_4ulist (stub)"; if (callback) callback("[]"); return BAMBU_NETWORK_SUCCESS; } std::string OrcaCloudServiceAgent::get_version() { return "OrcaCloudServiceAgent 1.0.0"; } // ============================================================================ // Bundle Subscription Implementation // ============================================================================ bool OrcaCloudServiceAgent::unsubscribe_bundle(const std::string& bundle_id) { std::string path = std::string(ORCA_SUBSCRIPTIONS_PATH) + "/" + bundle_id; std::string response; unsigned int http_code = 0; int result = http_delete(path, &response, &http_code); if (http_code >= 400) { return false; } return true; } std::string OrcaCloudServiceAgent::get_bundle_url(const std::string& bundle_id) const { return cloud_base_url + "/app/bundles/subscribed-bundles/" + bundle_id; } int OrcaCloudServiceAgent::get_subscribed_bundles(std::vector>* bundles,std::vector& notfound, std::vector& unauthorized) { if (!bundles) return -1; std::string response_body; unsigned int http_code = 0; // GET /api/v1/bundles int result = http_get("/api/v1/subscriptions", &response_body, &http_code); if (result != 0 || http_code != 200) { BOOST_LOG_TRIVIAL(error) << "get_subscribed_bundles failed: http_code=" << http_code << ", response=" << response_body; return result != 0 ? result : http_code; } // Parse JSON response try { auto json = nlohmann::json::parse(response_body); if (!json.contains("data") || !json["data"].is_array()) { BOOST_LOG_TRIVIAL(error) << "get_subscribed_bundles: invalid response format"; return -1; } if(json.contains("not_found") ) { //not found warning for(const auto& not_found : json["not_found"]) { notfound.push_back(not_found); } } if(json.contains("unauthorised") ) { // populate something to be iterated to show warning notifications for(const auto& u : json["unauthorised"]) { unauthorized.push_back(u); } } // if(json.contains("privated") && json["privated"].is_array() && !json["privated"].empty()) // { // // populate something to be iterated to show warning notifications // for(const auto& not_found : json["privated"]) // { // } // } for (const auto& bundle_json : json["data"]) { bundles->push_back(std::make_pair(bundle_json["id"].get(), bundle_json["version"].get())); } BOOST_LOG_TRIVIAL(info) << "get_subscribed_bundles: loaded " << bundles->size() << " bundles"; return 0; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "get_subscribed_bundles: JSON parse error: " << e.what(); return -1; } } int OrcaCloudServiceAgent::get_shared_bundle(const std::string& bundle_id, std::map>* presets, BundleMetadata* bundle_metadata) { if (!presets) return -1; std::string response_body; unsigned int http_code = 0; // GET /api/v1/bundles/{id} std::string path = "/api/v1/bundles/" + bundle_id; int result = http_get(path, &response_body, &http_code); if (result != 0 || http_code != 200) { BOOST_LOG_TRIVIAL(error) << "get_shared_bundle failed: bundle_id=" << bundle_id << ", http_code=" << http_code; return result != 0 ? result : http_code; } // Parse JSON response try { auto json = nlohmann::json::parse(response_body); BOOST_LOG_TRIVIAL(info) << "get_shared_bundle: response: " << response_body; BOOST_LOG_TRIVIAL(info) << "get_shared_bundle: shared_profile: " << json["shared_profiles"]; // Parse the bundle metadata if (json.contains("id")) bundle_metadata->id = json["id"].get(); if (json.contains("name")) bundle_metadata->name = json["name"].get(); if (json.contains("version")) bundle_metadata->version = json["version"].get(); if (json.contains("description")) bundle_metadata->description = json["description"].get(); if (json.contains("author")) bundle_metadata->author = json["author"].get(); if (json.contains("updated_time")) bundle_metadata->updated_time = json["updated_time"].get(); if (!json.contains("shared_profiles") || !json["shared_profiles"].is_array()) { BOOST_LOG_TRIVIAL(error) << "get_shared_bundle: invalid response format"; return -1; } for (auto& preset_object : json["shared_profiles"]) { BOOST_LOG_TRIVIAL(info) << "shared profile object: " << preset_object; // Extract preset name and content std::string preset_name = preset_object.value("name", ""); if (preset_name.empty()) { BOOST_LOG_TRIVIAL(warning) << "get_shared_bundle: preset has no name, skipping"; continue; } // Parse content JSON into key-value pairs using helper (same as get_user_presets) std::map value_map; if (preset_object.contains("content") && preset_object["content"].is_object()) { json_to_map(preset_object["content"].dump(), value_map); } // Add metadata fields to match get_user_presets format // These are required by PresetCollection::load_user_preset if (preset_object.contains("id") && value_map.find(BBL_JSON_KEY_SETTING_ID) == value_map.end()) { value_map[BBL_JSON_KEY_SETTING_ID] = preset_object["id"]; } if (value_map.find(BBL_JSON_KEY_USER_ID) == value_map.end()) { // Bundle presets don't have a user_id in the traditional sense // Use bundle_id as a placeholder to indicate source value_map[BBL_JSON_KEY_USER_ID] = "bundle:" + bundle_id; } if (preset_object.contains("updated_time") && value_map.find(ORCA_JSON_KEY_UPDATE_TIME) == value_map.end()) { value_map[ORCA_JSON_KEY_UPDATE_TIME] = preset_object["updated_time"].dump(); } if (value_map.find(BBL_JSON_KEY_NAME) == value_map.end()) { value_map[BBL_JSON_KEY_NAME] = preset_name; } // Store as: presets[preset_name][key] = value // This matches the format expected by PresetBundle::load_user_presets (*presets)[preset_name] = value_map; } BOOST_LOG_TRIVIAL(info) << "get_shared_bundle: loaded " << presets->size() << " presets for bundle_id=" << bundle_id; return 0; } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(error) << "get_shared_bundle: JSON parse error: " << e.what(); return -1; } } } // namespace Slic3r