Adds api_client support for GET /licenses/{key}/schedule with If-None-Match
and target-payload parsing (version/type/daily/weekly + legacy blocks), an
atomic on-disk schedule cache, a pure schedule engine (half-open blocks,
midnight self-wrap for daily, Sunday->Monday spillover for weekly, invalid
blocks skipped with a warning), and a background ScheduleManager that loads
the cache offline and pulls on boot and every 5 minutes. Adds engine/cache
tests against the shared docs/fixtures, including 304, corrupt cache, and
offline cases.
234 lines
9.5 KiB
C++
234 lines
9.5 KiB
C++
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace radiantwave::api {
|
|
|
|
// Base URL of the RadiantWave content API.
|
|
constexpr const char* API_BASE_URL = "https://portal.radiantwavetech.com/api/v2";
|
|
|
|
// Paths relative to API_BASE_URL.
|
|
constexpr const char* PATH_PROCLAMATIONS = "/proclamations";
|
|
constexpr const char* PATH_CATEGORIES = "/categories";
|
|
constexpr const char* PATH_GROUPS = "/groups";
|
|
constexpr const char* PATH_FREQUENCIES = "/frequencies";
|
|
constexpr const char* PATH_LICENSE = "/license";
|
|
constexpr const char* PATH_CREATE_ACCOUNT = "/automations/create/account";
|
|
constexpr const char* PATH_COLOR_PALETTES = "/color-palettes";
|
|
constexpr const char* PATH_SCHEDULE = "/schedule";
|
|
|
|
// Result of a single HTTP request.
|
|
struct HttpResponse {
|
|
long status = 0; // HTTP status code; 0 if the request never completed
|
|
std::string body; // raw response body (any status)
|
|
std::string etag; // response ETag header, if any
|
|
bool transportOk = false; // true when the request completed without a transport error
|
|
};
|
|
|
|
// Performs a GET/POST/PUT against `path` (relative to API_BASE_URL).
|
|
HttpResponse httpGet(const std::string& path);
|
|
HttpResponse httpPost(const std::string& path, const std::string& jsonBody);
|
|
HttpResponse httpPut(const std::string& path, const std::string& jsonBody);
|
|
|
|
// GET with an optional If-None-Match header. Captures the response ETag.
|
|
HttpResponse httpGetConditional(const std::string& path, const std::string& ifNoneMatch);
|
|
|
|
// ─── GET /frequencies ──────────────────────────────────────────────────
|
|
|
|
struct Frequency {
|
|
int id = 0;
|
|
std::string name;
|
|
int value = 0; // frequency in Hz
|
|
};
|
|
|
|
// Parse the JSON payload returned by the frequencies endpoint.
|
|
bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out);
|
|
|
|
// Fetch the solfeggio frequencies. Returns true only on HTTP 2xx + successful parse.
|
|
bool fetchFrequencies(std::vector<Frequency>& out);
|
|
|
|
// ─── GET /color-palettes ───────────────────────────────────────────────
|
|
|
|
// A single color swatch (RGBA). Mirrors db::PaletteColor without coupling the
|
|
// API layer to the database.
|
|
struct PaletteColor {
|
|
uint8_t r = 0;
|
|
uint8_t g = 0;
|
|
uint8_t b = 0;
|
|
uint8_t a = 255;
|
|
};
|
|
|
|
// The ordered 7-color palette for one solfeggio frequency.
|
|
struct ColorPalette {
|
|
int solfeggio_frequency_id = 0;
|
|
std::vector<PaletteColor> colors;
|
|
};
|
|
|
|
// Parse the JSON payload returned by the color-palettes endpoint. Each entry
|
|
// is expected to look like {"solfeggioFrequencyId": 1, "colors": ["#RRGGBB", ...]}.
|
|
// Returns false on malformed input.
|
|
bool parseColorPalettesJson(const std::string& body, std::vector<ColorPalette>& out);
|
|
|
|
// Fetch the color palettes for every solfeggio frequency. Returns true only on
|
|
// HTTP 2xx + successful parse.
|
|
//
|
|
// NOTE: the portal does not expose this route yet. This is readiness code — it
|
|
// is deliberately not called anywhere in the app until the route ships, so
|
|
// palettes can be updated from the portal rather than code releases.
|
|
bool fetchColorPalettes(std::vector<ColorPalette>& out);
|
|
|
|
// ─── POST /license ─────────────────────────────────────────────────────
|
|
|
|
struct LicenseInfo {
|
|
std::string key;
|
|
std::string email_address;
|
|
// Advisory System Name status returned by the portal: one of "ok",
|
|
// "unchanged", "conflict", "invalid", or empty when the portal did not
|
|
// report one (e.g. older portal).
|
|
std::string system_name_status;
|
|
};
|
|
|
|
// Parse the 200 JSON payload returned by the license endpoint.
|
|
bool parseLicenseJson(const std::string& body, LicenseInfo& out);
|
|
|
|
struct LicenseResult {
|
|
int status = 0;
|
|
LicenseInfo info;
|
|
std::string error_message; // extracted from the error body on failure
|
|
|
|
bool ok() const { return status >= 200 && status < 300; }
|
|
};
|
|
|
|
// Validate a license key / email pair against the portal. Never throws; failures
|
|
// (network, HTTP error status) are reported through the returned LicenseResult.
|
|
// A 2xx response means the license is valid and the subscription is active.
|
|
// When `systemName` is non-empty it is sent as the additive `systemName` field
|
|
// (the Kiosk is the only writer); the response's `systemNameStatus` is advisory.
|
|
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
|
const std::string& systemName = "");
|
|
|
|
// ─── PUT /licenses/{key}/system-name ───────────────────────────────────
|
|
|
|
// Result of a System Name PUT. `status` is the HTTP status (0 on transport
|
|
// failure); `transport_ok` is false when the request never completed.
|
|
struct PutNameResult {
|
|
int status = 0;
|
|
bool transport_ok = false;
|
|
|
|
bool ok() const { return status >= 200 && status < 300; }
|
|
};
|
|
|
|
// Set the System Name for a License Key. 200 on success, 400 invalid_name,
|
|
// 404 unknown_license, 409 name_taken. Never throws.
|
|
PutNameResult putSystemName(const std::string& key, const std::string& name);
|
|
|
|
// ─── POST /automations/create/account ──────────────────────────────────
|
|
|
|
struct AccountBody {
|
|
bool success = false;
|
|
std::string message;
|
|
std::string user_id;
|
|
};
|
|
|
|
// Parse the 200 JSON payload returned by the create-account endpoint.
|
|
bool parseCreateAccountJson(const std::string& body, AccountBody& out);
|
|
|
|
struct AccountResult {
|
|
int status = 0;
|
|
AccountBody body;
|
|
std::string error_message;
|
|
|
|
bool ok() const { return status >= 200 && status < 300; }
|
|
};
|
|
|
|
// Creates (or reuses) a user account and issues a license key. The key is emailed
|
|
// to the user and is NOT part of the response. `name` may be empty.
|
|
AccountResult createAccount(const std::string& email, const std::string& name = "");
|
|
|
|
// ─── GET /schedule ─────────────────────────────────────────────────────
|
|
|
|
// One scheduled listening block. Wall-clock times are carried as minutes from
|
|
// midnight; overnight blocks wrap past midnight (end clock time <= start).
|
|
struct ScheduleBlock {
|
|
std::string id;
|
|
int start_minutes = 0;
|
|
int end_minutes = 0;
|
|
bool overnight = false;
|
|
int duration_minutes = 0;
|
|
// When true the kiosk enables dynamic audio for the block's duration,
|
|
// overriding the device's audio selection. Defaults to true: schedules are
|
|
// dynamic unless the portal explicitly says otherwise.
|
|
bool dynamic_audio = true;
|
|
// Selected proclamation ids, in play order.
|
|
std::vector<std::string> proclamation_ids;
|
|
};
|
|
|
|
struct ScheduleResponse {
|
|
std::string email;
|
|
std::vector<ScheduleBlock> blocks;
|
|
};
|
|
|
|
// Parse the JSON payload returned by the schedule endpoint. `dynamicAudio` is
|
|
// optional and defaults to true when absent. Returns false on malformed input.
|
|
bool parseScheduleJson(const std::string& body, ScheduleResponse& out);
|
|
|
|
// Fetch a user's schedule. Authenticates via the paired `email` + `license_key`
|
|
// query parameters. Returns true only on HTTP 2xx + successful parse; failures
|
|
// (including a missing/blank pair) leave `out` untouched.
|
|
bool fetchSchedule(const std::string& email, const std::string& license_key,
|
|
ScheduleResponse& out);
|
|
|
|
// ─── GET /licenses/{key}/schedule (Phase 3, conditional) ───────────────
|
|
|
|
// One Block of the target payload. `day` is "" for daily schedules and
|
|
// "mon".."sun" for weekly schedules. Times are wall-clock minutes.
|
|
struct TargetBlock {
|
|
std::string id;
|
|
std::string day; // "" for daily
|
|
int start_minutes = 0;
|
|
int end_minutes = 0;
|
|
bool overnight = false;
|
|
int duration_minutes = 0;
|
|
bool dynamic_audio = true;
|
|
std::vector<std::string> proclamation_ids;
|
|
};
|
|
|
|
// The target schedule payload (version/type/daily/weekly).
|
|
struct SchedulePayload {
|
|
std::string version;
|
|
// "daily", "weekly", or "" for no schedule (type null).
|
|
std::string type;
|
|
std::vector<TargetBlock> daily;
|
|
// Weekly blocks keyed by day (mon..sun); empty for daily/none.
|
|
std::vector<TargetBlock> weekly;
|
|
// Legacy `blocks` flattening carried for backward compatibility.
|
|
std::vector<ScheduleBlock> legacy_blocks;
|
|
|
|
bool hasSchedule() const { return type == "daily" || type == "weekly"; }
|
|
};
|
|
|
|
// Parse the target payload (with version/type/daily/weekly). `blocks` (legacy)
|
|
// is also read so older responses still populate `legacy_blocks`. Returns false
|
|
// on malformed input.
|
|
bool parseSchedulePayloadJson(const std::string& body, SchedulePayload& out);
|
|
|
|
// Result of a conditional schedule fetch.
|
|
enum class ScheduleFetchStatus {
|
|
Ok, // 200; payload replaced
|
|
NotModified, // 304; caller keeps its cached payload
|
|
NotFound, // 404 unknown license
|
|
NetworkError, // transport/other failure
|
|
};
|
|
|
|
// Fetch a License Key's schedule with conditional GET. When `etag` is
|
|
// non-empty it is sent as If-None-Match. On success `out` is replaced and
|
|
// `outEtag` receives the response ETag. Never throws.
|
|
ScheduleFetchStatus fetchScheduleForLicense(const std::string& license_key,
|
|
const std::string& etag,
|
|
SchedulePayload& out,
|
|
std::string& outEtag);
|
|
|
|
} // namespace radiantwave::api
|