#include "network/api_client.h" #include "network/json.h" #include #include namespace radiantwave::api { namespace { constexpr long HTTP_OK = 200; // Builds the full URL for a path relative to the base API URL. std::string urlFor(const std::string& path) { std::string url = API_BASE_URL; if (!path.empty() && path.front() != '/') { url += '/'; } url += path; return url; } size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { auto* out = static_cast(userdata); out->append(ptr, size * nmemb); return size * nmemb; } // Shared curl setup + perform. Returns a populated HttpResponse. HttpResponse performRequest(const std::string& url, bool post, const std::string& jsonBody) { HttpResponse response; CURL* curl = curl_easy_init(); if (!curl) return response; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "RadiantWave-Kiosk/0.1"); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); struct curl_slist* headers = nullptr; if (post) { headers = curl_slist_append(headers, "Content-Type: application/json"); curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(jsonBody.size())); } if (headers) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); } CURLcode res = curl_easy_perform(curl); long status = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); curl_slist_free_all(headers); curl_easy_cleanup(curl); response.status = status; response.transportOk = (res == CURLE_OK); return response; } } // anonymous namespace // ─── HTTP helpers ────────────────────────────────────────────────────── HttpResponse httpGet(const std::string& path) { return performRequest(urlFor(path), false, ""); } HttpResponse httpPost(const std::string& path, const std::string& jsonBody) { return performRequest(urlFor(path), true, jsonBody); } // ─── GET /frequencies ────────────────────────────────────────────────── bool parseFrequenciesJson(const std::string& body, std::vector& out) { radiantwave::json::JsonValue root; if (!radiantwave::json::parseDocument(body, root) || root.type != radiantwave::json::JsonValue::Type::Array) { return false; } out.clear(); for (const auto& entry : root.array) { if (entry.type != radiantwave::json::JsonValue::Type::Object) continue; Frequency freq; freq.id = static_cast(entry.numberField("id")); if (freq.id == 0) continue; freq.name = entry.stringField("name"); freq.value = static_cast(entry.numberField("value")); out.push_back(std::move(freq)); } return true; } bool fetchFrequencies(std::vector& out) { HttpResponse response = httpGet(PATH_FREQUENCIES); if (!response.transportOk || response.status < 200 || response.status >= 300) { return false; } return parseFrequenciesJson(response.body, out); } // ─── GET /color-palettes ─────────────────────────────────────────────── bool parseColorPalettesJson(const std::string& body, std::vector& out) { radiantwave::json::JsonValue root; if (!radiantwave::json::parseDocument(body, root) || root.type != radiantwave::json::JsonValue::Type::Array) { return false; } auto parseHex = [](const std::string& hex) -> PaletteColor { PaletteColor c; auto nibble = [&hex](size_t i) -> int { char ch = hex.size() > i ? hex[i] : '0'; if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; return 0; }; // Accept "#RRGGBB" or "RRGGBB". size_t base = (!hex.empty() && hex[0] == '#') ? 1 : 0; c.r = static_cast(nibble(base) * 16 + nibble(base + 1)); c.g = static_cast(nibble(base + 2) * 16 + nibble(base + 3)); c.b = static_cast(nibble(base + 4) * 16 + nibble(base + 5)); return c; }; out.clear(); for (const auto& entry : root.array) { if (entry.type != radiantwave::json::JsonValue::Type::Object) continue; int freqId = static_cast(entry.numberField("solfeggioFrequencyId")); if (freqId == 0) continue; ColorPalette palette; palette.solfeggio_frequency_id = freqId; const radiantwave::json::JsonValue* colors = entry.find("colors"); if (colors && colors->type == radiantwave::json::JsonValue::Type::Array) { for (const auto& color : colors->array) { if (color.type == radiantwave::json::JsonValue::Type::String) { palette.colors.push_back(parseHex(color.str)); } } } out.push_back(std::move(palette)); } return true; } bool fetchColorPalettes(std::vector& out) { HttpResponse response = httpGet(PATH_COLOR_PALETTES); if (!response.transportOk || response.status < 200 || response.status >= 300) { return false; } return parseColorPalettesJson(response.body, out); } // ─── POST /license ───────────────────────────────────────────────────── bool parseLicenseJson(const std::string& body, LicenseInfo& out) { radiantwave::json::JsonValue root; if (!radiantwave::json::parseDocument(body, root) || root.type != radiantwave::json::JsonValue::Type::Object) { return false; } out.key = root.stringField("key"); out.email_address = root.stringField("email_address"); // Any other fields (e.g. the legacy `subscription_level`) are ignored. A // successful validation itself is the "subscribed" flag; the kiosk treats // a 2xx response as an active license. return !out.key.empty(); } LicenseResult validateLicense(const std::string& key, const std::string& email) { LicenseResult result; const std::string jsonBody = "{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}"; HttpResponse response = httpPost(PATH_LICENSE, jsonBody); result.status = response.status; if (!response.transportOk) { result.error_message = "network error"; return result; } if (result.ok()) { parseLicenseJson(response.body, result.info); } else { result.error_message = radiantwave::json::extractErrorMessage(response.body); } return result; } // ─── POST /automations/create/account ────────────────────────────────── bool parseCreateAccountJson(const std::string& body, AccountBody& out) { radiantwave::json::JsonValue root; if (!radiantwave::json::parseDocument(body, root) || root.type != radiantwave::json::JsonValue::Type::Object) { return false; } out.success = root.boolField("success"); out.message = root.stringField("message"); out.user_id = root.stringField("user_id"); return true; } AccountResult createAccount(const std::string& email, const std::string& name) { AccountResult result; std::string jsonBody = "{\"email\":\"" + email + "\""; if (!name.empty()) { jsonBody += ",\"name\":\"" + name + "\""; } jsonBody += "}"; HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody); result.status = response.status; if (!response.transportOk) { result.error_message = "network error"; return result; } if (result.ok()) { parseCreateAccountJson(response.body, result.body); } else { result.error_message = radiantwave::json::extractErrorMessage(response.body); } return result; } } // namespace radiantwave::api