From 7e85b35d7d1714e3f85434b891a1050ad584e337 Mon Sep 17 00:00:00 2001 From: Kelly Rauchenberger Date: Wed, 13 Apr 2016 11:24:24 -0400 Subject: Added ability to upload media and tweet it Images (static and animated) and videos have been tested. Currently all media uploads occur in one large chunk; support to break down chunks will be added later. --- src/auth.cpp | 2 +- src/client.cpp | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- src/client.h | 15 ++- src/codes.cpp | 24 +++++ src/codes.h | 31 ++++++ src/tweet.cpp | 2 +- src/tweet.h | 4 + src/twitter.h | 24 +---- src/util.h | 29 ++++++ 9 files changed, 402 insertions(+), 40 deletions(-) create mode 100644 src/codes.cpp create mode 100644 src/codes.h create mode 100644 src/util.h (limited to 'src') diff --git a/src/auth.cpp b/src/auth.cpp index 325c521..f0f17e0 100644 --- a/src/auth.cpp +++ b/src/auth.cpp @@ -1,4 +1,4 @@ -#include "twitter.h" +#include "auth.h" namespace twitter { diff --git a/src/client.cpp b/src/client.cpp index b71ff70..39d6b5d 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -1,10 +1,81 @@ #include "client.h" -#include -#include #include #include #include #include +#include "util.h" + +// These are here for debugging curl stuff + +static +void dump(const char *text, + FILE *stream, unsigned char *ptr, size_t size) +{ + size_t i; + size_t c; + unsigned int width=80; + + fprintf(stream, "%s, %10.10ld bytes (0x%8.8lx)\n", + text, (long)size, (long)size); + + for(i=0; i= 0x20 && ptr[i+c] < 0x80) ? ptr[i+c] : '.'; + fputc(x, stream); + } + + fputc('\n', stream); /* newline */ + } +} + +static +int my_trace(CURL *handle, curl_infotype type, + char *data, size_t size, + void *userp) +{ + const char *text; + (void)handle; /* prevent compiler warning */ + + switch (type) { + case CURLINFO_TEXT: + fprintf(stderr, "== Info: %s", data); + default: /* in case a new one is introduced to shock us */ + return 0; + + case CURLINFO_HEADER_OUT: + text = "=> Send header"; + break; + case CURLINFO_DATA_OUT: + text = "=> Send data"; + break; + case CURLINFO_SSL_DATA_OUT: + text = "=> Send SSL data"; + break; + case CURLINFO_HEADER_IN: + text = "<= Recv header"; + break; + case CURLINFO_DATA_IN: + text = "<= Recv data"; + break; + case CURLINFO_SSL_DATA_IN: + text = "<= Recv SSL data"; + break; + } + + dump(text, stderr, (unsigned char *)data, size); + return 0; +} namespace twitter { @@ -22,18 +93,181 @@ namespace twitter { delete _oauth_consumer; } - response client::updateStatus(std::string msg, tweet& result) + response client::updateStatus(std::string msg, tweet& result, std::list media_ids) { - std::ostringstream output; - curl::curl_ios ios(output); - curl::curl_easy conn(ios); - std::stringstream datastrstream; datastrstream << "status=" << OAuth::PercentEncode(msg); + 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"; + long response_code; + json response_data; + if (!performPost(url, datastr, response_code, response_data)) + { + return response::curl_error; + } + + if (response_code == 200) + { + result = tweet(response_data); + return response::ok; + } else { + return codeForError(response_code, response_data); + } + } + + response client::uploadMedia(std::string media_type, const char* data, long data_length, long& media_id) + { + curl::curl_form form; + + 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"); + curl::curl_pair bytes_cont(CURLFORM_COPYCONTENTS, std::to_string(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"); + curl::curl_pair category_cont(CURLFORM_COPYCONTENTS, "tweet_gif"); + form.add(category_name, category_cont); + } + + long response_code; + json response_data; + if (!performMultiPost("https://upload.twitter.com/1.1/media/upload.json", form.get(), response_code, response_data)) + { + return response::curl_error; + } + + if (response_code / 100 != 2) + { + return codeForError(response_code, response_data); + } + + media_id = response_data["media_id"].get(); + + 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)) + { + return response::curl_error; + } + + curl_formfree(append_form_post); + + if (response_code / 100 != 2) + { + return codeForError(response_code, response_data); + } + + curl::curl_form finalize_form; + 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"); + curl::curl_pair media_id_cont(CURLFORM_COPYCONTENTS, std::to_string(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; + } + + if (response_code / 100 != 2) + { + return codeForError(response_code, response_data); + } + + if (response_data.find("processing_info") != response_data.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; + } + + if (response_code / 100 != 2) + { + return codeForError(response_code, response_data); + } + + if (response_data["processing_info"]["state"] == "succeeded") + { + break; + } + + int ttw = response_data["processing_info"]["check_after_secs"].get(); + sleep(ttw); + } + } + + return response::ok; + } + + bool client::performGet(std::string url, long& response_code, json& 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(); + if (output.str().empty()) + { + result = json(); + } else { + result = json::parse(output.str()); + } + + return true; + } + + bool client::performPost(std::string url, std::string datastr, long& response_code, json& 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()) @@ -42,9 +276,10 @@ namespace twitter { } try { + //conn.add(1); + //conn.add(my_trace); conn.add(url.c_str()); conn.add(datastr.c_str()); - conn.add(1); conn.add(headers.get()); conn.perform(); @@ -52,17 +287,62 @@ namespace twitter { { error.print_traceback(); - return response::curl_error; + return false; } - long response_code = conn.get_info().get(); - json response_data = json::parse(output.str()); - if (response_code == 200) + response_code = conn.get_info().get(); + if (output.str().empty()) { - result = tweet(response_data); - return response::ok; + result = json(); + } else { + result = json::parse(output.str()); } + return true; + } + + bool client::performMultiPost(std::string url, const curl_httppost* fields, long& response_code, json& 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(); + + if (output.str().empty()) + { + result = json(); + } else { + result = json::parse(output.str()); + } + + return true; + } + + response client::codeForError(int response_code, json response_data) const + { std::set error_codes; if (response_data.find("errors") != response_data.end()) { @@ -101,6 +381,9 @@ namespace twitter { } 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; diff --git a/src/client.h b/src/client.h index 1ab5c70..3a133e4 100644 --- a/src/client.h +++ b/src/client.h @@ -1,7 +1,12 @@ #ifndef TWITTER_H_ABFF6A12 #define TWITTER_H_ABFF6A12 -#include "twitter.h" +#include "codes.h" +#include "tweet.h" +#include "auth.h" +#include +#include +#include namespace OAuth { class Consumer; @@ -16,12 +21,18 @@ namespace twitter { client(const auth& _auth); ~client(); - response updateStatus(std::string msg, tweet& result); + response updateStatus(std::string msg, tweet& result, std::list media_ids = {}); + response uploadMedia(std::string media_type, const char* data, long data_length, long& media_id); private: OAuth::Consumer* _oauth_consumer; OAuth::Token* _oauth_token; OAuth::Client* _oauth_client; + + bool performGet(std::string url, long& response_code, json& result); + bool performPost(std::string url, std::string dataStr, long& response_code, json& result); + bool performMultiPost(std::string url, const curl_httppost* fields, long& response_code, json& result); + response codeForError(int httpcode, json errors) const; }; }; diff --git a/src/codes.cpp b/src/codes.cpp new file mode 100644 index 0000000..9639d5d --- /dev/null +++ b/src/codes.cpp @@ -0,0 +1,24 @@ +#include "codes.h" + +std::ostream& operator<<(std::ostream& os, twitter::response r) +{ + switch (r) + { + 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"; + } +} diff --git a/src/codes.h b/src/codes.h new file mode 100644 index 0000000..334f0ce --- /dev/null +++ b/src/codes.h @@ -0,0 +1,31 @@ +#ifndef CODES_H_05838D39 +#define CODES_H_05838D39 + +#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 + }; + +}; + +std::ostream& operator<<(std::ostream& os, twitter::response r); + +#endif /* end of include guard: CODES_H_05838D39 */ diff --git a/src/tweet.cpp b/src/tweet.cpp index 165187e..3ba3aa3 100644 --- a/src/tweet.cpp +++ b/src/tweet.cpp @@ -1,4 +1,4 @@ -#include "twitter.h" +#include "tweet.h" namespace twitter { diff --git a/src/tweet.h b/src/tweet.h index 1d83aae..e099579 100644 --- a/src/tweet.h +++ b/src/tweet.h @@ -1,6 +1,10 @@ #ifndef TWEET_H_CE980721 #define TWEET_H_CE980721 +#include + +using nlohmann::json; + namespace twitter { class tweet { diff --git a/src/twitter.h b/src/twitter.h index 39618c9..f9534c6 100644 --- a/src/twitter.h +++ b/src/twitter.h @@ -3,32 +3,12 @@ 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 - }; - class tweet; }; -#include - -using nlohmann::json; - +#include "codes.h" +#include "util.h" #include "auth.h" #include "client.h" #include "tweet.h" diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..e1c9370 --- /dev/null +++ b/src/util.h @@ -0,0 +1,29 @@ +#ifndef UTIL_H_440DEAA0 +#define UTIL_H_440DEAA0 + +#include +#include + +namespace twitter { + + template + std::string implode(InputIterator first, InputIterator last, std::string delimiter) + { + std::stringstream result; + + for (InputIterator it = first; it != last; it++) + { + if (it != first) + { + result << delimiter; + } + + result << *it; + } + + return result.str(); + } + +}; + +#endif /* end of include guard: UTIL_H_440DEAA0 */ -- cgit 1.4.1