Files
radiantwave/src/network/proclamation_sync.cpp
T

294 lines
10 KiB
C++

#include "network/proclamation_sync.h"
#include "db/db_keys.h"
#include "network/api_client.h"
#include "network/json.h"
#include <string>
namespace radiantwave::sync {
namespace {
// ─── HTML stripping ────────────────────────────────────────────────────
std::string replaceAll(std::string text, const std::string& from, const std::string& to) {
if (from.empty()) return text;
size_t pos = 0;
while ((pos = text.find(from, pos)) != std::string::npos) {
text.replace(pos, from.size(), to);
pos += to.size();
}
return text;
}
std::string stripHtml(const std::string& in) {
std::string out;
out.reserve(in.size());
bool inTag = false;
for (char c : in) {
if (c == '<') {
inTag = true;
} else if (c == '>') {
inTag = false;
} else if (!inTag) {
out += c;
}
}
out = replaceAll(std::move(out), "&nbsp;", " ");
out = replaceAll(std::move(out), "&amp;", "&");
out = replaceAll(std::move(out), "&lt;", "<");
out = replaceAll(std::move(out), "&gt;", ">");
out = replaceAll(std::move(out), "&quot;", "\"");
out = replaceAll(std::move(out), "&#39;", "'");
out = replaceAll(std::move(out), "&apos;", "'");
return out;
}
} // anonymous namespace
// ─── Public API ────────────────────────────────────────────────────────
bool parseProclamationsJson(const std::string& body,
std::vector<radiantwave::db::ProclamationRecord>& out) {
json::JsonValue root;
if (!json::parseDocument(body, root) || root.type != json::JsonValue::Type::Array) {
return false;
}
out.clear();
for (const auto& entry : root.array) {
if (entry.type != json::JsonValue::Type::Object) continue;
radiantwave::db::ProclamationRecord rec;
rec.id = entry.stringField("id");
if (rec.id.empty()) continue;
rec.title = stripHtml(entry.stringField("name"));
rec.content = stripHtml(entry.stringField("content"));
rec.category_id = static_cast<int>(entry.numberField("categoryId"));
rec.group_id = static_cast<int>(entry.numberField("groupId"));
rec.solfeggio_frequency_id = static_cast<int>(entry.numberField("solfeggioFrequencyId"));
out.push_back(std::move(rec));
}
return true;
}
bool parseCategoriesJson(const std::string& body,
std::vector<radiantwave::db::ProclamationCategoryRecord>& out) {
json::JsonValue root;
if (!json::parseDocument(body, root) || root.type != json::JsonValue::Type::Array) {
return false;
}
out.clear();
for (const auto& entry : root.array) {
if (entry.type != json::JsonValue::Type::Object) continue;
radiantwave::db::ProclamationCategoryRecord rec;
rec.id = static_cast<int>(entry.numberField("id"));
if (rec.id == 0) continue;
rec.name = entry.stringField("name");
rec.sort_order = static_cast<int>(entry.numberField("sortOrder"));
out.push_back(std::move(rec));
}
return true;
}
bool parseGroupsJson(const std::string& body,
std::vector<radiantwave::db::ProclamationGroupRecord>& out) {
json::JsonValue root;
if (!json::parseDocument(body, root) || root.type != json::JsonValue::Type::Array) {
return false;
}
out.clear();
for (const auto& entry : root.array) {
if (entry.type != json::JsonValue::Type::Object) continue;
radiantwave::db::ProclamationGroupRecord rec;
rec.id = static_cast<int>(entry.numberField("id"));
if (rec.id == 0) continue;
rec.name = entry.stringField("name");
rec.sort_order = static_cast<int>(entry.numberField("sortOrder"));
out.push_back(std::move(rec));
}
return true;
}
bool syncProclamationsFromApi(radiantwave::db::DatabaseManager& db,
const std::string& defaultProclamationId) {
// TODO(portal #4/#5): once the portal exposes color palettes, sync them
// alongside proclamations here (syncColorPalettesFromApi or
// syncProclamationPalettesFromApi) so every caller gets palettes in one step.
api::HttpResponse proclamationsResponse =
api::httpGet(api::PATH_PROCLAMATIONS);
if (!proclamationsResponse.transportOk ||
proclamationsResponse.status < 200 || proclamationsResponse.status >= 300) {
return false;
}
api::HttpResponse categoriesResponse =
api::httpGet(api::PATH_CATEGORIES);
if (!categoriesResponse.transportOk ||
categoriesResponse.status < 200 || categoriesResponse.status >= 300) {
return false;
}
api::HttpResponse groupsResponse =
api::httpGet(api::PATH_GROUPS);
if (!groupsResponse.transportOk ||
groupsResponse.status < 200 || groupsResponse.status >= 300) {
return false;
}
std::vector<radiantwave::db::ProclamationRecord> proclamations;
std::vector<radiantwave::db::ProclamationCategoryRecord> categories;
std::vector<radiantwave::db::ProclamationGroupRecord> groups;
if (!parseProclamationsJson(proclamationsResponse.body, proclamations) ||
!parseCategoriesJson(categoriesResponse.body, categories) ||
!parseGroupsJson(groupsResponse.body, groups)) {
return false;
}
if (!db.replaceCategories(categories)) {
return false;
}
if (!db.replaceGroups(groups)) {
return false;
}
if (!db.syncProclamations(proclamations, defaultProclamationId)) {
return false;
}
// Best-effort: a schedule fetch/store failure must never fail the
// proclamation sync or roll back the data just written.
syncScheduleFromApi(db);
return true;
}
bool syncScheduleFromApi(radiantwave::db::DatabaseManager& db) {
const std::string email = db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
const std::string license_key = db.getKV(radiantwave::db::KV_LICENSE_KEY);
if (email.empty() || license_key.empty()) {
return false;
}
api::ScheduleResponse schedule;
if (!api::fetchSchedule(email, license_key, schedule)) {
return false;
}
std::vector<radiantwave::db::ScheduleBlockRecord> records;
records.reserve(schedule.blocks.size());
for (const auto& block : schedule.blocks) {
radiantwave::db::ScheduleBlockRecord rec;
rec.id = block.id;
rec.start_minutes = block.start_minutes;
rec.end_minutes = block.end_minutes;
rec.overnight = block.overnight;
rec.duration_minutes = block.duration_minutes;
rec.dynamic_audio = block.dynamic_audio;
rec.proclamation_ids = block.proclamation_ids;
records.push_back(std::move(rec));
}
return db.replaceScheduleBlocks(records);
}
// ─── Color palettes (readiness code; not wired into the app flow) ─────
bool parseProclamationPalettesJson(const std::string& body,
std::vector<radiantwave::db::PaletteRecord>& out) {
json::JsonValue root;
if (!json::parseDocument(body, root) || root.type != json::JsonValue::Type::Array) {
return false;
}
auto parseHex = [](const std::string& hex) -> radiantwave::db::PaletteColor {
radiantwave::db::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 != json::JsonValue::Type::Object) continue;
int freqId = static_cast<int>(entry.numberField("solfeggioFrequencyId"));
if (freqId == 0) continue;
const json::JsonValue* colors = entry.find("colorPalette");
if (!colors || colors->type != json::JsonValue::Type::Array || colors->array.empty()) {
continue;
}
// When several proclamations share a frequency, the first wins.
bool exists = false;
for (const auto& existing : out) {
if (existing.solfeggio_frequency_id == freqId) {
exists = true;
break;
}
}
if (exists) continue;
radiantwave::db::PaletteRecord rec;
rec.solfeggio_frequency_id = freqId;
for (const auto& color : colors->array) {
if (color.type == json::JsonValue::Type::String) {
rec.colors.push_back(parseHex(color.str));
}
}
out.push_back(std::move(rec));
}
return true;
}
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db) {
std::vector<api::ColorPalette> palettes;
if (!api::fetchColorPalettes(palettes)) {
return false;
}
std::vector<radiantwave::db::PaletteRecord> records;
records.reserve(palettes.size());
for (const auto& p : palettes) {
radiantwave::db::PaletteRecord rec;
rec.solfeggio_frequency_id = p.solfeggio_frequency_id;
rec.colors.reserve(p.colors.size());
for (const auto& c : p.colors) {
rec.colors.push_back({c.r, c.g, c.b, c.a});
}
records.push_back(std::move(rec));
}
return db.replaceColorPalettes(records);
}
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db) {
api::HttpResponse response = api::httpGet(api::PATH_PROCLAMATIONS);
if (!response.transportOk || response.status < 200 || response.status >= 300) {
return false;
}
std::vector<radiantwave::db::PaletteRecord> records;
if (!parseProclamationPalettesJson(response.body, records)) {
return false;
}
return db.replaceColorPalettes(records);
}
} // namespace radiantwave::sync