From 69fc8d805396b889b5e8c1c88e8129d93db77d29 Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Sat, 20 Aug 2016 13:56:23 -0400 Subject: Updated API to use exceptions and make tweet/user objects more helpful --- CMakeLists.txt | 2 +- src/bounding_box.h | 88 ++++ src/client.cpp | 1065 ++++++++++++++++++------------------------------- src/client.h | 129 ++---- src/codes.cpp | 33 +- src/codes.h | 193 ++++++++- src/configuration.cpp | 101 +---- src/configuration.h | 101 +++-- src/direct_message.h | 5 +- src/list.cpp | 5 - src/list.h | 6 +- src/notification.cpp | 269 +++++++------ src/notification.h | 213 +++++----- src/stream.cpp | 291 ++++++++++++++ src/stream.h | 67 ++++ src/tweet.cpp | 133 ++---- src/tweet.h | 70 +++- src/twitter.h | 1 + src/user.cpp | 48 +-- src/user.h | 44 +- src/util.h | 6 + vendor/liboauthcpp | 2 +- 22 files changed, 1531 insertions(+), 1341 deletions(-) create mode 100644 src/bounding_box.h create mode 100644 src/stream.cpp create mode 100644 src/stream.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a0a1b0b..74f1f64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ include_directories(vendor/liboauthcpp/include) add_subdirectory(vendor/curlcpp) include_directories(${CURLCPP_SOURCE_DIR}/include) -add_library(twitter++ src/client.cpp src/auth.cpp src/tweet.cpp src/codes.cpp src/notification.cpp src/direct_message.cpp src/list.cpp src/user.cpp src/configuration.cpp) +add_library(twitter++ src/client.cpp src/stream.cpp src/auth.cpp src/tweet.cpp src/codes.cpp src/notification.cpp src/direct_message.cpp src/list.cpp src/user.cpp src/configuration.cpp) set_property(TARGET twitter++ PROPERTY CXX_STANDARD 11) set_property(TARGET twitter++ PROPERTY CXX_STANDARD_REQUIRED ON) target_link_libraries(twitter++ oauthcpp curlcpp curl pthread) diff --git a/src/bounding_box.h b/src/bounding_box.h new file mode 100644 index 0000000..25c7790 --- /dev/null +++ b/src/bounding_box.h @@ -0,0 +1,88 @@ +#ifndef BOUNDING_BOX_H_75D2077D +#define BOUNDING_BOX_H_75D2077D + +namespace twitter { + + class coordinate { + public: + + coordinate(int degrees, int minutes = 0, int seconds = 0) : + _degrees(degrees), + _minutes(minutes), + _seconds(seconds) + { + } + + int getDegrees() const + { + return _degrees; + } + + int getMinutes() const + { + return _minutes; + } + + int getSeconds() const + { + return _seconds; + } + + operator double() const + { + return (double)_degrees + ((double)_minutes / (double)60) + ((double)_seconds / (double)3600); + } + + private: + + int _degrees; + int _minutes; + int _seconds; + }; + + class bounding_box { + public: + + bounding_box( + coordinate south_west_long, + coordinate south_west_lat, + coordinate north_east_long, + coordinate north_east_lat) : + _south_west_long(south_west_long), + _south_west_lat(south_west_lat), + _north_east_long(north_east_long), + _north_east_lat(north_east_lat) + { + } + + const coordinate& getSouthWestLongitude() const + { + return _south_west_long; + } + + const coordinate& getSouthWestLatitude() const + { + return _south_west_lat; + } + + const coordinate& getNorthEastLongitude() const + { + return _north_east_long; + } + + const coordinate& getNorthEastLatitude() const + { + return _north_east_lat; + } + + private: + + coordinate _south_west_long; + coordinate _south_west_lat; + coordinate _north_east_long; + coordinate _north_east_lat; + }; + +} + +#endif /* end of include guard: BOUNDING_BOX_H_75D2077D */ diff --git a/src/client.cpp b/src/client.cpp index 1b32b8b..f8908fd 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -5,10 +5,8 @@ #include #include #include -#include "util.h" #include - -using nlohmann::json; +#include // These are here for debugging curl stuff @@ -84,99 +82,315 @@ int my_trace(CURL *handle, curl_infotype type, namespace twitter { - int client_stream_progress_callback_wrapper(void* cdata, curl_off_t, curl_off_t, curl_off_t, curl_off_t) + class request { - return static_cast(cdata)->progress(); - } + public: + + explicit request(std::string url) try + : _ios(_output), _conn(_ios) + { + _conn.add(url.c_str()); + } catch (const curl::curl_easy_exception& error) + { + error.print_traceback(); + + assert(false); + } + + std::string perform() + { + try + { + _conn.perform(); + } catch (const curl::curl_easy_exception& error) + { + std::throw_with_nested(connection_error()); + } + + int response_code = _conn.get_info().get(); + std::string result = _output.str(); + + if (response_code / 100 != 2) + { + nlohmann::json response_json; + + try + { + response_json = nlohmann::json::parse(result); + } catch (const std::invalid_argument& e) + { + std::throw_with_nested(invalid_response(result)); + } + + for (nlohmann::json& error : response_json["errors"]) + { + int error_code; + std::string error_message; + + try + { + error_code = error["code"].get(); + error_message = error["message"].get(); + } catch (const std::domain_error& e) + { + std::throw_with_nested(invalid_response(result)); + } + + switch (error_code) + { + case 32: + case 135: + case 215: + throw bad_auth(error_message); + + case 44: + throw invalid_media(error_message); + + case 64: + throw account_suspended(error_message); + + case 88: + throw rate_limit_exceeded(error_message); + + case 89: + throw bad_token(error_message); + + case 130: + throw server_overloaded(error_message); + + case 131: + throw server_error(error_message); + + case 185: + throw update_limit_exceeded(error_message); + + case 186: + throw bad_length(error_message); + + case 187: + throw duplicate_status(error_message); + + case 226: + throw suspected_spam(error_message); + + case 261: + throw write_restricted(error_message); + } + } + + if (response_code == 429) + { + throw rate_limit_exceeded("HTTP 429 Too Many Requests"); + } else if (response_code == 500) + { + throw server_error("HTTP 500 Internal Server Error"); + } else if (response_code == 502) + { + throw server_unavailable("HTTP 502 Bad Gateway"); + } else if (response_code == 503) + { + throw server_overloaded("HTTP 503 Service Unavailable"); + } else if (response_code == 504) + { + throw server_timeout("HTTP 504 Gateway Timeout"); + } + + throw unknown_error(response_code, result); + } + + return result; + } + + private: + + std::ostringstream _output; + curl::curl_ios _ios; + + protected: + + curl::curl_easy _conn; + }; - size_t client_stream_write_callback_wrapper(void* ptr, size_t size, size_t nmemb, void* cdata) + class get : public request { - return static_cast(cdata)->write(static_cast(ptr), size, nmemb); - } + public: + + get(const OAuth::Client& oauth_client, std::string url) try + : request(url) + { + std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Get, url, ""); + if (!oauth_header.empty()) + { + _headers.add(std::move(oauth_header)); + } - client::client(const auth& _arg) + _conn.add(_headers.get()); + } catch (const OAuth::ParseError& error) + { + std::cout << "Error generating OAuth header:" << std::endl; + std::cout << error.what() << std::endl; + std::cout << "This is likely due to a malformed URL." << std::endl; + + assert(false); + } catch (const curl::curl_easy_exception& error) + { + error.print_traceback(); + + assert(false); + } + + private: + + curl::curl_header _headers; + }; + + class post : public request { - _oauth_consumer = new OAuth::Consumer(_arg.getConsumerKey(), _arg.getConsumerSecret()); - _oauth_token = new OAuth::Token(_arg.getAccessKey(), _arg.getAccessSecret()); - _oauth_client = new OAuth::Client(_oauth_consumer, _oauth_token); - - std::string url = "https://api.twitter.com/1.1/account/verify_credentials.json"; - long response_code; - std::string response_data; - if (performGet(url, response_code, response_data) && (response_code == 200)) + public: + + post(const OAuth::Client& oauth_client, std::string url, std::string datastr) try + : request(url) { - try { - _current_user = user(response_data); - } catch (std::invalid_argument e) + std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Post, url, datastr); + if (!oauth_header.empty()) { - // Ignore + _headers.add(std::move(oauth_header)); } + + _conn.add(_headers.get()); + _conn.add(datastr.c_str()); + } catch (const OAuth::ParseError& error) + { + std::cout << "Error generating OAuth header:" << std::endl; + std::cout << error.what() << std::endl; + std::cout << "This is likely due to a malformed URL." << std::endl; + + assert(false); + } catch (const curl::curl_easy_exception& error) + { + error.print_traceback(); + + assert(false); } - } + + private: + + curl::curl_header _headers; + }; + + class multipost : public request + { + public: + + multipost(const OAuth::Client& oauth_client, std::string url, const curl_httppost* fields) try + : request(url) + { + std::string oauth_header = oauth_client.getFormattedHttpHeader(OAuth::Http::Post, url, ""); + if (!oauth_header.empty()) + { + _headers.add(std::move(oauth_header)); + } + + _conn.add(_headers.get()); + _conn.add(fields); + } catch (const OAuth::ParseError& error) + { + std::cout << "Error generating OAuth header:" << std::endl; + std::cout << error.what() << std::endl; + std::cout << "This is likely due to a malformed URL." << std::endl; + + assert(false); + } catch (const curl::curl_easy_exception& error) + { + error.print_traceback(); - client::~client() + assert(false); + } + + private: + + curl::curl_header _headers; + }; + + client::client(const auth& _arg) { - delete _oauth_client; - delete _oauth_token; - delete _oauth_consumer; + _oauth_consumer = + make_unique( + _arg.getConsumerKey(), + _arg.getConsumerSecret()); + + _oauth_token = + make_unique( + _arg.getAccessKey(), + _arg.getAccessSecret()); + + _oauth_client = + make_unique( + _oauth_consumer.get(), + _oauth_token.get()); + + _current_user = + make_unique(*this, + get(*_oauth_client, + "https://api.twitter.com/1.1/account/verify_credentials.json") + .perform()); } - response client::updateStatus(std::string msg, tweet& result, tweet in_response_to, std::list media_ids) + client::~client() = default; + + tweet client::updateStatus(std::string msg, std::list media_ids) const { std::stringstream datastrstream; datastrstream << "status=" << OAuth::PercentEncode(msg); - if (in_response_to) - { - datastrstream << "&in_reply_to_status_id="; - datastrstream << in_response_to.getID(); - } - if (!media_ids.empty()) { datastrstream << "&media_ids="; datastrstream << twitter::implode(std::begin(media_ids), std::end(media_ids), ","); } - std::string datastr = datastrstream.str(); - std::string url = "https://api.twitter.com/1.1/statuses/update.json"; + return tweet(*this, + post(*_oauth_client, + "https://api.twitter.com/1.1/statuses/update.json", + datastrstream.str()) + .perform()); + } + + tweet client::replyToTweet(std::string msg, tweet_id in_response_to, std::list media_ids) const + { + std::stringstream datastrstream; + datastrstream << "status=" << OAuth::PercentEncode(msg); + datastrstream << "&in_reply_to_status_id="; + datastrstream << in_response_to; - long response_code; - std::string response_data; - if (!performPost(url, datastr, response_code, response_data)) + if (!media_ids.empty()) { - return response::curl_error; + datastrstream << "&media_ids="; + datastrstream << twitter::implode(std::begin(media_ids), std::end(media_ids), ","); } - if (response_code == 200) - { - try { - result = tweet(response_data); - return response::ok; - } catch (std::invalid_argument e) - { - return response::invalid_response; - } - } else { - return codeForError(response_code, response_data); - } + return tweet(*this, + post(*_oauth_client, + "https://api.twitter.com/1.1/statuses/update.json", + datastrstream.str()) + .perform()); } - response client::uploadMedia(std::string media_type, const char* data, long data_length, long& media_id) + long client::uploadMedia(std::string media_type, const char* data, long data_length) const try { curl::curl_form form; + std::string str_data_length = std::to_string(data_length); curl::curl_pair command_name(CURLFORM_COPYNAME, "command"); curl::curl_pair command_cont(CURLFORM_COPYCONTENTS, "INIT"); curl::curl_pair bytes_name(CURLFORM_COPYNAME, "total_bytes"); - std::string str_data_length = std::to_string(data_length); curl::curl_pair bytes_cont(CURLFORM_COPYCONTENTS, str_data_length); curl::curl_pair type_name(CURLFORM_COPYNAME, "media_type"); curl::curl_pair type_cont(CURLFORM_COPYCONTENTS, media_type); form.add(command_name, command_cont); form.add(bytes_name, bytes_cont); form.add(type_name, type_cont); - + if (media_type == "image/gif") { curl::curl_pair category_name(CURLFORM_COPYNAME, "media_category"); @@ -184,212 +398,109 @@ namespace twitter { form.add(category_name, category_cont); } - long response_code; - std::string response_data; - if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", form.get(), response_code, response_data)) - { - return response::curl_error; - } + std::string init_response = + multipost(*_oauth_client, + "https://upload.twitter.com/1.1/media/upload.json", + form.get()) + .perform(); + + long media_id; - if (response_code / 100 != 2) + try { - return codeForError(response_code, response_data); - } - - json response_json; - try { - response_json = json::parse(response_data); - } catch (std::invalid_argument e) + nlohmann::json response_json = nlohmann::json::parse(init_response); + media_id = response_json["media_id"].get(); + } catch (const std::invalid_argument& error) + { + std::throw_with_nested(invalid_response(init_response)); + } catch (const std::domain_error& error) { - return response::invalid_response; + std::throw_with_nested(invalid_response(init_response)); } - - media_id = response_json["media_id"].get(); + // TODO: Currently have to use the C libcurl API to create this form because it uses a buffer and + // libcurlcpp currently messes that up. curl_httppost* append_form_post = nullptr; curl_httppost* append_form_last = nullptr; - curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "command", CURLFORM_COPYCONTENTS, "APPEND", CURLFORM_END); - curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media_id", CURLFORM_COPYCONTENTS, std::to_string(media_id).c_str(), CURLFORM_END); - curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media", CURLFORM_BUFFER, "media", CURLFORM_BUFFERPTR, data, CURLFORM_BUFFERLENGTH, data_length, CURLFORM_CONTENTTYPE, "application/octet-stream", CURLFORM_END); - curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "segment_index", CURLFORM_COPYCONTENTS, std::to_string(0).c_str(), CURLFORM_END); - if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", append_form_post, response_code, response_data)) + if ( curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "command", CURLFORM_COPYCONTENTS, "APPEND", CURLFORM_END) + || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media_id", CURLFORM_COPYCONTENTS, std::to_string(media_id).c_str(), CURLFORM_END) + || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "media", CURLFORM_BUFFER, "media", CURLFORM_BUFFERPTR, data, CURLFORM_BUFFERLENGTH, data_length, CURLFORM_CONTENTTYPE, "application/octet-stream", CURLFORM_END) + || curl_formadd(&append_form_post, &append_form_last, CURLFORM_COPYNAME, "segment_index", CURLFORM_COPYCONTENTS, std::to_string(0).c_str(), CURLFORM_END)) { - return response::curl_error; + assert(false); } - curl_formfree(append_form_post); + multipost(*_oauth_client, "https://upload.twitter.com/1.1/media/upload.json", append_form_post).perform(); - if (response_code / 100 != 2) - { - return codeForError(response_code, response_data); - } + curl_formfree(append_form_post); curl::curl_form finalize_form; + std::string str_media_id = std::to_string(media_id); + curl::curl_pair command3_name(CURLFORM_COPYNAME, "command"); curl::curl_pair command3_cont(CURLFORM_COPYCONTENTS, "FINALIZE"); curl::curl_pair media_id_name(CURLFORM_COPYNAME, "media_id"); - std::string str_media_id = std::to_string(media_id); curl::curl_pair media_id_cont(CURLFORM_COPYCONTENTS, str_media_id); finalize_form.add(command3_name, command3_cont); finalize_form.add(media_id_name, media_id_cont); - if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", finalize_form.get(), response_code, response_data)) - { - return response::curl_error; - } + std::string finalize_response = + multipost(*_oauth_client, + "https://upload.twitter.com/1.1/media/upload.json", + finalize_form.get()) + .perform(); + + nlohmann::json finalize_json; - if (response_code / 100 != 2) + try { - return codeForError(response_code, response_data); - } - - try { - response_json = json::parse(response_data); - } catch (std::invalid_argument e) + finalize_json = nlohmann::json::parse(finalize_response); + } catch (const std::invalid_argument& error) { - return response::invalid_response; + std::throw_with_nested(invalid_response(finalize_response)); } - if (response_json.find("processing_info") != response_json.end()) + if (finalize_json.find("processing_info") != finalize_json.end()) { std::stringstream datastr; datastr << "https://upload.twitter.com/1.1/media/upload.json?command=STATUS&media_id=" << media_id; for (;;) { - if (!performGet(datastr.str(), response_code, response_data)) - { - return response::curl_error; - } + std::string status_response = get(*_oauth_client, datastr.str()).perform(); - if (response_code / 100 != 2) + try { - return codeForError(response_code, response_data); - } - - try { - response_json = json::parse(response_data); - } catch (std::invalid_argument e) + nlohmann::json status_json = nlohmann::json::parse(status_response); + std::string state = status_json["processing_info"]["state"].get(); + + if (state == "succeeded") + { + break; + } + + int ttw = status_json["processing_info"]["check_after_secs"].get(); + std::this_thread::sleep_for(std::chrono::seconds(ttw)); + } catch (const std::invalid_argument& error) { - return response::invalid_response; - } - - if (response_json["processing_info"]["state"] == "succeeded") + std::throw_with_nested(invalid_response(status_response)); + } catch (const std::domain_error& error) { - break; + std::throw_with_nested(invalid_response(status_response)); } - - int ttw = response_json["processing_info"]["check_after_secs"].get(); - std::this_thread::sleep_for(std::chrono::seconds(ttw)); } } - return response::ok; - } - - response client::follow(user_id toFollow) + return media_id; + } catch (const curl::curl_exception& error) { - std::stringstream datastrstream; - datastrstream << "follow=true&user_id="; - datastrstream << toFollow; - - std::string datastr = datastrstream.str(); - std::string url = "https://api.twitter.com/1.1/friendships/create.json"; + error.print_traceback(); - long response_code; - std::string response_data; - if (!performPost(url, datastr, response_code, response_data)) - { - return response::curl_error; - } - - if (response_code == 200) - { - return response::ok; - } else { - return codeForError(response_code, response_data); - } - } - - response client::follow(user toFollow) - { - return follow(toFollow.getID()); - } - - response client::unfollow(user_id toUnfollow) - { - std::stringstream datastrstream; - datastrstream << "user_id="; - datastrstream << toUnfollow; - - std::string datastr = datastrstream.str(); - std::string url = "https://api.twitter.com/1.1/friendships/destroy.json"; - - long response_code; - std::string response_data; - if (!performPost(url, datastr, response_code, response_data)) - { - return response::curl_error; - } - - if (response_code == 200) - { - return response::ok; - } else { - return codeForError(response_code, response_data); - } - } - - response client::unfollow(user toUnfollow) - { - return unfollow(toUnfollow.getID()); + assert(false); } - response client::getUser(user& result) + std::set client::getFriends(user_id id) const { - if (!_current_user) - { - std::string url = "https://api.twitter.com/1.1/account/verify_credentials.json"; - long response_code; - std::string response_data; - if (performGet(url, response_code, response_data) && (response_code == 200)) - { - try { - _current_user = user(response_data); - } catch (std::invalid_argument e) - { - return response::invalid_response; - } - } - } - - result = _current_user; - return response::ok; - } - - configuration client::getConfiguration() - { - if (!_configuration || (difftime(time(NULL), _last_configuration_update) > 60*60*24)) - { - long response_code; - std::string response_data; - if (performGet("https://api.twitter.com/1.1/help/configuration.json", response_code, response_data)) - { - _configuration = configuration(response_data); - _last_configuration_update = time(NULL); - } - } - - return _configuration; - } - - response client::getFriends(std::set& _ret) - { - if (!_current_user) - { - return response::unknown_error; - } - long long cursor = -1; std::set result; @@ -397,48 +508,33 @@ namespace twitter { { std::stringstream urlstream; urlstream << "https://api.twitter.com/1.1/friends/ids.json?user_id="; - urlstream << _current_user.getID(); + urlstream << id; urlstream << "&cursor="; urlstream << cursor; std::string url = urlstream.str(); - - long response_code; - std::string response_data; - if (!performGet(url, response_code, response_data)) - { - return response::curl_error; - } - - if (response_code == 200) + std::string response_data = get(*_oauth_client, url).perform(); + + try { - json rjs; - try { - rjs = json::parse(response_data); - } catch (std::invalid_argument e) - { - return response::invalid_response; - } + nlohmann::json rjs = nlohmann::json::parse(response_data); - cursor = rjs.at("next_cursor"); - result.insert(std::begin(rjs.at("ids")), std::end(rjs.at("ids"))); - } else { - return codeForError(response_code, response_data); + cursor = rjs["next_cursor"].get(); + result.insert(std::begin(rjs["ids"]), std::end(rjs["ids"])); + } catch (const std::invalid_argument& error) + { + std::throw_with_nested(invalid_response(response_data)); + } catch (const std::domain_error& error) + { + std::throw_with_nested(invalid_response(response_data)); } } - _ret = result; - - return response::ok; + return result; } - response client::getFollowers(std::set& _ret) + std::set client::getFollowers(user_id id) const { - if (!_current_user) - { - return response::unknown_error; - } - long long cursor = -1; std::set result; @@ -446,473 +542,68 @@ namespace twitter { { std::stringstream urlstream; urlstream << "https://api.twitter.com/1.1/followers/ids.json?user_id="; - urlstream << _current_user.getID(); + urlstream << id; urlstream << "&cursor="; urlstream << cursor; std::string url = urlstream.str(); - - long response_code; - std::string response_data; - if (!performGet(url, response_code, response_data)) - { - return response::curl_error; - } - - if (response_code == 200) + std::string response_data = get(*_oauth_client, url).perform(); + + try { - json rjs; - try { - rjs = json::parse(response_data); - } catch (std::invalid_argument e) - { - return response::invalid_response; - } + nlohmann::json rjs = nlohmann::json::parse(response_data); - cursor = rjs.at("next_cursor"); - result.insert(std::begin(rjs.at("ids")), std::end(rjs.at("ids"))); - } else { - return codeForError(response_code, response_data); - } - } - - _ret = result; - - return response::ok; - } - - void client::setUserStreamNotifyCallback(stream::notify_callback callback) - { - _user_stream.setNotifyCallback(callback); - } - - void client::setUserStreamReceiveAllReplies(bool _arg) - { - _user_stream.setReceiveAllReplies(_arg); - } - - void client::startUserStream() - { - _user_stream.start(); - } - - void client::stopUserStream() - { - _user_stream.stop(); - } - - std::string client::generateReplyPrefill(tweet _tweet) const - { - std::ostringstream output; - output << "@" << _tweet.getAuthor().getScreenName() << " "; - - for (auto mention : _tweet.getMentions()) - { - if ((mention.first != _tweet.getAuthor().getID()) && (mention.first != _current_user.getID())) + cursor = rjs["next_cursor"].get(); + result.insert(std::begin(rjs["ids"]), std::end(rjs["ids"])); + } catch (const std::invalid_argument& error) { - output << "@" << mention.second << " "; + std::throw_with_nested(invalid_response(response_data)); + } catch (const std::domain_error& error) + { + std::throw_with_nested(invalid_response(response_data)); } } - return output.str(); - } - - bool client::performGet(std::string url, long& response_code, std::string& result) - { - std::ostringstream output; - curl::curl_ios ios(output); - curl::curl_easy conn(ios); - - curl::curl_header headers; - std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Get, url, ""); - if (!oauth_header.empty()) - { - headers.add(oauth_header); - } - - try { - //conn.add(1); - //conn.add(my_trace); - conn.add(url.c_str()); - conn.add(headers.get()); - - conn.perform(); - } catch (curl::curl_easy_exception error) - { - error.print_traceback(); - - return false; - } - - response_code = conn.get_info().get(); - result = output.str(); - - return true; - } - - bool client::performPost(std::string url, std::string datastr, long& response_code, std::string& result) - { - std::ostringstream output; - curl::curl_ios ios(output); - curl::curl_easy conn(ios); - - curl::curl_header headers; - std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Post, url, datastr); - if (!oauth_header.empty()) - { - headers.add(oauth_header); - } - - try { - //conn.add(1); - //conn.add(my_trace); - conn.add(url.c_str()); - conn.add(datastr.c_str()); - conn.add(headers.get()); - - conn.perform(); - } catch (curl::curl_easy_exception error) - { - error.print_traceback(); - - return false; - } - - response_code = conn.get_info().get(); - result = output.str(); - - return true; - } - - bool client::performMultiPost(std::string url, const curl_httppost* fields, long& response_code, std::string& result) - { - std::ostringstream output; - curl::curl_ios ios(output); - curl::curl_easy conn(ios); - - curl::curl_header headers; - std::string oauth_header = _oauth_client->getFormattedHttpHeader(OAuth::Http::Post, url, ""); - if (!oauth_header.empty()) - { - headers.add(oauth_header); - } - - try { - //conn.add(1); - //conn.add(my_trace); - conn.add(headers.get()); - conn.add(url.c_str()); - conn.add(fields); - - conn.perform(); - } catch (curl::curl_easy_exception error) - { - error.print_traceback(); - - return false; - } - - response_code = conn.get_info().get(); - result = output.str(); - - return true; + return result; } - response client::codeForError(int response_code, std::string response_data) const + void client::follow(user_id toFollow) const { - json response_json; - try { - response_json = json::parse(response_data); - } catch (std::invalid_argument e) - { - return response::invalid_response; - } - - std::set error_codes; - if (response_json.find("errors") != response_json.end()) - { - std::transform(std::begin(response_json["errors"]), std::end(response_json["errors"]), std::inserter(error_codes, std::begin(error_codes)), [] (const json& error) { - return error["code"].get(); - }); - } - - if (error_codes.count(32) == 1 || error_codes.count(135) == 1 || error_codes.count(215) == 1) - { - return response::bad_auth; - } else if (error_codes.count(64) == 1) - { - return response::suspended; - } else if (error_codes.count(88) == 1 || error_codes.count(185) == 1) - { - return response::limited; - } else if (error_codes.count(89) == 1) - { - return response::bad_token; - } else if (error_codes.count(130) == 1) - { - return response::server_overloaded; - } else if (error_codes.count(131) == 1) - { - return response::server_error; - } else if (error_codes.count(186) == 1) - { - return response::bad_length; - } else if (error_codes.count(187) == 1) - { - return response::duplicate_status; - } else if (error_codes.count(226) == 1) - { - return response::suspected_spam; - } else if (error_codes.count(261) == 1) - { - return response::write_restricted; - } else if (error_codes.count(44) == 1) - { - return response::invalid_media; - } else if (response_code == 429) - { - return response::limited; - } else if (response_code == 500) - { - return response::server_error; - } else if (response_code == 502) - { - return response::server_unavailable; - } else if (response_code == 503) - { - return response::server_overloaded; - } else if (response_code == 504) - { - return response::server_timeout; - } else { - return response::unknown_error; - } - } - - client::stream::stream(client& _client) : _client(_client) - { - - } - - bool client::stream::isRunning() const - { - return _thread.joinable(); - } - - void client::stream::setNotifyCallback(notify_callback _n) - { - std::lock_guard _running_lock(_running_mutex); - - if (!_thread.joinable()) - { - _notify = _n; - } - } - - void client::stream::setReceiveAllReplies(bool _arg) - { - std::lock_guard _running_lock(_running_mutex); - - if (!_thread.joinable()) - { - _receive_all_replies = _arg; - } - } - - void client::stream::start() - { - std::lock_guard _running_lock(_running_mutex); + std::stringstream datastrstream; + datastrstream << "follow=true&user_id="; + datastrstream << toFollow; - if (!_thread.joinable()) - { - _thread = std::thread(&stream::run, this); - } + post(*_oauth_client, "https://api.twitter.com/1.1/friendships/create.json", datastrstream.str()).perform(); } - void client::stream::stop() + void client::unfollow(user_id toUnfollow) const { - std::lock_guard _running_lock(_running_mutex); + std::stringstream datastrstream; + datastrstream << "user_id="; + datastrstream << toUnfollow; - if (_thread.joinable()) - { - _stop = true; - _thread.join(); - _stop = false; - } + post(*_oauth_client, "https://api.twitter.com/1.1/friendships/destroy.json", datastrstream.str()).perform(); } - - void client::stream::run() - { - curl::curl_easy conn; - std::ostringstream urlstr; - urlstr << "https://userstream.twitter.com/1.1/user.json"; - - if (_receive_all_replies) - { - urlstr << "?replies=all"; - } - - std::string url = urlstr.str(); - curl::curl_header headers; - std::string oauth_header = _client._oauth_client->getFormattedHttpHeader(OAuth::Http::Get, url, ""); - if (!oauth_header.empty()) - { - headers.add(oauth_header); - } - - conn.add(client_stream_write_callback_wrapper); - conn.add(this); - conn.add(nullptr); - conn.add(nullptr); - conn.add(client_stream_progress_callback_wrapper); - conn.add(this); - conn.add(0); - //conn.add(1); - //conn.add(my_trace); - conn.add(url.c_str()); - conn.add(headers.get()); - - _backoff_type = backoff::none; - _backoff_amount = std::chrono::milliseconds(0); - for (;;) - { - bool failure = false; - try { - conn.perform(); - } catch (curl::curl_easy_exception error) - { - failure = true; - if ((error.get_code() == CURLE_ABORTED_BY_CALLBACK) && _stop) - { - break; - } else { - if (_backoff_type == backoff::none) - { - _established = false; - _backoff_type = backoff::network; - _backoff_amount = std::chrono::milliseconds(0); - } - } - } - - if (!failure) - { - long response_code = conn.get_info().get(); - if (response_code == 420) - { - if (_backoff_type == backoff::none) - { - _established = false; - _backoff_type = backoff::rate_limit; - _backoff_amount = std::chrono::minutes(1); - } - } else if (response_code != 200) - { - if (_backoff_type == backoff::none) - { - _established = false; - _backoff_type = backoff::http; - _backoff_amount = std::chrono::seconds(5); - } - } else { - if (_backoff_type == backoff::none) - { - _established = false; - _backoff_type = backoff::network; - _backoff_amount = std::chrono::milliseconds(0); - } - } - } - - std::this_thread::sleep_for(_backoff_amount); - switch (_backoff_type) - { - case backoff::network: - { - if (_backoff_amount < std::chrono::seconds(16)) - { - _backoff_amount += std::chrono::milliseconds(250); - } - - break; - } - - case backoff::http: - { - if (_backoff_amount < std::chrono::seconds(320)) - { - _backoff_amount *= 2; - } - - break; - } - - case backoff::rate_limit: - { - _backoff_amount *= 2; - - break; - } - } - } - } - - size_t client::stream::write(char* ptr, size_t size, size_t nmemb) + const user& client::getUser() const { - for (size_t i = 0; i < size*nmemb; i++) - { - if (ptr[i] == '\r') - { - i++; // Skip the \n - - if (!_buffer.empty()) - { - notification n(_buffer, _client._current_user); - if (n.getType() == notification::type::friends) - { - _established = true; - _backoff_type = backoff::none; - _backoff_amount = std::chrono::milliseconds(0); - } - - if (_notify) - { - _notify(n); - } - - _buffer = ""; - } - } else { - _buffer.push_back(ptr[i]); - } - } - - { - std::lock_guard _stall_lock(_stall_mutex); - time(&_last_write); - } - - return size*nmemb; + return *_current_user; } - int client::stream::progress() + const configuration& client::getConfiguration() const { - if (_stop) - { - return 1; - } - - if (_established) + if (!_configuration || (difftime(time(NULL), _last_configuration_update) > 60*60*24)) { - std::lock_guard _stall_lock(_stall_mutex); - if (difftime(time(NULL), _last_write) >= 90) - { - return 1; - } + _configuration = + make_unique( + get(*_oauth_client, + "https://api.twitter.com/1.1/help/configuration.json") + .perform()); + + _last_configuration_update = time(NULL); } - return 0; + return *_configuration; } }; diff --git a/src/client.h b/src/client.h index 6963412..37081ff 100644 --- a/src/client.h +++ b/src/client.h @@ -1,17 +1,15 @@ #ifndef TWITTER_H_ABFF6A12 #define TWITTER_H_ABFF6A12 -#include "codes.h" -#include "tweet.h" -#include "auth.h" #include -#include -#include -#include "notification.h" #include #include -#include +#include +#include "codes.h" +#include "tweet.h" +#include "auth.h" #include "configuration.h" +#include "util.h" namespace OAuth { class Consumer; @@ -19,96 +17,39 @@ namespace OAuth { class Client; }; -class curl_httppost; - namespace twitter { class client { - public: - class stream { - public: - typedef std::function notify_callback; - - stream(client& _client); - - void setNotifyCallback(notify_callback _n); - void setReceiveAllReplies(bool _arg); - - bool isRunning() const; - void start(); - void stop(); - - int progress(); - size_t write(char* ptr, size_t size, size_t nmemb); - - private: - enum class backoff { - none, - network, - http, - rate_limit - }; - - void run(); - - client& _client; - notify_callback _notify; - bool _stop = false; - std::thread _thread; - std::mutex _running_mutex; - std::mutex _stall_mutex; - std::string _buffer; - time_t _last_write; - bool _established = false; - backoff _backoff_type = backoff::none; - std::chrono::milliseconds _backoff_amount; - bool _receive_all_replies = false; - }; - - client(const auth& _auth); - ~client(); - - response updateStatus(std::string msg, tweet& result, tweet in_response_to = tweet(), std::list media_ids = {}); - response uploadMedia(std::string media_type, const char* data, long data_length, long& media_id); - - response follow(user_id toFollow); - response follow(user toFollow); - - response unfollow(user_id toUnfollow); - response unfollow(user toUnfollow); - - response getFriends(std::set& result); - response getFollowers(std::set& result); - - response getUser(user& result); - - configuration getConfiguration(); - - // NOTE: stream setting function calls will fail silently when stream is running - void setUserStreamNotifyCallback(stream::notify_callback callback); - void setUserStreamReceiveAllReplies(bool _arg); - void startUserStream(); - void stopUserStream(); - - std::string generateReplyPrefill(tweet t) const; - - private: - friend class stream; - - OAuth::Consumer* _oauth_consumer; - OAuth::Token* _oauth_token; - OAuth::Client* _oauth_client; - - user _current_user; - stream _user_stream{*this}; - - configuration _configuration; - time_t _last_configuration_update; - - bool performGet(std::string url, long& response_code, std::string& result); - bool performPost(std::string url, std::string dataStr, long& response_code, std::string& result); - bool performMultiPost(std::string url, const curl_httppost* fields, long& response_code, std::string& result); - response codeForError(int httpcode, std::string errors) const; + public: + + client(const auth& _auth); + ~client(); + + tweet updateStatus(std::string msg, std::list media_ids = {}) const; + long uploadMedia(std::string media_type, const char* data, long data_length) const; + + tweet replyToTweet(std::string msg, tweet_id in_response_to, std::list media_ids = {}) const; + std::set getFriends(user_id id) const; + std::set getFollowers(user_id id) const; + void follow(user_id toFollow) const; + void unfollow(user_id toUnfollow) const; + + const user& getUser() const; + + const configuration& getConfiguration() const; + + private: + + friend class stream; + + std::unique_ptr _oauth_consumer; + std::unique_ptr _oauth_token; + std::unique_ptr _oauth_client; + + std::unique_ptr _current_user; + + mutable std::unique_ptr _configuration; + mutable time_t _last_configuration_update; }; }; diff --git a/src/codes.cpp b/src/codes.cpp index 9639d5d..7805c7a 100644 --- a/src/codes.cpp +++ b/src/codes.cpp @@ -1,24 +1,17 @@ #include "codes.h" +#include -std::ostream& operator<<(std::ostream& os, twitter::response r) -{ - switch (r) +namespace twitter { + + const char* invalid_response::WHAT_TEXT = "Invalid response data received from Twitter"; + const char* connection_error::WHAT_TEXT = "Error connecting to Twitter"; + + std::string unknown_error::generateMessage(int response_code) { - case twitter::response::ok: return os << "OK"; - case twitter::response::curl_error: return os << "Curl Error"; - case twitter::response::bad_auth: return os << "Bad Auth"; - case twitter::response::limited: return os << "Rate Limit Exceeded"; - case twitter::response::server_error: return os << "Twitter Server Error"; - case twitter::response::server_unavailable: return os << "Twitter Is Down"; - case twitter::response::server_overloaded: return os << "Twitter Is Over Capacity"; - case twitter::response::server_timeout: return os << "Twitter Connection Timed Out"; - case twitter::response::suspended: return os << "Authenticated User Is Suspended"; - case twitter::response::bad_token: return os << "Invalid Or Expired Access Token"; - case twitter::response::duplicate_status: return os << "Duplicate Status"; - case twitter::response::suspected_spam: return os << "Request Looks Automated"; - case twitter::response::write_restricted: return os << "Cannot Perform Write"; - case twitter::response::bad_length: return os << "Message Body Too Long"; - case twitter::response::unknown_error: return os << "Unknown Error"; - case twitter::response::invalid_media: return os << "Invalid Media"; + std::ostringstream msgbuilder; + msgbuilder << "Unknown error (HTTP " << response_code << ")"; + + return msgbuilder.str(); } -} + +}; diff --git a/src/codes.h b/src/codes.h index 6767805..baf4f8f 100644 --- a/src/codes.h +++ b/src/codes.h @@ -1,32 +1,183 @@ #ifndef CODES_H_05838D39 #define CODES_H_05838D39 -#include +#include +#include namespace twitter { - enum class response { - ok, - curl_error, - bad_auth, - limited, - server_error, - server_unavailable, - server_overloaded, - server_timeout, - suspended, - bad_token, - duplicate_status, - suspected_spam, - write_restricted, - bad_length, - unknown_error, - invalid_media, - invalid_response + class twitter_error : public std::runtime_error { + public: + + using std::runtime_error::runtime_error; + }; + + class bad_auth : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class invalid_response : public twitter_error { + public: + + static const char* WHAT_TEXT; + + explicit invalid_response(std::string response) noexcept + : twitter_error(WHAT_TEXT), _response(std::move(response)) + { + } + + const std::string& getResponse() const noexcept + { + return _response; + } + + private: + + std::string _response; + }; + + class account_suspended : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class rate_limit_exceeded : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class update_limit_exceeded : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class bad_token : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class server_overloaded : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class server_error : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class bad_length : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class duplicate_status : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class suspected_spam : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class write_restricted : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class invalid_media : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class server_unavailable : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class server_timeout : public twitter_error { + public: + + using twitter_error::twitter_error; + }; + + class unknown_error : public twitter_error { + public: + + unknown_error(int response_code, std::string response_data) + : twitter_error(generateMessage(response_code)), + _response_code(response_code), + _response_data(std::move(response_data)) + { + } + + int getResponseCode() const noexcept + { + return _response_code; + } + + const std::string& getResponse() const noexcept + { + return _response_data; + } + + private: + + static std::string generateMessage(int response_code); + + int _response_code; + std::string _response_data; + }; + + class connection_error : public twitter_error { + public: + + static const char* WHAT_TEXT; + + connection_error() noexcept : twitter_error(WHAT_TEXT) + { + } + }; + + class invalid_member : public std::domain_error { + public: + + using std::domain_error::domain_error; + }; + + class malformed_object : public invalid_response { + public: + + malformed_object(std::string type, std::string data) noexcept + : invalid_response(std::move(data)), _type(std::move(type)) + { + } + + const std::string& getType() const noexcept + { + return _type; + } + + private: + + std::string _type; }; }; -std::ostream& operator<<(std::ostream& os, twitter::response r); - #endif /* end of include guard: CODES_H_05838D39 */ diff --git a/src/configuration.cpp b/src/configuration.cpp index 63464ed..0010a40 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -1,35 +1,27 @@ #include "configuration.h" #include #include - -using nlohmann::json; +#include "codes.h" namespace twitter { - configuration::configuration() + configuration::configuration(std::string data) try { - _valid = false; - } - - configuration::configuration(std::string data) - { - _valid = true; - - auto _data = json::parse(data); + auto json_data = nlohmann::json::parse(data); - _characters_reserved_per_media = _data.at("characters_reserved_per_media"); - _dm_text_character_limit = _data.at("dm_text_character_limit"); - _max_media_per_upload = _data.at("max_media_per_upload"); - _photo_size_limit = _data.at("photo_size_limit"); - _short_url_length = _data.at("short_url_length"); - _short_https_url_length = _data.at("short_url_length_https"); + _characters_reserved_per_media = json_data["characters_reserved_per_media"].get(); + _dm_text_character_limit = json_data["dm_text_character_limit"].get(); + _max_media_per_upload = json_data["max_media_per_upload"].get(); + _photo_size_limit = json_data["photo_size_limit"].get(); + _short_url_length = json_data["short_url_length"].get(); + _short_https_url_length = json_data["short_url_length_https"].get(); - for (json::iterator sizedata = _data.at("photo_sizes").begin(); sizedata != _data.at("photo_sizes").end(); ++sizedata) + for (auto sizedata = std::begin(json_data["photo_sizes"]); sizedata != std::end(json_data["photo_sizes"]); ++sizedata) { photosize size; - size.height = sizedata.value().at("h"); - size.width = sizedata.value().at("w"); - if (sizedata.value().at("resize") == "fit") + size.height = sizedata.value()["h"].get(); + size.width = sizedata.value()["w"].get(); + if (sizedata.value()["resize"].get() == "fit") { size.resize = resizetype::fit; } else { @@ -39,71 +31,16 @@ namespace twitter { _photo_sizes[sizedata.key()] = size; } - for (auto path : _data.at("non_username_paths")) + for (auto path : json_data["non_username_paths"]) { _non_username_paths.insert(path.get()); } - } - - size_t configuration::getCharactersReservedPerMedia() const - { - assert(_valid); - - return _characters_reserved_per_media; - } - - size_t configuration::getDirectMessageCharacterLimit() const - { - assert(_valid); - - return _dm_text_character_limit; - } - - size_t configuration::getMaxMediaPerUpload() const - { - assert(_valid); - - return _max_media_per_upload; - } - - size_t configuration::getPhotoSizeLimit() const - { - assert(_valid); - - return _photo_size_limit; - } - - std::map configuration::getPhotoSizes() const - { - assert(_valid); - - return _photo_sizes; - } - - size_t configuration::getShortUrlLength() const - { - assert(_valid); - - return _short_url_length; - } - - size_t configuration::getShortHttpsUrlLength() const - { - assert(_valid); - - return _short_https_url_length; - } - - std::set configuration::getNonUsernamePaths() const + } catch (const std::invalid_argument& error) { - assert(_valid); - - return _non_username_paths; - } - - configuration::operator bool() const + std::throw_with_nested(malformed_object("configuration", data)); + } catch (const std::domain_error& error) { - return _valid; + std::throw_with_nested(malformed_object("configuration", data)); } - + }; diff --git a/src/configuration.h b/src/configuration.h index a15be3d..543e306 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -8,43 +8,70 @@ namespace twitter { class configuration { - public: - enum class resizetype { - fit, - crop - }; - - struct photosize { - size_t height; - size_t width; - resizetype resize; - }; - - configuration(); - configuration(std::string data); - - size_t getCharactersReservedPerMedia() const; - size_t getDirectMessageCharacterLimit() const; - size_t getMaxMediaPerUpload() const; - size_t getPhotoSizeLimit() const; - std::map getPhotoSizes() const; - size_t getShortUrlLength() const; - size_t getShortHttpsUrlLength() const; - std::set getNonUsernamePaths() const; - - operator bool() const; - - private: - bool _valid = false; - - size_t _characters_reserved_per_media; - size_t _dm_text_character_limit; - size_t _max_media_per_upload; - size_t _photo_size_limit; - std::map _photo_sizes; - size_t _short_url_length; - size_t _short_https_url_length; - std::set _non_username_paths; + public: + enum class resizetype { + fit, + crop + }; + + struct photosize { + size_t height; + size_t width; + resizetype resize; + }; + + explicit configuration(std::string data); + + size_t getCharactersReservedPerMedia() const + { + return _characters_reserved_per_media; + } + + size_t getDirectMessageCharacterLimit() const + { + return _dm_text_character_limit; + } + + size_t getMaxMediaPerUpload() const + { + return _max_media_per_upload; + } + + size_t getPhotoSizeLimit() const + { + return _photo_size_limit; + } + + const std::map& getPhotoSizes() const + { + return _photo_sizes; + } + + size_t getShortUrlLength() const + { + return _short_url_length; + } + + size_t getShortHttpsUrlLength() const + { + return _short_https_url_length; + } + + const std::set& getNonUsernamePaths() const + { + return _non_username_paths; + } + + private: + + size_t _characters_reserved_per_media; + size_t _dm_text_character_limit; + size_t _max_media_per_upload; + size_t _photo_size_limit; + std::map _photo_sizes; + size_t _short_url_length; + size_t _short_https_url_length; + std::set _non_username_paths; }; }; diff --git a/src/direct_message.h b/src/direct_message.h index 4b5e285..12e254c 100644 --- a/src/direct_message.h +++ b/src/direct_message.h @@ -6,8 +6,9 @@ namespace twitter { class direct_message { - public: - direct_message(std::string data); + public: + + explicit direct_message(std::string data); }; }; diff --git a/src/list.cpp b/src/list.cpp index 49405d0..d2b61cc 100644 --- a/src/list.cpp +++ b/src/list.cpp @@ -2,11 +2,6 @@ namespace twitter { - list::list() - { - - } - list::list(std::string data) { diff --git a/src/list.h b/src/list.h index 3876f2d..69085b4 100644 --- a/src/list.h +++ b/src/list.h @@ -6,9 +6,9 @@ namespace twitter { class list { - public: - list(); - list(std::string data); + public: + + explicit list(std::string data); }; }; diff --git a/src/notification.cpp b/src/notification.cpp index 0397ce5..3269a90 100644 --- a/src/notification.cpp +++ b/src/notification.cpp @@ -2,8 +2,8 @@ #include #include #include - -using nlohmann::json; +#include "codes.h" +#include "client.h" namespace twitter { @@ -12,26 +12,32 @@ namespace twitter { return _type; } - notification::notification() : _type(type::invalid) + notification::notification(const client& tclient, std::string data) { + const user& current_user = tclient.getUser(); - } - - notification::notification(std::string data, const user& current_user) - { - try { - auto _data = json::parse(data); + nlohmann::json json; - if (_data.find("in_reply_to_status_id") != _data.end()) + try + { + json = nlohmann::json::parse(data); + } catch (const std::invalid_argument& error) + { + std::throw_with_nested(invalid_response(data)); + } + + try + { + if (!json["in_reply_to_status_id"].is_null()) { _type = type::tweet; - new(&_tweet) tweet(data); - } else if (_data.find("event") != _data.end()) + new(&_tweet) tweet(tclient, data); + } else if (!json["event"].is_null()) { - std::string event = _data.at("event"); - user source(_data.at("source").dump()); - user target(_data.at("target").dump()); + std::string event = json["event"]; + user source(tclient, json["source"].dump()); + user target(tclient, json["target"].dump()); if (event == "user_update") { @@ -50,7 +56,7 @@ namespace twitter { new(&_user) user(target); } else if (event == "favorite") { - new(&_user_and_tweet._tweet) tweet(_data.at("target_object").dump()); + new(&_user_and_tweet._tweet) tweet(tclient, json["target_object"].dump()); if (current_user == source) { @@ -64,7 +70,7 @@ namespace twitter { } } else if (event == "unfavorite") { - new(&_user_and_tweet._tweet) tweet(_data.at("target_object").dump()); + new(&_user_and_tweet._tweet) tweet(tclient, json["target_object"].dump()); if (current_user == source) { @@ -97,20 +103,20 @@ namespace twitter { { _type = type::list_created; - new(&_list) list(_data.at("target_object").dump()); + new(&_list) list(json["target_object"].dump()); } else if (event == "list_destroyed") { _type = type::list_destroyed; - new(&_list) list(_data.at("target_object").dump()); + new(&_list) list(json["target_object"].dump()); } else if (event == "list_updated") { _type = type::list_updated; - new(&_list) list(_data.at("target_object").dump()); + new(&_list) list(json["target_object"].dump()); } else if (event == "list_member_added") { - new(&_user_and_list._list) list(_data.at("target_object").dump()); + new(&_user_and_list._list) list(json["target_object"].dump()); if (current_user == source) { @@ -124,7 +130,7 @@ namespace twitter { } } else if (event == "list_member_removed") { - new(&_user_and_list._list) list(_data.at("target_object").dump()); + new(&_user_and_list._list) list(json["target_object"].dump()); if (current_user == source) { @@ -138,7 +144,7 @@ namespace twitter { } } else if (event == "list_member_subscribe") { - new(&_user_and_list._list) list(_data.at("target_object").dump()); + new(&_user_and_list._list) list(json["target_object"].dump()); if (current_user == source) { @@ -152,7 +158,7 @@ namespace twitter { } } else if (event == "list_member_unsubscribe") { - new(&_user_and_list._list) list(_data.at("target_object").dump()); + new(&_user_and_list._list) list(json["target_object"].dump()); if (current_user == source) { @@ -169,66 +175,67 @@ namespace twitter { _type = type::quoted; new(&_user_and_tweet._user) user(source); - new(&_user_and_tweet._tweet) tweet(_data.at("target_object").dump()); + new(&_user_and_tweet._tweet) tweet(tclient, json["target_object"].dump()); } - } else if (_data.find("warning") != _data.end()) + } else if (!json["warning"].is_null()) { - new(&_warning) std::string(_data.at("warning").at("message").get()); + new(&_warning) std::string(json["warning"]["message"].get()); - if (_data.at("warning").at("code") == "FALLING_BEHIND") + auto warning_code = json["warning"]["code"].get(); + if (warning_code == "FALLING_BEHIND") { _type = type::stall; - } else if (_data.at("warning").at("code") == "FOLLOWS_OVER_LIMIT") + } else if (warning_code == "FOLLOWS_OVER_LIMIT") { _type = type::follow_limit; } else { _type = type::unknown_warning; } - } else if (_data.find("delete") != _data.end()) + } else if (!json["delete"].is_null()) { _type = type::deletion; - _user_id_and_tweet_id._tweet_id = _data.at("delete").at("status").at("id"); - _user_id_and_tweet_id._user_id = _data.at("delete").at("status").at("user_id"); - } else if (_data.find("scrub_geo") != _data.end()) + _user_id_and_tweet_id._tweet_id = json["delete"]["status"]["id"].get(); + _user_id_and_tweet_id._user_id = json["delete"]["status"]["user_id"].get(); + } else if (!json["scrub_geo"].is_null()) { _type = type::scrub_location; - _user_id_and_tweet_id._tweet_id = _data.at("scrub_geo").at("up_to_status_id"); - _user_id_and_tweet_id._user_id = _data.at("scrub_geo").at("user_id"); - } else if (_data.find("limit") != _data.end()) + _user_id_and_tweet_id._tweet_id = json["scrub_geo"]["up_to_status_id"].get(); + _user_id_and_tweet_id._user_id = json["scrub_geo"]["user_id"].get(); + } else if (!json["limit"].is_null()) { _type = type::limit; - _limit = _data.at("limit").at("track"); - } else if (_data.find("status_withheld") != _data.end()) + _limit = json["limit"]["track"].get(); + } else if (!json["status_withheld"].is_null()) { _type = type::withhold_status; - _withhold_status._user_id = _data.at("status_withheld").at("user_id"); - _withhold_status._tweet_id = _data.at("status_withheld").at("id"); + _withhold_status._user_id = json["status_withheld"]["user_id"].get(); + _withhold_status._tweet_id = json["status_withheld"]["id"].get(); new(&_withhold_status._countries) std::vector(); - for (auto s : _data.at("status_withheld").at("withheld_in_countries")) + for (auto s : json["status_withheld"]["withheld_in_countries"]) { _withhold_status._countries.push_back(s); } - } else if (_data.find("user_withheld") != _data.end()) + } else if (!json["user_withheld"].is_null()) { _type = type::withhold_user; - _withhold_user._user_id = _data.at("user_withheld").at("id"); + _withhold_user._user_id = json["user_withheld"]["id"].get(); new(&_withhold_user._countries) std::vector(); - for (auto s : _data.at("user_withheld").at("withheld_in_countries")) + for (auto s : json["user_withheld"]["withheld_in_countries"]) { _withhold_user._countries.push_back(s); } - } else if (_data.find("disconnect") != _data.end()) + } else if (!json["disconnect"].is_null()) { _type = type::disconnect; - switch (_data.at("disconnect").at("code").get()) + switch (json["disconnect"]["code"].get()) { case 1: _disconnect = disconnect_code::shutdown; break; case 2: _disconnect = disconnect_code::duplicate; break; @@ -242,26 +249,26 @@ namespace twitter { case 12: _disconnect = disconnect_code::load; break; default: _disconnect = disconnect_code::unknown; } - } else if (_data.find("friends") != _data.end()) + } else if (!json["friends"].is_null()) { _type = type::friends; - new(&_friends) std::set(_data.at("friends").begin(), _data.at("friends").end()); - } else if (_data.find("direct_message") != _data.end()) + new(&_friends) std::set(std::begin(json["friends"]), std::end(json["friends"])); + } else if (!json["direct_message"].is_null()) { _type = type::direct; - new(&_direct_message) direct_message(_data.at("direct_message").dump()); + new(&_direct_message) direct_message(json["direct_message"].dump()); } else { _type = type::unknown; } - } catch (std::invalid_argument e) + } catch (const std::domain_error& error) { - _type = type::invalid; + std::throw_with_nested(invalid_response(data)); } } - notification::notification(const notification& other) + notification::notification(notification&& other) { _type = other._type; @@ -269,11 +276,11 @@ namespace twitter { { case type::tweet: { - new(&_tweet) tweet(other._tweet); - + new(&_tweet) tweet(std::move(other._tweet)); + break; } - + case type::update_user: case type::block: case type::unblock: @@ -281,32 +288,32 @@ namespace twitter { case type::followed: case type::unfollow: { - new(&_user) user(other._user); - + new(&_user) user(std::move(other._user)); + break; } - + case type::favorite: case type::favorited: case type::unfavorite: case type::unfavorited: case type::quoted: { - new(&_user_and_tweet._user) user(other._user_and_tweet._user); - new(&_user_and_tweet._tweet) tweet(other._user_and_tweet._tweet); - + new(&_user_and_tweet._user) user(std::move(other._user_and_tweet._user)); + new(&_user_and_tweet._tweet) tweet(std::move(other._user_and_tweet._tweet)); + break; } - + case type::list_created: case type::list_destroyed: case type::list_updated: { - new(&_list) list(other._list); - + new(&_list) list(std::move(other._list)); + break; } - + case type::list_add: case type::list_added: case type::list_remove: @@ -316,78 +323,84 @@ namespace twitter { case type::list_unsubscribe: case type::list_unsubscribed: { - new(&_user_and_list._user) user(other._user_and_list._user); - new(&_user_and_list._list) list(other._user_and_list._list); - + new(&_user_and_list._user) user(std::move(other._user_and_list._user)); + new(&_user_and_list._list) list(std::move(other._user_and_list._list)); + break; } - + case type::stall: case type::follow_limit: case type::unknown_warning: { - new(&_warning) std::string(other._warning); - + new(&_warning) std::string(std::move(other._warning)); + break; } - + case type::deletion: case type::scrub_location: { _user_id_and_tweet_id._user_id = other._user_id_and_tweet_id._user_id; _user_id_and_tweet_id._tweet_id = other._user_id_and_tweet_id._tweet_id; - + break; } - + case type::limit: { _limit = other._limit; - + break; } - + case type::withhold_status: { _withhold_status._user_id = other._withhold_status._user_id; _withhold_status._tweet_id = other._withhold_status._tweet_id; - new(&_withhold_status._countries) std::vector(other._withhold_status._countries); - + new(&_withhold_status._countries) std::vector(std::move(other._withhold_status._countries)); + break; } - + case type::withhold_user: { _withhold_user._user_id = other._withhold_user._user_id; - new(&_withhold_user._countries) std::vector(other._withhold_user._countries); - + new(&_withhold_user._countries) std::vector(std::move(other._withhold_user._countries)); + break; } - + case type::disconnect: { _disconnect = other._disconnect; - + break; } - + case type::friends: { - new(&_friends) std::set(other._friends); - + new(&_friends) std::set(std::move(other._friends)); + break; } - + case type::direct: { - new(&_direct_message) direct_message(other._direct_message); - + new(&_direct_message) direct_message(std::move(other._direct_message)); + + break; + } + + case type::unknown: + case type::invalid: + { break; } } } - notification& notification::operator=(const notification& other) + notification& notification::operator=(notification&& other) { this->~notification(); @@ -397,7 +410,7 @@ namespace twitter { { case type::tweet: { - new(&_tweet) tweet(other._tweet); + new(&_tweet) tweet(std::move(other._tweet)); break; } @@ -409,7 +422,7 @@ namespace twitter { case type::followed: case type::unfollow: { - new(&_user) user(other._user); + new(&_user) user(std::move(other._user)); break; } @@ -420,8 +433,8 @@ namespace twitter { case type::unfavorited: case type::quoted: { - new(&_user_and_tweet._user) user(other._user_and_tweet._user); - new(&_user_and_tweet._tweet) tweet(other._user_and_tweet._tweet); + new(&_user_and_tweet._user) user(std::move(other._user_and_tweet._user)); + new(&_user_and_tweet._tweet) tweet(std::move(other._user_and_tweet._tweet)); break; } @@ -430,7 +443,7 @@ namespace twitter { case type::list_destroyed: case type::list_updated: { - new(&_list) list(other._list); + new(&_list) list(std::move(other._list)); break; } @@ -444,8 +457,8 @@ namespace twitter { case type::list_unsubscribe: case type::list_unsubscribed: { - new(&_user_and_list._user) user(other._user_and_list._user); - new(&_user_and_list._list) list(other._user_and_list._list); + new(&_user_and_list._user) user(std::move(other._user_and_list._user)); + new(&_user_and_list._list) list(std::move(other._user_and_list._list)); break; } @@ -454,7 +467,7 @@ namespace twitter { case type::follow_limit: case type::unknown_warning: { - new(&_warning) std::string(other._warning); + new(&_warning) std::string(std::move(other._warning)); break; } @@ -479,7 +492,7 @@ namespace twitter { { _withhold_status._user_id = other._withhold_status._user_id; _withhold_status._tweet_id = other._withhold_status._tweet_id; - new(&_withhold_status._countries) std::vector(other._withhold_status._countries); + new(&_withhold_status._countries) std::vector(std::move(other._withhold_status._countries)); break; } @@ -487,7 +500,7 @@ namespace twitter { case type::withhold_user: { _withhold_user._user_id = other._withhold_user._user_id; - new(&_withhold_user._countries) std::vector(other._withhold_user._countries); + new(&_withhold_user._countries) std::vector(std::move(other._withhold_user._countries)); break; } @@ -501,17 +514,23 @@ namespace twitter { case type::friends: { - new(&_friends) std::set(other._friends); + new(&_friends) std::set(std::move(other._friends)); break; } case type::direct: { - new(&_direct_message) direct_message(other._direct_message); + new(&_direct_message) direct_message(std::move(other._direct_message)); break; } + + case type::invalid: + case type::unknown: + { + break; + } } return *this; @@ -578,6 +597,7 @@ namespace twitter { case type::stall: case type::follow_limit: + case type::unknown_warning: { using string_type = std::string; _warning.~string_type(); @@ -615,10 +635,20 @@ namespace twitter { break; } + + case type::deletion: + case type::scrub_location: + case type::limit: + case type::disconnect: + case type::unknown: + case type::invalid: + { + break; + } } } - tweet notification::getTweet() const + const tweet& notification::getTweet() const { switch (_type) { @@ -639,13 +669,11 @@ namespace twitter { default: { assert(false); - - return tweet(); } } } - user notification::getUser() const + const user& notification::getUser() const { switch (_type) { @@ -683,13 +711,11 @@ namespace twitter { default: { assert(false); - - return user(); } } } - list notification::getList() const + const list& notification::getList() const { switch (_type) { @@ -715,8 +741,6 @@ namespace twitter { default: { assert(false); - - return list(); } } } @@ -739,8 +763,6 @@ namespace twitter { default: { assert(false); - - return 0; } } } @@ -768,13 +790,11 @@ namespace twitter { default: { assert(false); - - return 0; } } } - std::vector notification::getCountries() const + const std::vector& notification::getCountries() const { switch (_type) { @@ -791,8 +811,6 @@ namespace twitter { default: { assert(false); - - return std::vector(); } } } @@ -804,14 +822,14 @@ namespace twitter { return _disconnect; } - std::set notification::getFriends() const + const std::set& notification::getFriends() const { assert(_type == type::friends); return _friends; } - direct_message notification::getDirectMessage() const + const direct_message& notification::getDirectMessage() const { assert(_type == type::direct); @@ -825,7 +843,7 @@ namespace twitter { return _limit; } - std::string notification::getWarning() const + const std::string& notification::getWarning() const { switch (_type) { @@ -839,15 +857,8 @@ namespace twitter { default: { assert(false); - - return ""; } } } - notification::operator bool() const - { - return _type != type::invalid; - } - }; diff --git a/src/notification.h b/src/notification.h index da83b0f..b5ecd49 100644 --- a/src/notification.h +++ b/src/notification.h @@ -11,7 +11,10 @@ namespace twitter { - enum class disconnect_code { + class client; + + enum class disconnect_code + { shutdown, duplicate, stall, @@ -26,118 +29,118 @@ namespace twitter { }; class notification { - public: - enum class type { - // Tweet object - tweet, - - // User object - update_user, - block, - unblock, - follow, - followed, - unfollow, - - // User and tweet - favorite, - favorited, - unfavorite, - unfavorited, - quoted, - - // List - list_created, - list_destroyed, - list_updated, - - // User and list - list_add, - list_added, - list_remove, - list_removed, - list_subscribe, - list_subscribed, - list_unsubscribe, - list_unsubscribed, - - // Warning - stall, - follow_limit, - unknown_warning, - - // User ID and tweet ID - deletion, - scrub_location, - - // Special - limit, - withhold_status, - withhold_user, - disconnect, - friends, - direct, - - // Nothing - unknown, - invalid - }; + public: + enum class type { + // Tweet object + tweet, + + // User object + update_user, + block, + unblock, + follow, + followed, + unfollow, + + // User and tweet + favorite, + favorited, + unfavorite, + unfavorited, + quoted, - type getType() const; + // List + list_created, + list_destroyed, + list_updated, - notification(); - notification(std::string data, const user& current_user); - notification(const notification& other); - notification& operator=(const notification& other); - ~notification(); + // User and list + list_add, + list_added, + list_remove, + list_removed, + list_subscribe, + list_subscribed, + list_unsubscribe, + list_unsubscribed, - tweet getTweet() const; - user getUser() const; - list getList() const; - tweet_id getTweetID() const; - user_id getUserID() const; - std::vector getCountries() const; - disconnect_code getDisconnectCode() const; - std::set getFriends() const; - direct_message getDirectMessage() const; - int getLimit() const; - std::string getWarning() const; + // Warning + stall, + follow_limit, + unknown_warning, - operator bool() const; + // User ID and tweet ID + deletion, + scrub_location, - private: - union { + // Special + limit, + withhold_status, + withhold_user, + disconnect, + friends, + direct, + + // Nothing + unknown, + invalid + }; + + type getType() const; + + notification(const client& tclient, std::string data); + notification(notification&& other); + notification& operator=(notification&& other); + ~notification(); + + notification(const notification& other) = delete; + notification& operator=(const notification& other) = delete; + + const tweet& getTweet() const; + const user& getUser() const; + const list& getList() const; + tweet_id getTweetID() const; + user_id getUserID() const; + const std::vector& getCountries() const; + disconnect_code getDisconnectCode() const; + const std::set& getFriends() const; + const direct_message& getDirectMessage() const; + int getLimit() const; + const std::string& getWarning() const; + + private: + union { + tweet _tweet; + user _user; + list _list; + struct { + user _user; tweet _tweet; + } _user_and_tweet; + struct { user _user; list _list; - struct { - user _user; - tweet _tweet; - } _user_and_tweet; - struct { - user _user; - list _list; - } _user_and_list; - std::string _warning; - struct { - user_id _user_id; - tweet_id _tweet_id; - } _user_id_and_tweet_id; - int _limit; - struct { - user_id _user_id; - tweet_id _tweet_id; - std::vector _countries; - } _withhold_status; - struct { - user_id _user_id; - std::vector _countries; - } _withhold_user; - disconnect_code _disconnect; - std::set _friends; - direct_message _direct_message; - }; - type _type; + } _user_and_list; + std::string _warning; + struct { + user_id _user_id; + tweet_id _tweet_id; + } _user_id_and_tweet_id; + int _limit; + struct { + user_id _user_id; + tweet_id _tweet_id; + std::vector _countries; + } _withhold_status; + struct { + user_id _user_id; + std::vector _countries; + } _withhold_user; + disconnect_code _disconnect; + std::set _friends; + direct_message _direct_message; + }; + type _type; }; }; diff --git a/src/stream.cpp b/src/stream.cpp new file mode 100644 index 0000000..17dcce7 --- /dev/null +++ b/src/stream.cpp @@ -0,0 +1,291 @@ +#include "stream.h" +#include +#include +#include +#include "util.h" +#include "notification.h" +#include "client.h" + +namespace twitter { + + stream::stream( + const client& tclient, + notify_callback callback, + bool with_followings, + bool receive_all_replies, + std::list track, + std::list locations) : + _client(tclient), + _notify(callback), + _thread(&stream::run, this, generateUrl(with_followings, receive_all_replies, track, locations)) + { + } + + stream::~stream() + { + if (_thread.joinable()) + { + _stop = true; + _thread.join(); + } + } + + std::string stream::generateUrl( + bool with_followings, + bool receive_all_replies, + std::list track, + std::list locations) + { + std::list arguments; + + if (receive_all_replies) + { + arguments.push_back("replies=all"); + } + + if (!with_followings) + { + arguments.push_back("with=user"); + } + + if (!track.empty()) + { + std::ostringstream trackstr; + trackstr << "track="; + + for (auto it = std::begin(track); it != std::end(track); it++) + { + if (it != std::begin(track)) + { + trackstr << ","; + } + + trackstr << OAuth::HttpEncodeQueryValue(*it); + } + + arguments.push_back(trackstr.str()); + } + + if (!locations.empty()) + { + std::ostringstream localstr; + localstr << "locations="; + + for (auto it = std::begin(locations); it != std::end(locations); it++) + { + if (it != std::begin(locations)) + { + localstr << ","; + } + + localstr << (double)it->getSouthWestLongitude() << ","; + localstr << (double)it->getSouthWestLatitude() << ","; + localstr << (double)it->getNorthEastLongitude() << ","; + localstr << (double)it->getNorthEastLatitude(); + } + + arguments.push_back(localstr.str()); + } + + std::ostringstream urlstr; + urlstr << "https://userstream.twitter.com/1.1/user.json"; + + if (!arguments.empty()) + { + urlstr << "?"; + urlstr << implode(std::begin(arguments), std::end(arguments), "&"); + } + + return urlstr.str(); + } + + void stream::run(std::string url) + { + curl::curl_ios ios(this, [] (void* contents, size_t size, size_t nmemb, void* userp) { + return static_cast(userp)->write(static_cast(contents), size, nmemb); + }); + + curl::curl_easy conn(ios); + curl::curl_header headers; + std::string oauth_header; + + try + { + oauth_header = _client._oauth_client->getFormattedHttpHeader(OAuth::Http::Get, url, ""); + + if (!oauth_header.empty()) + { + headers.add(oauth_header); + } + } catch (const OAuth::ParseError& error) + { + std::cout << "Error generating OAuth header:" << std::endl; + std::cout << error.what() << std::endl; + std::cout << "This is likely due to a malformed URL." << std::endl; + + assert(false); + } + + try + { + conn.add(nullptr); + conn.add(nullptr); + conn.add([] (void* cdata, curl_off_t, curl_off_t, curl_off_t, curl_off_t) { + return static_cast(cdata)->progress(); + }); + conn.add(this); + conn.add(0); + //conn.add(1); + //conn.add(my_trace); + conn.add(url.c_str()); + conn.add(headers.get()); + } catch (const curl::curl_exception& error) + { + error.print_traceback(); + + assert(false); + } + + _backoff_type = backoff::none; + _backoff_amount = std::chrono::milliseconds(0); + for (;;) + { + bool failure = false; + try + { + conn.perform(); + } catch (const curl::curl_easy_exception& error) + { + failure = true; + if ((error.get_code() == CURLE_ABORTED_BY_CALLBACK) && _stop) + { + break; + } else { + if (_backoff_type == backoff::none) + { + _established = false; + _backoff_type = backoff::network; + _backoff_amount = std::chrono::milliseconds(0); + } + } + } + + if (!failure) + { + long response_code = conn.get_info().get(); + if (response_code == 420) + { + if (_backoff_type == backoff::none) + { + _established = false; + _backoff_type = backoff::rate_limit; + _backoff_amount = std::chrono::minutes(1); + } + } else if (response_code != 200) + { + if (_backoff_type == backoff::none) + { + _established = false; + _backoff_type = backoff::http; + _backoff_amount = std::chrono::seconds(5); + } + } else { + if (_backoff_type == backoff::none) + { + _established = false; + _backoff_type = backoff::network; + _backoff_amount = std::chrono::milliseconds(0); + } + } + } + + std::this_thread::sleep_for(_backoff_amount); + + switch (_backoff_type) + { + case backoff::network: + { + if (_backoff_amount < std::chrono::seconds(16)) + { + _backoff_amount += std::chrono::milliseconds(250); + } + + break; + } + + case backoff::http: + { + if (_backoff_amount < std::chrono::seconds(320)) + { + _backoff_amount *= 2; + } + + break; + } + + case backoff::rate_limit: + { + _backoff_amount *= 2; + + break; + } + + case backoff::none: + { + break; + } + } + } + } + + size_t stream::write(char* ptr, size_t size, size_t nmemb) + { + for (size_t i = 0; i < size*nmemb; i++) + { + if (ptr[i] == '\r') + { + i++; // Skip the \n + + if (!_buffer.empty()) + { + notification n(_client, _buffer); + if (n.getType() == notification::type::friends) + { + _established = true; + _backoff_type = backoff::none; + _backoff_amount = std::chrono::milliseconds(0); + } + + _notify(n); + + _buffer = ""; + } + } else { + _buffer.push_back(ptr[i]); + } + } + + time(&_last_write); + + return size*nmemb; + } + + int stream::progress() + { + if (_stop) + { + return 1; + } + + if (_established) + { + if (difftime(time(NULL), _last_write) >= 90) + { + return 1; + } + } + + return 0; + } + +} diff --git a/src/stream.h b/src/stream.h new file mode 100644 index 0000000..3391912 --- /dev/null +++ b/src/stream.h @@ -0,0 +1,67 @@ +#ifndef STREAM_H_E9146952 +#define STREAM_H_E9146952 + +#include +#include +#include +#include +#include +#include "bounding_box.h" + +namespace twitter { + + class client; + class notification; + + class stream { + public: + + typedef std::function notify_callback; + + stream( + const client& tclient, + notify_callback callback, + bool with_followings = true, + bool receive_all_replies = false, + std::list track = {}, + std::list locations = {}); + + ~stream(); + + stream(const stream& other) = delete; + stream(stream&& other) = delete; + stream& operator=(const stream& other) = delete; + stream& operator=(stream&& other) = delete; + + private: + enum class backoff { + none, + network, + http, + rate_limit + }; + + static std::string generateUrl( + bool with_followings, + bool receive_all_replies, + std::list track, + std::list locations); + + void run(std::string url); + int progress(); + size_t write(char* ptr, size_t size, size_t nmemb); + + const client& _client; + notify_callback _notify; + bool _stop = false; + std::string _buffer; + time_t _last_write; + bool _established = false; + backoff _backoff_type = backoff::none; + std::chrono::milliseconds _backoff_amount; + std::thread _thread; + }; + +} + +#endif /* end of include guard: STREAM_H_E9146952 */ diff --git a/src/tweet.cpp b/src/tweet.cpp index 2927018..4849fb7 100644 --- a/src/tweet.cpp +++ b/src/tweet.cpp @@ -1,134 +1,69 @@ #include "tweet.h" #include -#include - -using nlohmann::json; +#include "util.h" +#include "codes.h" +#include "client.h" namespace twitter { - tweet::tweet() : _valid(false) - { - - } - - tweet::tweet(std::string data) : _valid(true) + tweet::tweet(const client& tclient, std::string data) try + : _client(tclient) { - auto _data = json::parse(data); - _id = _data.at("id"); - _text = _data.at("text"); - _author = user(_data.at("user").dump()); + auto json = nlohmann::json::parse(data); + _id = json["id"].get(); + _text = json["text"].get(); + _author = make_unique(_client, json["user"].dump()); - if (_data.find("retweeted_status") != _data.end()) + if (!json["retweeted_status"].is_null()) { _is_retweet = true; - std::string retweet = _data.at("retweeted_status").dump(); - _retweeted_status = new tweet(retweet); + _retweeted_status = make_unique(_client, json["retweeted_status"].dump()); } - if (_data.find("entities") != _data.end()) + if (!json["entities"].is_null()) { - auto _entities = _data.at("entities"); - if (_entities.find("user_mentions") != _entities.end()) + auto entities = json["entities"]; + if (!entities["user_mentions"].is_null()) { - for (auto _mention : _entities.at("user_mentions")) + for (auto mention : entities["user_mentions"]) { - _mentions.push_back(std::make_pair(_mention.at("id"), _mention.at("screen_name").get())); + _mentions.push_back(std::make_pair(mention["id"].get(), mention["screen_name"].get())); } } } - } - - tweet::tweet(const tweet& other) + } catch (const std::invalid_argument& error) { - _valid = other._valid; - _id = other._id; - _text = other._text; - _author = other._author; - _is_retweet = other._is_retweet; - - if (_is_retweet) - { - _retweeted_status = new tweet(*other._retweeted_status); - } - - _mentions = other._mentions; - } - - tweet::tweet(tweet&& other) : tweet() + std::throw_with_nested(malformed_object("tweet", data)); + } catch (const std::domain_error& error) { - swap(*this, other); + std::throw_with_nested(malformed_object("tweet", data)); } - tweet::~tweet() + std::string tweet::generateReplyPrefill() const { - if (_is_retweet) + std::ostringstream output; + output << "@" << _author->getScreenName() << " "; + + for (auto mention : _mentions) { - delete _retweeted_status; + if ((mention.first != _author->getID()) && (mention.first != _client.getUser().getID())) + { + output << "@" << mention.second << " "; + } } - } - - tweet& tweet::operator=(tweet other) - { - swap(*this, other); - - return *this; - } - - void swap(tweet& first, tweet& second) - { - std::swap(first._valid, second._valid); - std::swap(first._id, second._id); - std::swap(first._text, second._text); - std::swap(first._author, second._author); - std::swap(first._is_retweet, second._is_retweet); - std::swap(first._retweeted_status, second._retweeted_status); - std::swap(first._mentions, second._mentions); - } - - tweet_id tweet::getID() const - { - assert(_valid); - - return _id; - } - - std::string tweet::getText() const - { - assert(_valid); - - return _text; - } - - const user& tweet::getAuthor() const - { - assert(_valid); - - return _author; - } - - bool tweet::isRetweet() const - { - assert(_valid); - - return _is_retweet; - } - - tweet tweet::getRetweet() const - { - assert(_valid && _is_retweet); - return *_retweeted_status; + return output.str(); } - std::vector> tweet::getMentions() const + tweet tweet::reply(std::string message, std::list media_ids) const { - return _mentions; + return _client.replyToTweet(message, _id, media_ids); } - tweet::operator bool() const + bool tweet::isMyTweet() const { - return _valid; + return *_author == _client.getUser(); } }; diff --git a/src/tweet.h b/src/tweet.h index 387f73a..3035d51 100644 --- a/src/tweet.h +++ b/src/tweet.h @@ -2,41 +2,75 @@ #define TWEET_H_CE980721 #include -#include "user.h" #include #include +#include +#include +#include "user.h" namespace twitter { + class client; + typedef unsigned long long tweet_id; class tweet { public: - tweet(); - tweet(std::string data); - tweet(const tweet& other); - tweet(tweet&& other); - ~tweet(); - tweet& operator=(tweet other); - friend void swap(tweet& first, tweet& second); + tweet(const client& tclient, std::string data); + + tweet(const tweet& other) = delete; + tweet& operator=(const tweet& other) = delete; + + tweet(tweet&& other) = default; + tweet& operator=(tweet&& other) = default; + + tweet_id getID() const + { + return _id; + } + + std::string getText() const + { + return _text; + } - tweet_id getID() const; - std::string getText() const; - const user& getAuthor() const; - bool isRetweet() const; - tweet getRetweet() const; - std::vector> getMentions() const; + const user& getAuthor() const + { + return *_author; + } - operator bool() const; + bool isRetweet() const + { + return _is_retweet; + } + + const tweet& getRetweet() const + { + assert(_is_retweet); + + return *_retweeted_status; + } + + const std::vector>& getMentions() const + { + return _mentions; + } + + std::string generateReplyPrefill() const; + + tweet reply(std::string message, std::list media_ids = {}) const; + + bool isMyTweet() const; private: - bool _valid; + + const client& _client; tweet_id _id; std::string _text; - user _author; + std::unique_ptr _author; bool _is_retweet = false; - tweet* _retweeted_status = nullptr; + std::unique_ptr _retweeted_status; std::vector> _mentions; }; diff --git a/src/twitter.h b/src/twitter.h index 90925df..1ba4394 100644 --- a/src/twitter.h +++ b/src/twitter.h @@ -11,6 +11,7 @@ namespace twitter { #include "util.h" #include "auth.h" #include "client.h" +#include "stream.h" #include "tweet.h" #include "user.h" #include "notification.h" diff --git a/src/user.cpp b/src/user.cpp index 0fa1e39..0b6e93a 100644 --- a/src/user.cpp +++ b/src/user.cpp @@ -1,51 +1,43 @@ #include "user.h" #include - -using nlohmann::json; +#include "codes.h" +#include "client.h" namespace twitter { - user::user() : _valid(false) - { - - } - - user::user(std::string data) : _valid(true) + user::user(const client& tclient, std::string data) try + : _client(tclient) { - auto _data = json::parse(data); - _id = _data.at("id"); - _screen_name = _data.at("screen_name"); - _name = _data.at("name"); - } - - user_id user::getID() const + auto json = nlohmann::json::parse(data); + _id = json["id"].get(); + _screen_name = json["screen_name"].get(); + _name = json["name"].get(); + } catch (const std::invalid_argument& error) { - return _id; - } - - std::string user::getScreenName() const + std::throw_with_nested(malformed_object("user", data)); + } catch (const std::domain_error& error) { - return _screen_name; + std::throw_with_nested(malformed_object("user", data)); } - std::string user::getName() const + std::set user::getFriends() const { - return _name; + return _client.getFriends(_id); } - user::operator bool() const + std::set user::getFollowers() const { - return _valid; + return _client.getFollowers(_id); } - bool user::operator==(const user& other) const + void user::follow() const { - return _id == other._id; + _client.follow(_id); } - bool user::operator!=(const user& other) const + void user::unfollow() const { - return _id != other._id; + _client.unfollow(_id); } }; diff --git a/src/user.h b/src/user.h index 1d8be99..f08840b 100644 --- a/src/user.h +++ b/src/user.h @@ -2,26 +2,52 @@ #define USER_H_BF3AB38C #include +#include namespace twitter { + class client; + typedef unsigned long long user_id; class user { public: - user(); - user(std::string data); - user_id getID() const; - std::string getScreenName() const; - std::string getName() const; + user(const client& tclient, std::string data); + + user_id getID() const + { + return _id; + } + + std::string getScreenName() const + { + return _screen_name; + } + + std::string getName() const + { + return _name; + } - operator bool() const; - bool operator==(const user& other) const; - bool operator!=(const user& other) const; + bool operator==(const user& other) const + { + return _id == other._id; + } + + bool operator!=(const user& other) const + { + return _id != other._id; + } + + std::set getFriends() const; + std::set getFollowers() const; + void follow() const; + void unfollow() const; private: - bool _valid = false; + + const client& _client; user_id _id; std::string _screen_name; std::string _name; diff --git a/src/util.h b/src/util.h index e1c9370..2f4b9a3 100644 --- a/src/util.h +++ b/src/util.h @@ -24,6 +24,12 @@ namespace twitter { return result.str(); } + template + std::unique_ptr make_unique(Args&&... args) + { + return std::unique_ptr(new T(std::forward(args)...)); + } + }; #endif /* end of include guard: UTIL_H_440DEAA0 */ diff --git a/vendor/liboauthcpp b/vendor/liboauthcpp index b460cdf..9ebd39b 160000 --- a/vendor/liboauthcpp +++ b/vendor/liboauthcpp @@ -1 +1 @@ -Subproject commit b460cdfadb599c5cea88cbaff0539b7ba9fb17b8 +Subproject commit 9ebd39b8be7ccb846c13cc50d995b666500d495a -- cgit 1.4.1