initial commit
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
#include "network/api_client.h"
|
||||
#include "network/json.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <string>
|
||||
|
||||
#ifndef API_KEY
|
||||
#define API_KEY ""
|
||||
#endif
|
||||
|
||||
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<std::string*>(userdata);
|
||||
out->append(ptr, size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
// Shared curl setup + perform. Returns a populated HttpResponse.
|
||||
HttpResponse performRequest(const std::string& url, const std::string& apiKey,
|
||||
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;
|
||||
// An explicit apiKey wins; otherwise fall back to the key baked in at
|
||||
// build time via -DAPI_KEY (empty string when unset).
|
||||
const std::string effectiveKey = apiKey.empty() ? API_KEY : apiKey;
|
||||
if (!effectiveKey.empty()) {
|
||||
headers = curl_slist_append(headers, ("Authorization: Bearer " + effectiveKey).c_str());
|
||||
}
|
||||
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<long>(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, const std::string& apiKey) {
|
||||
return performRequest(urlFor(path), apiKey, false, "");
|
||||
}
|
||||
|
||||
HttpResponse httpPost(const std::string& path, const std::string& jsonBody,
|
||||
const std::string& apiKey) {
|
||||
return performRequest(urlFor(path), apiKey, true, jsonBody);
|
||||
}
|
||||
|
||||
// ─── GET /frequencies ──────────────────────────────────────────────────
|
||||
|
||||
bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& 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<int>(entry.numberField("id"));
|
||||
if (freq.id == 0) continue;
|
||||
|
||||
freq.name = entry.stringField("name");
|
||||
freq.value = static_cast<int>(entry.numberField("value"));
|
||||
out.push_back(std::move(freq));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fetchFrequencies(std::vector<Frequency>& out, const std::string& apiKey) {
|
||||
HttpResponse response = httpGet(PATH_FREQUENCIES, apiKey);
|
||||
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<ColorPalette>& 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<uint8_t>(nibble(base) * 16 + nibble(base + 1));
|
||||
c.g = static_cast<uint8_t>(nibble(base + 2) * 16 + nibble(base + 3));
|
||||
c.b = static_cast<uint8_t>(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<int>(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<ColorPalette>& out, const std::string& apiKey) {
|
||||
HttpResponse response = httpGet(PATH_COLOR_PALETTES, apiKey);
|
||||
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");
|
||||
|
||||
// subscription_level is an object {id,name,description} in the updated API
|
||||
// and a bare level-name string in the current one. Tolerate both, plus a
|
||||
// flat {subscription_level, subscription_level_id, subscription_level_description}.
|
||||
const radiantwave::json::JsonValue* level = root.find("subscription_level");
|
||||
if (level && level->type == radiantwave::json::JsonValue::Type::Object) {
|
||||
out.subscription_level.id = static_cast<int>(level->numberField("id"));
|
||||
out.subscription_level.name = level->stringField("name");
|
||||
out.subscription_level.description = level->stringField("description");
|
||||
} else if (level && level->type == radiantwave::json::JsonValue::Type::String) {
|
||||
out.subscription_level.name = level->str;
|
||||
}
|
||||
|
||||
if (out.subscription_level.name.empty()) {
|
||||
out.subscription_level.name = root.stringField("subscription_level");
|
||||
}
|
||||
if (out.subscription_level.id == 0) {
|
||||
out.subscription_level.id = static_cast<int>(root.numberField("subscription_level_id"));
|
||||
}
|
||||
if (out.subscription_level.description.empty()) {
|
||||
out.subscription_level.description = root.stringField("subscription_level_description");
|
||||
}
|
||||
|
||||
return !out.key.empty();
|
||||
}
|
||||
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& apiKey) {
|
||||
LicenseResult result;
|
||||
|
||||
const std::string jsonBody =
|
||||
"{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}";
|
||||
HttpResponse response = httpPost(PATH_LICENSE, jsonBody, apiKey);
|
||||
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& subscription_level,
|
||||
const std::string& name, const std::string& apiKey) {
|
||||
AccountResult result;
|
||||
|
||||
std::string jsonBody =
|
||||
"{\"email\":\"" + email + "\",\"subscription_level\":\"" + subscription_level + "\"";
|
||||
if (!name.empty()) {
|
||||
jsonBody += ",\"name\":\"" + name + "\"";
|
||||
}
|
||||
jsonBody += "}";
|
||||
|
||||
HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody, apiKey);
|
||||
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
|
||||
Reference in New Issue
Block a user