Loading plugins/google/google.cpp +169 −24 Changes for plugins/google/google.cpp: 169 added lines, 24 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,10 @@ #include <regex> #include <chrono> #include <string> #include <thread> #include <atomic> #include <mutex> #include <condition_variable> #include <htmlpp/html.h> #include <htmlpp/exception.h> Loading @@ -24,6 +28,13 @@ namespace blogi { class GooglePlugin : public PluginApi { public: ~GooglePlugin() { _stop.store(true); _cv.notify_all(); if (_syncThread.joinable()) _syncThread.join(); } void initPlugin() override { registerGPO("gpo-google-plugin-settings", "Blogi: Google Settings", Loading @@ -42,6 +53,13 @@ namespace blogi { << ");"; Args->database[0]->exec(sql, res); try { getConfig(*Args->database[0], "GOOGLE_PLACES_API_KEY", _apiKey); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_PLACE_ID", _placeId); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_REVIEW_NOTIFY_EMAIL", _notifyEmail); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_REVIEW_SYNC_DAYS", _syncDays); } catch(...) {} _syncThread = std::thread(&GooglePlugin::_syncWorker, this); std::cout << "Google plugin loaded" << std::endl; } Loading Loading @@ -76,11 +94,30 @@ namespace blogi { << " WHERE author_name='" << author << "';"; Args->database[tid]->exec(sql, res); saved = true; } else if(cur.key == "google_notify_email") { _notifyEmail = cur.value; setConfig(*Args->database[tid], "GOOGLE_REVIEW_NOTIFY_EMAIL", cur.value); saved = true; } else if(cur.key == "google_sync_days") { int days = atoi(cur.value.c_str()); if(days > 0) { _syncDays = days; setConfig(*Args->database[tid], "GOOGLE_REVIEW_SYNC_DAYS", cur.value); } saved = true; } else if(cur.key == "google_api_key") { _apiKey = cur.value; setConfig(*Args->database[tid], "GOOGLE_PLACES_API_KEY", cur.value); saved = true; } else if(cur.key == "google_place_id") { _placeId = cur.value; setConfig(*Args->database[tid], "GOOGLE_PLACE_ID", cur.value); saved = true; } } json_object *jroot = json_object_new_object(); json_object_object_add(jroot, "title", json_object_new_string("Google Reviews Approval")); json_object_object_add(jroot, "title", json_object_new_string("Google Reviews")); if(saved) { json_object_object_add(jroot, "message", json_object_new_string("Saved!")); Loading @@ -93,10 +130,23 @@ namespace blogi { json_object *jfields = json_object_new_array(); auto addTextField = [&](const char *name, const char *label, const std::string &value, const char *type = "text"){ json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string(name)); json_object_object_add(jf, "label", json_object_new_string(label)); json_object_object_add(jf, "type", json_object_new_string(type)); json_object_object_add(jf, "value", json_object_new_string(value.c_str())); json_object_array_add(jfields, jf); }; addTextField("google_api_key", "Google Places API Key", _apiKey); addTextField("google_place_id", "Google Place ID", _placeId); addTextField("google_notify_email", "Notification E-Mail", _notifyEmail); addTextField("google_sync_days", "Sync Interval (days)", std::to_string(_syncDays), "number"); dbpp::SQL sql; dbpp::DBResult res; sql << "SELECT author_name, rating, text, approved FROM google_reviews;"; int ncount = Args->database[tid]->exec(sql, res); for(int i = 0; i < ncount; ++i) { Loading @@ -119,7 +169,6 @@ namespace blogi { json_object_object_add(jform, "fields", jfields); // Generate submit button json_object *jbuttons = json_object_new_array(); json_object *jbtn = json_object_new_object(); json_object_object_add(jbtn, "type", json_object_new_string("submit")); Loading Loading @@ -162,9 +211,6 @@ namespace blogi { } if(req.getRequestURL().compare(0, pushUrl.length(), pushUrl) == 0 && req.getRequestType() == POSTREQUEST) { // Receive incoming reviews (JSON format) size_t datalen = req.getRequestLength(); if(req.RecvData.size() < req.getRequestLength()){ libhttppp::HTTPException exp; exp << "Google invalied PostData size !"; Loading @@ -187,24 +233,8 @@ namespace blogi { if (json_object_object_get_ex(jrev, "text", &jtext)) text = json_object_get_string(jtext); if (json_object_object_get_ex(jrev, "time", &jtime)) time = json_object_get_string(jtime); if(!author.empty()) { dbpp::SQL sqlexist, sql; dbpp::DBResult res; sqlexist << "SELECT author_name FROM google_reviews WHERE author_name='" << author << "';"; int ncount = Args->database[tid]->exec(sqlexist, res); if (ncount > 0) { // update sql << "UPDATE google_reviews SET rating='" << rating << "', text='" << text << "', time='" << time << "' WHERE author_name='" << author << "';"; } else { // insert sql << "INSERT INTO google_reviews (author_name, rating, text, time, approved) VALUES ('" << author << "', '" << rating << "', '" << text << "', '" << time << "', false);"; } Args->database[tid]->exec(sql, res); } if(!author.empty()) _processReview(tid, author, rating, text, time); } } Loading @@ -220,6 +250,121 @@ namespace blogi { return false; } private: std::thread _syncThread; std::atomic<bool> _stop{false}; std::mutex _mutex; std::condition_variable _cv; std::string _apiKey; std::string _placeId; std::string _notifyEmail; int _syncDays = 1; void _syncWorker() { while (true) { { std::unique_lock<std::mutex> lock(_mutex); _cv.wait_for(lock, std::chrono::hours(24 * _syncDays), [this]{ return _stop.load(); }); } if (_stop.load()) break; if (!_apiKey.empty() && !_placeId.empty()) _fetchFromGoogle(); } } void _fetchFromGoogle() { try { std::string path = "/maps/api/place/details/json?place_id=" + _placeId + "&fields=reviews&key=" + _apiKey; libhttppp::HttpUrl apiUrl("https://maps.googleapis.com" + path); libhttppp::HttpClient client(apiUrl); client.setTimeout(15); libhttppp::HttpRequest req; req.setRequestType(GETREQUEST); req.setRequestURL(path); auto bodyVec = client.Get(req, 3); std::string body(bodyVec.begin(), bodyVec.end()); json_tokener *tok = json_tokener_new(); json_object *jobj = json_tokener_parse_ex(tok, body.c_str(), (int)body.size()); json_tokener_free(tok); if (!jobj) return; json_object *jresult; if (json_object_object_get_ex(jobj, "result", &jresult)) { json_object *jreviews; if (json_object_object_get_ex(jresult, "reviews", &jreviews) && json_object_is_type(jreviews, json_type_array)) { int len = json_object_array_length(jreviews); for (int i = 0; i < len; ++i) { json_object *jrev = json_object_array_get_idx(jreviews, i); json_object *jauthor, *jrating, *jtext, *jtime; std::string author, text, timeStr; int rating = 5; if (json_object_object_get_ex(jrev, "author_name", &jauthor)) author = json_object_get_string(jauthor); if (json_object_object_get_ex(jrev, "rating", &jrating)) rating = json_object_get_int(jrating); if (json_object_object_get_ex(jrev, "text", &jtext)) text = json_object_get_string(jtext); if (json_object_object_get_ex(jrev, "time", &jtime)) timeStr = std::to_string(json_object_get_int64(jtime)); if (!author.empty()) _processReview(0, author, rating, text, timeStr); } } } json_object_put(jobj); } catch (const std::exception &e) { std::cerr << "Google sync error: " << e.what() << std::endl; } catch (...) { std::cerr << "Google sync: unknown error" << std::endl; } } void _processReview(int tid, const std::string &author, int rating, const std::string &text, const std::string &time) { std::vector<char> buf; dbpp::SQL sqlexist; dbpp::DBResult res; sqlexist << "SELECT author_name FROM google_reviews WHERE author_name='" << dbpp::SQL::escaped(buf, author.c_str()) << "';"; int ncount = Args->database[tid]->exec(sqlexist, res); bool isNew = (ncount == 0); buf.clear(); dbpp::SQL sql; if (!isNew) { sql << "UPDATE google_reviews SET rating=" << rating << ", text='" << dbpp::SQL::escaped(buf, text.c_str()) << "'" << ", time='" << dbpp::SQL::escaped(buf, time.c_str()) << "'" << " WHERE author_name='" << dbpp::SQL::escaped(buf, author.c_str()) << "';"; } else { sql << "INSERT INTO google_reviews (author_name, rating, text, time, approved) VALUES ('" << dbpp::SQL::escaped(buf, author.c_str()) << "', " << rating << ", '" << dbpp::SQL::escaped(buf, text.c_str()) << "', '" << dbpp::SQL::escaped(buf, time.c_str()) << "', false);"; } Args->database[tid]->exec(sql, res); if (isNew && !_notifyEmail.empty() && Args->smtp) { std::string subject = "New Google Review from " + author; std::string body = "A new Google review has been received:\n\n" "Author: " + author + "\n" "Rating: " + std::to_string(rating) + " stars\n" "Review:\n" + text + "\n"; sendMail(Args->smtp.get(), _notifyEmail.c_str(), subject.c_str(), body); } } }; } Loading Loading
plugins/google/google.cpp +169 −24 Changes for plugins/google/google.cpp: 169 added lines, 24 removed lines. Original line number Diff line number Diff line Loading @@ -3,6 +3,10 @@ #include <regex> #include <chrono> #include <string> #include <thread> #include <atomic> #include <mutex> #include <condition_variable> #include <htmlpp/html.h> #include <htmlpp/exception.h> Loading @@ -24,6 +28,13 @@ namespace blogi { class GooglePlugin : public PluginApi { public: ~GooglePlugin() { _stop.store(true); _cv.notify_all(); if (_syncThread.joinable()) _syncThread.join(); } void initPlugin() override { registerGPO("gpo-google-plugin-settings", "Blogi: Google Settings", Loading @@ -42,6 +53,13 @@ namespace blogi { << ");"; Args->database[0]->exec(sql, res); try { getConfig(*Args->database[0], "GOOGLE_PLACES_API_KEY", _apiKey); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_PLACE_ID", _placeId); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_REVIEW_NOTIFY_EMAIL", _notifyEmail); } catch(...) {} try { getConfig(*Args->database[0], "GOOGLE_REVIEW_SYNC_DAYS", _syncDays); } catch(...) {} _syncThread = std::thread(&GooglePlugin::_syncWorker, this); std::cout << "Google plugin loaded" << std::endl; } Loading Loading @@ -76,11 +94,30 @@ namespace blogi { << " WHERE author_name='" << author << "';"; Args->database[tid]->exec(sql, res); saved = true; } else if(cur.key == "google_notify_email") { _notifyEmail = cur.value; setConfig(*Args->database[tid], "GOOGLE_REVIEW_NOTIFY_EMAIL", cur.value); saved = true; } else if(cur.key == "google_sync_days") { int days = atoi(cur.value.c_str()); if(days > 0) { _syncDays = days; setConfig(*Args->database[tid], "GOOGLE_REVIEW_SYNC_DAYS", cur.value); } saved = true; } else if(cur.key == "google_api_key") { _apiKey = cur.value; setConfig(*Args->database[tid], "GOOGLE_PLACES_API_KEY", cur.value); saved = true; } else if(cur.key == "google_place_id") { _placeId = cur.value; setConfig(*Args->database[tid], "GOOGLE_PLACE_ID", cur.value); saved = true; } } json_object *jroot = json_object_new_object(); json_object_object_add(jroot, "title", json_object_new_string("Google Reviews Approval")); json_object_object_add(jroot, "title", json_object_new_string("Google Reviews")); if(saved) { json_object_object_add(jroot, "message", json_object_new_string("Saved!")); Loading @@ -93,10 +130,23 @@ namespace blogi { json_object *jfields = json_object_new_array(); auto addTextField = [&](const char *name, const char *label, const std::string &value, const char *type = "text"){ json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string(name)); json_object_object_add(jf, "label", json_object_new_string(label)); json_object_object_add(jf, "type", json_object_new_string(type)); json_object_object_add(jf, "value", json_object_new_string(value.c_str())); json_object_array_add(jfields, jf); }; addTextField("google_api_key", "Google Places API Key", _apiKey); addTextField("google_place_id", "Google Place ID", _placeId); addTextField("google_notify_email", "Notification E-Mail", _notifyEmail); addTextField("google_sync_days", "Sync Interval (days)", std::to_string(_syncDays), "number"); dbpp::SQL sql; dbpp::DBResult res; sql << "SELECT author_name, rating, text, approved FROM google_reviews;"; int ncount = Args->database[tid]->exec(sql, res); for(int i = 0; i < ncount; ++i) { Loading @@ -119,7 +169,6 @@ namespace blogi { json_object_object_add(jform, "fields", jfields); // Generate submit button json_object *jbuttons = json_object_new_array(); json_object *jbtn = json_object_new_object(); json_object_object_add(jbtn, "type", json_object_new_string("submit")); Loading Loading @@ -162,9 +211,6 @@ namespace blogi { } if(req.getRequestURL().compare(0, pushUrl.length(), pushUrl) == 0 && req.getRequestType() == POSTREQUEST) { // Receive incoming reviews (JSON format) size_t datalen = req.getRequestLength(); if(req.RecvData.size() < req.getRequestLength()){ libhttppp::HTTPException exp; exp << "Google invalied PostData size !"; Loading @@ -187,24 +233,8 @@ namespace blogi { if (json_object_object_get_ex(jrev, "text", &jtext)) text = json_object_get_string(jtext); if (json_object_object_get_ex(jrev, "time", &jtime)) time = json_object_get_string(jtime); if(!author.empty()) { dbpp::SQL sqlexist, sql; dbpp::DBResult res; sqlexist << "SELECT author_name FROM google_reviews WHERE author_name='" << author << "';"; int ncount = Args->database[tid]->exec(sqlexist, res); if (ncount > 0) { // update sql << "UPDATE google_reviews SET rating='" << rating << "', text='" << text << "', time='" << time << "' WHERE author_name='" << author << "';"; } else { // insert sql << "INSERT INTO google_reviews (author_name, rating, text, time, approved) VALUES ('" << author << "', '" << rating << "', '" << text << "', '" << time << "', false);"; } Args->database[tid]->exec(sql, res); } if(!author.empty()) _processReview(tid, author, rating, text, time); } } Loading @@ -220,6 +250,121 @@ namespace blogi { return false; } private: std::thread _syncThread; std::atomic<bool> _stop{false}; std::mutex _mutex; std::condition_variable _cv; std::string _apiKey; std::string _placeId; std::string _notifyEmail; int _syncDays = 1; void _syncWorker() { while (true) { { std::unique_lock<std::mutex> lock(_mutex); _cv.wait_for(lock, std::chrono::hours(24 * _syncDays), [this]{ return _stop.load(); }); } if (_stop.load()) break; if (!_apiKey.empty() && !_placeId.empty()) _fetchFromGoogle(); } } void _fetchFromGoogle() { try { std::string path = "/maps/api/place/details/json?place_id=" + _placeId + "&fields=reviews&key=" + _apiKey; libhttppp::HttpUrl apiUrl("https://maps.googleapis.com" + path); libhttppp::HttpClient client(apiUrl); client.setTimeout(15); libhttppp::HttpRequest req; req.setRequestType(GETREQUEST); req.setRequestURL(path); auto bodyVec = client.Get(req, 3); std::string body(bodyVec.begin(), bodyVec.end()); json_tokener *tok = json_tokener_new(); json_object *jobj = json_tokener_parse_ex(tok, body.c_str(), (int)body.size()); json_tokener_free(tok); if (!jobj) return; json_object *jresult; if (json_object_object_get_ex(jobj, "result", &jresult)) { json_object *jreviews; if (json_object_object_get_ex(jresult, "reviews", &jreviews) && json_object_is_type(jreviews, json_type_array)) { int len = json_object_array_length(jreviews); for (int i = 0; i < len; ++i) { json_object *jrev = json_object_array_get_idx(jreviews, i); json_object *jauthor, *jrating, *jtext, *jtime; std::string author, text, timeStr; int rating = 5; if (json_object_object_get_ex(jrev, "author_name", &jauthor)) author = json_object_get_string(jauthor); if (json_object_object_get_ex(jrev, "rating", &jrating)) rating = json_object_get_int(jrating); if (json_object_object_get_ex(jrev, "text", &jtext)) text = json_object_get_string(jtext); if (json_object_object_get_ex(jrev, "time", &jtime)) timeStr = std::to_string(json_object_get_int64(jtime)); if (!author.empty()) _processReview(0, author, rating, text, timeStr); } } } json_object_put(jobj); } catch (const std::exception &e) { std::cerr << "Google sync error: " << e.what() << std::endl; } catch (...) { std::cerr << "Google sync: unknown error" << std::endl; } } void _processReview(int tid, const std::string &author, int rating, const std::string &text, const std::string &time) { std::vector<char> buf; dbpp::SQL sqlexist; dbpp::DBResult res; sqlexist << "SELECT author_name FROM google_reviews WHERE author_name='" << dbpp::SQL::escaped(buf, author.c_str()) << "';"; int ncount = Args->database[tid]->exec(sqlexist, res); bool isNew = (ncount == 0); buf.clear(); dbpp::SQL sql; if (!isNew) { sql << "UPDATE google_reviews SET rating=" << rating << ", text='" << dbpp::SQL::escaped(buf, text.c_str()) << "'" << ", time='" << dbpp::SQL::escaped(buf, time.c_str()) << "'" << " WHERE author_name='" << dbpp::SQL::escaped(buf, author.c_str()) << "';"; } else { sql << "INSERT INTO google_reviews (author_name, rating, text, time, approved) VALUES ('" << dbpp::SQL::escaped(buf, author.c_str()) << "', " << rating << ", '" << dbpp::SQL::escaped(buf, text.c_str()) << "', '" << dbpp::SQL::escaped(buf, time.c_str()) << "', false);"; } Args->database[tid]->exec(sql, res); if (isNew && !_notifyEmail.empty() && Args->smtp) { std::string subject = "New Google Review from " + author; std::string body = "A new Google review has been received:\n\n" "Author: " + author + "\n" "Rating: " + std::to_string(rating) + " stars\n" "Review:\n" + text + "\n"; sendMail(Args->smtp.get(), _notifyEmail.c_str(), subject.c_str(), body); } } }; } Loading