#include "okapi.h" #include "debug.h" #include #include #include #include #include #include using json = nlohmann::json; Okapi::Okapi(const Service serv) : service(serv) { #include "config_user.h" switch (serv) { case ocpl: url = ocpl_url; key = ocpl_key; break; case ocde: url = ocde_url; key = ocde_key; break; case ocus: url = ocus_url; key = ocus_key; break; case ocnl: url = ocnl_url; key = ocnl_key; break; case ocro: url = ocro_url; key = ocro_key; break; case ocuk: url = ocuk_url; key = ocuk_key; break; case none: throw 1; } 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_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&curl_output); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5); } Okapi::~Okapi() { curl_easy_cleanup(curl); } // 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; } void Okapi::curl_post(const std::string& url, const std::string& post) const { CURLcode res; Debug(5) << "API query: " << post; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str()); curl_output.clear(); 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); } Debug(5) << "API query result: " << curl_output; if (curl_output.starts_with("{\"error\"")) { json j = json::parse(curl_output); Debug(1) << "OKAPI error: " << j["error"]["developer_message"]; std::exit(EXIT_FAILURE); } } void Okapi::get_user_caches_json(const std::string& uuid, int count, int offset) const { std::string api_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); curl_post(api_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); // } void Okapi::get_caches_json(const std::string& codes) const { std::string api_service = url + OKAPI_caches; std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|name|location|type|status|difficulty|terrain|owner|region|country2|size2|date_hidden|date_created|recommendations|rating|founds|status|internal_id"; curl_post(api_service, query); } // void Okapi::get_caches_ratings_json(const std::string& codes) const { // std::string api_service = url + OKAPI_caches; // std::string query = "consumer_key=" + key + "&cache_codes=" + codes + "&fields=code|recommendations|rating|status"; // curl_post(api_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 '|' get_caches_json(codes_list); json j = json::parse(curl_output); codes_list.clear(); for (auto& el : j.items()) { if (el.value().is_null()) continue; c.code = el.value()["code"]; if (el.value()["internal_id"].is_number()) c.internal_id = el.value()["internal_id"]; else c.internal_id = std::stoi(el.value()["internal_id"].get()); c.name = el.value()["name"]; c.type = el.value()["type"]; c.size = el.value()["size2"]; c.region = el.value()["region"]; c.country = el.value()["country2"]; 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"].get()); c.origin = service; c.fav = el.value()["recommendations"]; if (!el.value()["rating"].is_null()) c.rating = el.value()["rating"]; 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(std::max(el.value()["date_hidden"].get(), el.value()["date_created"].get())); ss >> std::get_time(&tmp, "%Y-%m-%dT%H:%M:%S+"); tmp.tm_isdst = -1; 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 { get_user_caches_json(uuid, MAX_LOGS, off); j = json::parse(curl_output); for (auto& el : j.items()) { if (el.value()["type"] == "Found it" || el.value()["type"] == "Attended") { 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+"); date.tm_isdst = -1; 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 { get_user_caches_json(uuid, count_req, off); j = json::parse(curl_output); for (auto& el : j.items()) { if (el.value()["type"] == "Found it" || el.value()["type"] == "Attended") { std::stringstream ss(el.value()["date"].get()); ss >> std::get_time(&date, "%Y-%m-%dT%H-%M-%S"); date.tm_isdst = -1; 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 (const auto& el : tmp_data) { auto& it = mcc.at(el.first); it.set_date(el.second.date); it.recommended = el.second.was_recommended; } return cc; } int Okapi::get_user_hidden_caches_no(const std::string& uuid) const { std::string api_service = url + OKAPI_user; std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=caches_hidden"; curl_post(api_service, query); json j = json::parse(curl_output); return j["caches_hidden"]; } void Okapi::update_caches(Caches& cc) const { std::set codes; for (auto& i : cc) codes.insert(i.code); cc = get_caches(codes); } // void Okapi::update_caches_ratings(Caches& cc) const { // std::map pcc; // std::string code; // // 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 = cc.begin(); // while (it != cc.end()) { // k = 0; // // while (it != cc.end() && k < MAX_CACHES) { // codes_list += it->code; // codes_list += '|'; // pcc[it->code] = &*it; // it++; // k++; // } // codes_list.pop_back(); // remove trailing '|' // // get_caches_ratings_json(codes_list); // json j = json::parse(curl_output); // codes_list.clear(); // // for (auto& el : j.items()) { // code = el.value()["code"]; // pcc[code]->fav = el.value()["recommendations"]; // if (!el.value()["rating"].is_null()) pcc[code]->rating = el.value()["rating"]; // // if (el.value()["status"] == "Available") // pcc[code]->status = ok; // else if (el.value()["status"] == "Temporarily unavailable") // pcc[code]->status = disabled; // else if (el.value()["status"] == "Archived") // pcc[code]->status = archived; // else // pcc[code]->status = unknown; // } // } // } std::string Okapi::get_uuid(const std::string& username, uint* id) const { std::string api_service = url + OKAPI_username; char* user_esc = curl_easy_escape(curl, username.c_str(), username.size()); std::string query = "consumer_key=" + key + "&username=" + user_esc + "&fields=uuid|internal_id"; curl_free(user_esc); curl_post(api_service, query); json j = json::parse(curl_output); if (id) *id = j["internal_id"]; return j["uuid"]; } std::string Okapi::get_profile_url(const std::string& uuid) const { std::string api_service = url + OKAPI_user; std::string query = "consumer_key=" + key + "&user_uuid=" + uuid + "&fields=profile_url"; curl_post(api_service, query); json j = json::parse(curl_output); return j["profile_url"]; } std::string Okapi::get_changelog_json(int revision) const { std::string api_service = url + OKAPI_changelog; std::string query = "consumer_key=" + key + "&since=" + std::to_string(revision); curl_post(api_service, query); return curl_output; } void Okapi::get_ftf(uint uid, Caches& cc) const { std::string ftf_url = "https://opencaching.pl/UserProfile/getUserFtfsAjax/" + std::to_string(uid); CURLcode res; curl_easy_setopt(curl, CURLOPT_URL, ftf_url.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ""); curl_output.clear(); 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); } Debug(5) << "FTF query result: " << curl_output; json j = json::parse(curl_output); for (auto& el : j.items()) { if (el.value().is_null()) continue; uint id = el.value()["cache_id"]; auto c = std::find_if(cc.begin(), cc.end(), [&](const auto& a) { return a.internal_id == id; }); if (c != std::end(cc)) c->ftf = 1; } }