#include "okapi.h" #include "debug.h" #include #include #include #include #include #include using json = nlohmann::json; static const std::string OKAPI_logs = "services/logs/userlogs"; static const std::string OKAPI_cache = "services/caches/geocache"; static const std::string OKAPI_caches = "services/caches/geocaches"; static const std::string OKAPI_username = "services/users/by_username"; static const std::string OKAPI_user = "services/users/user"; static const std::string OKAPI_changelog = "services/replicate/changelog"; Okapi::Okapi(const std::string& server_url, const std::string& consumer_key) : url(server_url), key(consumer_key) { if (url.find(".pl/") != std::string::npos) service = "OC.pl"; if (url.find(".de/") != std::string::npos) service = "OC.de"; if (url.find(".us/") != std::string::npos) service = "OC.us"; if (url.find(".nl/") != std::string::npos) service = "OC.nl"; if (url.find(".ro/") != std::string::npos) service = "OC.ro"; if (url.find(".uk/") != std::string::npos) service = "OC.uk"; } // Callback for cURL easy interface used to save received output to std::string object size_t Okapi::write_cb(char* ptr, __attribute__((unused)) size_t size, size_t nmemb, void* userdata) { std::string* str = reinterpret_cast(userdata); str->append(ptr, nmemb); return nmemb; } std::string Okapi::curl_post(const std::string& url, const std::string& post) const { CURL* curl; CURLcode res; std::string output; Debug(5) << "API query: " << post; curl = curl_easy_init(); if (!curl) { curl_global_cleanup(); throw 0; } curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&output); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5); res = curl_easy_perform(curl); if (res != CURLE_OK) { Debug(1) << "Connection failed: " << curl_easy_strerror(res) << '\n'; curl_easy_cleanup(curl); std::exit(EXIT_FAILURE); } curl_easy_cleanup(curl); Debug(5) << "API query result: " << output; if (output.starts_with("{\"error\"")) { json j = json::parse(output); Debug(1) << "OKAPI error: " << j["error"]["developer_message"]; std::exit(EXIT_FAILURE); } return output; } std::string Okapi::get_user_caches_json(const std::string& uuid, int count, int offset) const { std::string service = url + OKAPI_logs; std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=cache_code|type|date|was_recommended&limit=" + std::to_string(count) + "&offset=" + std::to_string(offset); return curl_post(service, query); } // std::string Okapi::get_cache_json(std::string code) { // std::string service = url + OKAPI_cache; // std::string query = "consumer_key=" + key + "&cache_code=" + code + "&fields=code|name|location|type|status|difficulty|terrain"; // return curl_post(service, query); // } std::string Okapi::get_caches_json(const std::string& codes) const { std::string service = url + OKAPI_caches; std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|name|location|type|status|difficulty|terrain|owner|region|size2|date_created|recommendations|founds|status"; return curl_post(service, query); } // Cache Okapi::get_cache(std::string code) { // std::string output = get_cache_json(code); // json j = json::parse(output); // // Cache c; // c.code = code; // c.name = j["name"]; // c.type = j["type"]; // c.diff = j["difficulty"].get(); // c.terr = j["terrain"].get(); // // std::cout << j["difficulty"] << '\n'; // // std::cout << j["difficulty"].get() << '\n'; // // std::cout << j["difficulty"].get() << '\n'; // // std::cout << j["difficulty"].get() << '\n'; // // std::string loc = j["location"]; // int pos = loc.find("|"); // //c.pos.lat = stof(loc.substr(0, pos)); // //c.pos.lon = stof(loc.substr(pos + 1)); // // return c; // } Caches Okapi::get_caches(const std::set& codes) const { Cache c; Caches cc; uint k; std::string codes_list; codes_list.reserve(MAX_CACHES * 8); // maximum of MAX_CACHES codes, 7 chars per code plus a separator auto it = codes.begin(); while (it != codes.end()) { k = 0; while (it != codes.end() && k < MAX_CACHES) { codes_list += *it; codes_list += '|'; it++; k++; } codes_list.pop_back(); // remove trailing '|' std::string output = get_caches_json(codes_list); json j = json::parse(output); codes_list.clear(); for (auto& el : j.items()) { c.code = el.value()["code"]; c.name = el.value()["name"]; c.type = el.value()["type"]; c.size = el.value()["size2"]; c.region = el.value()["region"]; c.diff = el.value()["difficulty"]; c.terr = el.value()["terrain"]; c.owner = el.value()["owner"]["username"]; c.owner_uuid = el.value()["owner"]["uuid"]; c.pos = Position(el.value()["location"]); c.origin = service; c.fav = el.value()["recommendations"]; c.founds = el.value()["founds"]; if (el.value()["status"] == "Available") c.status = ok; else if (el.value()["status"] == "Temporarily unavailable") c.status = disabled; else if (el.value()["status"] == "Archived") c.status = archived; else c.status = unknown; std::tm tmp; std::stringstream ss(el.value()["date_created"].get()); ss >> std::get_time(&tmp, "%Y-%m-%dT%H:%M:%S+"); c.set_date_hidden(tmp); cc.push_back(c); } } return cc; } Caches Okapi::get_user_caches(const std::string& uuid, int count) const { Caches cc; std::set codes; struct extra_data { std::tm date; bool was_recommended; }; std::map tmp_data; // holds data from logs which needs to be added to Caches later json j; std::tm date; int off = 0; if (count == 0) do { std::string output = get_user_caches_json(uuid, MAX_LOGS, off); j = json::parse(output); for (auto& el : j.items()) { if (el.value()["type"] == "Found it") { std::stringstream ss(el.value()["date"].get()); // TODO need to take care of the time zone :/ ss >> std::get_time(&date, "%Y-%m-%dT%H:%M:%S+"); codes.insert(el.value()["cache_code"].get()); tmp_data[el.value()["cache_code"].get()] = { date, el.value()["was_recommended"] }; } } off += j.size(); } while (j.size() > 0); else { int count_req = (count > MAX_LOGS) ? MAX_LOGS : count; do { std::string output = get_user_caches_json(uuid, count_req, off); j = json::parse(output); for (auto& el : j.items()) { if (el.value()["type"] == "Found it") { std::stringstream ss(el.value()["date"].get()); ss >> std::get_time(&date, "%Y-%m-%dT%H-%M-%S"); codes.insert(el.value()["cache_code"].get()); tmp_data[el.value()["cache_code"].get()] = { date, el.value()["was_recommended"] }; } } off += j.size(); count -= count_req; } while (j.size() > 0 && count > 0); } cc = get_caches(codes); Debug(2) << "Caches read from OC: " << cc.size() << '\n'; std::map mcc; for (auto& i : cc) mcc.insert( { i.code, i } ); // introduce extra data from the logs to Caches for (auto& el : tmp_data) { auto& it = mcc.at(el.first); it.set_date(el.second.date); it.recommended = el.second.was_recommended; } return cc; } void Okapi::update_caches(Caches& cc) const { std::set codes; for (auto& i : cc) codes.insert(i.code); cc = get_caches(codes); } std::string Okapi::get_uuid(const std::string& username) const { std::string service = url + OKAPI_username; std::string query = "consumer_key=" + key + "&username=" + username + "&fields=uuid"; json j = json::parse(curl_post(service, query)); return j["uuid"]; } std::string Okapi::get_profile_url(const std::string& uuid) const { std::string service = url + OKAPI_user; std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=profile_url"; json j = json::parse(curl_post(service, query)); return j["profile_url"]; } std::string Okapi::get_changelog_json(int revision) const { std::string service = url + OKAPI_changelog; std::string query = "consumer_key=" + key + "&since=" + std::to_string(revision); return curl_post(service, query); }