#include "supabase_client.h" #include #include #include #include #include #include using json = nlohmann::json; namespace { // Supabase constants (publishable anon key) const std::string SUPABASE_URL = "https://xzxpmvyamjvtxpwnjpad.supabase.co"; const std::string SUPABASE_ANON_KEY = "sb_publishable_GqQx844xYDizO9-ytlBXfA_MVT6N7yA"; std::string buildUrl(const std::string &path) { std::string url = SUPABASE_URL; if (!url.empty() && url.back() == '/') url.pop_back(); url += "/rest/v1/" + path; return url; } size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t realSize = size * nmemb; std::string *s = reinterpret_cast(userp); s->append(reinterpret_cast(contents), realSize); return realSize; } struct CurlInit { CurlInit() { curl_global_init(CURL_GLOBAL_DEFAULT); } ~CurlInit() { curl_global_cleanup(); } }; static CurlInit g_curl_init; } namespace supabase { static bool g_verbose = false; void SetVerbose(bool enabled) { g_verbose = enabled; } void SubmitHighscoreAsync(const ScoreEntry &entry) { std::thread([entry]() { try { CURL* curl = curl_easy_init(); if (!curl) return; std::string url = buildUrl("highscores"); json j; j["score"] = entry.score; j["lines"] = entry.lines; j["level"] = entry.level; j["time_sec"] = static_cast(std::lround(entry.timeSec)); j["name"] = entry.name; j["game_type"] = entry.gameType; j["timestamp"] = static_cast(std::time(nullptr)); std::string body = j.dump(); struct curl_slist *headers = nullptr; std::string h1 = std::string("apikey: ") + SUPABASE_ANON_KEY; std::string h2 = std::string("Authorization: Bearer ") + SUPABASE_ANON_KEY; headers = curl_slist_append(headers, h1.c_str()); headers = curl_slist_append(headers, h2.c_str()); headers = curl_slist_append(headers, "Content-Type: application/json"); std::string resp; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); if (g_verbose) { std::cerr << "[Supabase] POST " << url << "\n"; std::cerr << "[Supabase] Body: " << body << "\n"; } CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { if (g_verbose) std::cerr << "[Supabase] POST error: " << curl_easy_strerror(res) << "\n"; } else { long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (g_verbose) { std::cerr << "[Supabase] POST response code: " << http_code << " body_len=" << resp.size() << "\n"; if (!resp.empty()) std::cerr << "[Supabase] POST response: " << resp << "\n"; } } curl_slist_free_all(headers); curl_easy_cleanup(curl); } catch (...) { // swallow errors } }).detach(); } std::vector FetchHighscores(const std::string &gameType, int limit) { std::vector out; try { CURL* curl = curl_easy_init(); if (!curl) return out; std::string path = "highscores"; // Clamp limit to max 10 to keep payloads small int l = std::clamp(limit, 1, 10); std::string query; if (!gameType.empty()) { if (gameType == "challenge") { query = "?game_type=eq." + gameType + "&order=level.desc,time_sec.asc&limit=" + std::to_string(l); } else { query = "?game_type=eq." + gameType + "&order=score.desc&limit=" + std::to_string(l); } } else { query = "?order=score.desc&limit=" + std::to_string(l); } std::string url = buildUrl(path) + query; struct curl_slist *headers = nullptr; headers = curl_slist_append(headers, ("apikey: " + SUPABASE_ANON_KEY).c_str()); headers = curl_slist_append(headers, ("Authorization: Bearer " + SUPABASE_ANON_KEY).c_str()); headers = curl_slist_append(headers, "Content-Type: application/json"); std::string resp; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); if (g_verbose) std::cerr << "[Supabase] GET " << url << "\n"; CURLcode res = curl_easy_perform(curl); if (res == CURLE_OK) { long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); if (g_verbose) { std::cerr << "[Supabase] GET response code: " << http_code << " body_len=" << resp.size() << "\n"; if (!resp.empty()) std::cerr << "[Supabase] GET response: " << resp << "\n"; } try { auto j = json::parse(resp); if (j.is_array()) { for (auto &v : j) { ScoreEntry e{}; if (v.contains("score")) e.score = v["score"].get(); if (v.contains("lines")) e.lines = v["lines"].get(); if (v.contains("level")) e.level = v["level"].get(); if (v.contains("time_sec")) { try { e.timeSec = v["time_sec"].get(); } catch(...) { e.timeSec = v["time_sec"].get(); } } else if (v.contains("timestamp")) { e.timeSec = v["timestamp"].get(); } if (v.contains("name")) e.name = v["name"].get(); if (v.contains("game_type")) e.gameType = v["game_type"].get(); out.push_back(e); } } } catch (...) { if (g_verbose) std::cerr << "[Supabase] GET parse error" << std::endl; } } else { if (g_verbose) std::cerr << "[Supabase] GET error: " << curl_easy_strerror(res) << "\n"; } curl_slist_free_all(headers); curl_easy_cleanup(curl); } catch (...) { // swallow } return out; } } // namespace supabase