#include "obn/cloud_filament.hpp" #include "obn/agent.hpp" #include "obn/bambu_networking.hpp" #include "obn/cloud_auth.hpp" #include "obn/http_client.hpp" #include "obn/log.hpp" #include #include namespace obn::cloud_filament { namespace { std::string base_v2(Agent* a) { return obn::cloud::api_host(a->cloud_region()) + "/v1/design-user-service/my/filament/v2"; } std::string base_config(Agent* a) { return obn::cloud::api_host(a->cloud_region()) + "/v1/design-user-service/filament/config"; } // Build "?k1=v1&k2=v2" out of pairs, skipping empties. // We mirror Studio's ListFilamentV2Req schema: offset / limit are always // emitted (server defaults differ from local defaults), the rest only // when non-empty. std::string build_list_query(const BBL::FilamentQueryParams& p) { std::ostringstream os; char sep = '?'; auto append = [&](const char* k, const std::string& v) { if (v.empty()) return; os << sep << k << '=' << obn::http::url_encode(v); sep = '&'; }; auto append_int = [&](const char* k, int v) { os << sep << k << '=' << v; sep = '&'; }; append_int("offset", p.offset); append_int("limit", p.limit); append("category", p.category); append("status", p.status); // Studio's CloudClient maps three local key spellings (ids / // spoolId / spool_id) into FilamentQueryParams::spool_id, then // forwards a single comma-separated string. The cloud's swagger // calls the field `ids`. append("ids", p.spool_id); append("RFIDs", p.rfid); return os.str(); } // JSON-escape `s` into a quoted string literal. Keeps the implementation // dependency-free; we only need the small subset of escapes that string // ids / RFIDs may contain. std::string json_str(const std::string& s) { std::string out; out.reserve(s.size() + 2); out.push_back('"'); for (char c : s) { switch (c) { case '\\': out += "\\\\"; break; case '"': out += "\\\""; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (static_cast(c) < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", c); out += buf; } else { out.push_back(c); } } } out.push_back('"'); return out; } // Build {"ids":[...],"RFIDs":[...]} from a FilamentDeleteParams. We send // ids as JSON strings (matches the live MITM trace and the way // wgtFilaManagerCloudClient::batch_delete forwards them); the Studio // dispatcher upstream converts numeric-looking ids to int64 before // re-stringifying so the server accepts both forms. std::string build_delete_body(const BBL::FilamentDeleteParams& p) { std::ostringstream os; os << '{'; bool first = true; auto emit_arr = [&](const char* key, const std::vector& v) { if (v.empty()) return; if (!first) os << ','; first = false; os << '"' << key << "\":["; for (size_t i = 0; i < v.size(); ++i) { if (i) os << ','; os << json_str(v[i]); } os << ']'; }; emit_arr("ids", p.ids); emit_arr("RFIDs", p.rfids); os << '}'; return os.str(); } // Common prologue: ensure we're logged in and grab the auth headers. // Returns false (and stamps an empty body) when no session is available // so callers can short-circuit with the right BAMBU_NETWORK_* code. bool prepare(Agent* a, std::string* out_body, std::map* hdrs) { if (out_body) out_body->clear(); if (!a) return false; *hdrs = a->cloud_api_http_headers(); return hdrs->find("Authorization") != hdrs->end(); } } // namespace int list(Agent* a, const BBL::FilamentQueryParams& params, std::string* out_body) { std::map hdrs; if (!prepare(a, out_body, &hdrs)) { OBN_WARN("cloud_filament::list: not logged in"); return BAMBU_NETWORK_ERR_GET_FILAMENTS_FAILED; } // GET: avoid Content-Type so picky backends don't 415. hdrs.erase("Content-Type"); const std::string url = base_v2(a) + build_list_query(params); auto resp = obn::http::get_json(url, hdrs); OBN_INFO("cloud_filament::list http=%ld bytes=%zu (offset=%d limit=%d cat='%s' status='%s' ids='%s' rfid='%s')", resp.status_code, resp.body.size(), params.offset, params.limit, params.category.c_str(), params.status.c_str(), params.spool_id.c_str(), params.rfid.c_str()); if (out_body) *out_body = resp.body; if (!resp.error.empty() || resp.status_code < 200 || resp.status_code >= 300) { OBN_WARN("cloud_filament::list failed: http=%ld err=%s", resp.status_code, resp.error.c_str()); return BAMBU_NETWORK_ERR_GET_FILAMENTS_FAILED; } return BAMBU_NETWORK_SUCCESS; } int create(Agent* a, const std::string& request_body, std::string* out_body) { std::map hdrs; if (!prepare(a, out_body, &hdrs)) { OBN_WARN("cloud_filament::create: not logged in"); return BAMBU_NETWORK_ERR_CREATE_FILAMENT_FAILED; } auto resp = obn::http::post_json(base_v2(a), request_body, hdrs); OBN_INFO("cloud_filament::create http=%ld bytes=%zu req_len=%zu", resp.status_code, resp.body.size(), request_body.size()); if (out_body) *out_body = resp.body; if (!resp.error.empty() || resp.status_code < 200 || resp.status_code >= 300) { OBN_WARN("cloud_filament::create failed: http=%ld err=%s body=%s", resp.status_code, resp.error.c_str(), resp.body.c_str()); return BAMBU_NETWORK_ERR_CREATE_FILAMENT_FAILED; } return BAMBU_NETWORK_SUCCESS; } int update(Agent* a, const std::string& spool_id, const std::string& request_body, std::string* out_body) { std::map hdrs; if (!prepare(a, out_body, &hdrs)) { OBN_WARN("cloud_filament::update: not logged in"); return BAMBU_NETWORK_ERR_UPDATE_FILAMENT_FAILED; } obn::http::Request req; req.method = obn::http::Method::PUT; req.url = base_v2(a); req.headers = hdrs; req.body = request_body; auto resp = obn::http::perform(req); OBN_INFO("cloud_filament::update id='%s' http=%ld bytes=%zu req_len=%zu", spool_id.c_str(), resp.status_code, resp.body.size(), request_body.size()); if (out_body) *out_body = resp.body; if (!resp.error.empty() || resp.status_code < 200 || resp.status_code >= 300) { OBN_WARN("cloud_filament::update failed: http=%ld err=%s body=%s", resp.status_code, resp.error.c_str(), resp.body.c_str()); return BAMBU_NETWORK_ERR_UPDATE_FILAMENT_FAILED; } return BAMBU_NETWORK_SUCCESS; } int batch_delete(Agent* a, const BBL::FilamentDeleteParams& params, std::string* out_body) { std::map hdrs; if (!prepare(a, out_body, &hdrs)) { OBN_WARN("cloud_filament::batch_delete: not logged in"); return BAMBU_NETWORK_ERR_DELETE_FILAMENT_FAILED; } if (params.ids.empty() && params.rfids.empty()) { OBN_WARN("cloud_filament::batch_delete: nothing to delete"); return BAMBU_NETWORK_ERR_DELETE_FILAMENT_FAILED; } obn::http::Request req; req.method = obn::http::Method::DEL; req.url = base_v2(a) + "/batch"; req.headers = hdrs; req.body = build_delete_body(params); auto resp = obn::http::perform(req); OBN_INFO("cloud_filament::batch_delete http=%ld ids=%zu rfids=%zu", resp.status_code, params.ids.size(), params.rfids.size()); if (out_body) *out_body = resp.body; if (!resp.error.empty() || resp.status_code < 200 || resp.status_code >= 300) { OBN_WARN("cloud_filament::batch_delete failed: http=%ld err=%s body=%s", resp.status_code, resp.error.c_str(), resp.body.c_str()); return BAMBU_NETWORK_ERR_DELETE_FILAMENT_FAILED; } return BAMBU_NETWORK_SUCCESS; } int config(Agent* a, std::string* out_body) { std::map hdrs; if (!prepare(a, out_body, &hdrs)) { OBN_WARN("cloud_filament::config: not logged in"); return BAMBU_NETWORK_ERR_GET_FILAMENT_CONFIG_FAILED; } hdrs.erase("Content-Type"); auto resp = obn::http::get_json(base_config(a), hdrs); OBN_INFO("cloud_filament::config http=%ld bytes=%zu", resp.status_code, resp.body.size()); if (out_body) *out_body = resp.body; if (!resp.error.empty() || resp.status_code < 200 || resp.status_code >= 300) { OBN_WARN("cloud_filament::config failed: http=%ld err=%s", resp.status_code, resp.error.c_str()); return BAMBU_NETWORK_ERR_GET_FILAMENT_CONFIG_FAILED; } return BAMBU_NETWORK_SUCCESS; } } // namespace obn::cloud_filament