From 1f78b00205f885c6becdbb51688393cd37573899 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 16:46:27 +0100 Subject: [PATCH 01/13] Replace string with string_view where possible. --- include/api.hpp | 2 +- include/connection.hpp | 11 ++++++++--- include/curl_wrapper.hpp | 4 ++-- include/instance.hpp | 4 ++-- src/connection.cpp | 6 +++--- src/curl_wrapper.cpp | 20 ++++++++++---------- src/instance.cpp | 2 +- 7 files changed, 27 insertions(+), 22 deletions(-) diff --git a/include/api.hpp b/include/api.hpp index e508fa1..7c89af4 100644 --- a/include/api.hpp +++ b/include/api.hpp @@ -312,7 +312,7 @@ public: [[nodiscard]] inline string_view to_string_view() const { - return _endpoint_map.at(_endpoint).data(); + return _endpoint_map.at(_endpoint); } private: diff --git a/include/connection.hpp b/include/connection.hpp index 90027fb..fcb2a23 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -33,7 +33,12 @@ using std::string; using std::string_view; using std::variant; -using endpoint_variant = variant; +/*! + * @brief An endpoint. Either API::endpoint_type or `std::string_view`. + * + * @since 0.1.0 + */ +using endpoint_variant = variant; /*! * @brief Represents a connection to an instance. Used for requests. @@ -66,7 +71,7 @@ public: * })}; * @endcode * - * @param endpoint Endpoint as API::endpoint_type or `std::string`. + * @param endpoint Endpoint as API::endpoint_type or `std::string_view`. * @param parameters A map of parameters. * * @@ -83,7 +88,7 @@ public: * auto answer{connection.get("/api/v1/instance")}; * @endcode * - * @param endpoint Endpoint as API::endpoint_type or `std::string`. + * @param endpoint Endpoint as API::endpoint_type or `std::string_view`. * * @since 0.1.0 */ diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index 146e481..ecc30e1 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -58,13 +58,13 @@ enum class http_method * parametermap parameters * { * {"id", "12"}, - * {"poll[options]", vector{"Yes", "No", "Maybe"}} + * {"poll[options]", vector{"Yes", "No", "Maybe"}} * }; * @endcode * * @since 0.1.0 */ -using parametermap = map>>; +using parametermap = map>>; /*! * @brief Handles the details of network connections. diff --git a/include/instance.hpp b/include/instance.hpp index 914a470..cecb324 100644 --- a/include/instance.hpp +++ b/include/instance.hpp @@ -50,8 +50,8 @@ public: * * @since 0.1.0 */ - explicit Instance(const string_view &hostname, - const string_view &access_token); + explicit Instance(const string_view hostname, + const string_view access_token); /*! * @brief Returns the hostname. diff --git a/src/connection.cpp b/src/connection.cpp index 794247b..f471c4e 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -29,16 +29,16 @@ Connection::Connection(Instance &instance) answer_type Connection::get(const endpoint_variant &endpoint, const parametermap ¶meters) { - string uri{[&] + const string uri{[&] { if (holds_alternative(endpoint)) { return string(_baseuri).append( API{std::get(endpoint)}.to_string_view()); } - - return std::get(endpoint); + return string(std::get(endpoint)); }()}; + return make_request(http_method::GET, uri, parameters); } diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index 1e1040b..7b0eb79 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -88,32 +88,32 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, if (pos != string::npos) { uri.replace(pos, param.first.size() + 2, - get(param.second)); + get(param.second)); } continue; } static bool first{true}; if (first) { - uri.append("?"); + uri += "?"; first = false; } else { - uri.append("&"); + uri += "&"; } - if (holds_alternative(param.second)) + if (holds_alternative(param.second)) { - uri.append(param.first + '=' + get(param.second)); + ((uri += param.first) += "=") += get(param.second); } else { - for (const auto &arg : get>(param.second)) + for (const auto &arg : get>(param.second)) { - uri.append(param.first + "[]=" + arg); - if (arg != *get>(param.second).rbegin()) + ((uri += param.first) += "[]=") += arg; + if (arg != *get>(param.second).rbegin()) { - uri.append("&"); + uri += "&"; } } } @@ -238,7 +238,7 @@ void CURLWrapper::setup_curl() // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) code = curl_easy_setopt(_connection, CURLOPT_USERAGENT, - string("mastorss/").append(version).c_str()); + (string("mastorss/") += version).c_str()); if (code != CURLE_OK) { throw CURLException{code, "Failed to set User-Agent", diff --git a/src/instance.cpp b/src/instance.cpp index e10675f..72369e5 100644 --- a/src/instance.cpp +++ b/src/instance.cpp @@ -23,7 +23,7 @@ namespace mastodonpp using std::stoull; -Instance::Instance(const string_view &hostname, const string_view &access_token) +Instance::Instance(const string_view hostname, const string_view access_token) : _hostname{hostname} , _baseuri{"https://" + _hostname} , _access_token{access_token} From deed340f3d98150f96ef78e7bb4e0ae9f35d4436 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 17:16:15 +0100 Subject: [PATCH 02/13] Add set_proxy(). --- include/connection.hpp | 7 +++++++ include/curl_wrapper.hpp | 12 ++++++++++++ src/connection.cpp | 6 ++++++ src/curl_wrapper.cpp | 10 ++++++++++ 4 files changed, 35 insertions(+) diff --git a/include/connection.hpp b/include/connection.hpp index fcb2a23..2469738 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -98,6 +98,13 @@ public: return get(endpoint, {}); } + /*! @copydoc CURLWrapper::set_proxy(string_view) + * + * Sets also the proxy for the Instance you used to initialize this + * Connection. + */ + void set_proxy(const string_view proxy); + private: Instance &_instance; const string_view _baseuri; diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index ecc30e1..faf79a5 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -127,6 +127,18 @@ public: return _connection; } + /*! + * @brief Set the proxy to use. + * + * See [CURLOPT_PROXY(3)] + * (https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html). + * + * @param proxy Examples: "socks4a://127.0.0.1:9050", "http://[::1]:3128". + * + * @since 0.1.0 + */ + void set_proxy(const string_view proxy); + protected: /*! * @brief Make a HTTP request. diff --git a/src/connection.cpp b/src/connection.cpp index f471c4e..9bc43f0 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -42,4 +42,10 @@ answer_type Connection::get(const endpoint_variant &endpoint, return make_request(http_method::GET, uri, parameters); } +void Connection::set_proxy(const string_view proxy) +{ + CURLWrapper::set_proxy(proxy); + _instance.set_proxy(proxy); +} + } // namespace mastodonpp diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index 7b0eb79..0b8b49c 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -63,6 +63,16 @@ CURLWrapper::~CURLWrapper() noexcept } } +void CURLWrapper::set_proxy(const string_view proxy) +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + CURLcode code = curl_easy_setopt(_connection, CURLOPT_PROXY, proxy); + if (code != CURLE_OK) + { + throw CURLException{code, "Failed to set proxy", _curl_buffer_error}; + } +} + answer_type CURLWrapper::make_request(const http_method &method, string uri, const parametermap ¶meters) { From 577c252b4232af466a6c186b5fa5fc79e999747c Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 17:20:44 +0100 Subject: [PATCH 03/13] Improve documentation. --- include/connection.hpp | 1 + include/mastodonpp.hpp | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/connection.hpp b/include/connection.hpp index 2469738..09a3945 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -83,6 +83,7 @@ public: /*! * @brief Make a HTTP GET call. + * * Example: * @code * auto answer{connection.get("/api/v1/instance")}; diff --git a/include/mastodonpp.hpp b/include/mastodonpp.hpp index 85f1214..eecac21 100644 --- a/include/mastodonpp.hpp +++ b/include/mastodonpp.hpp @@ -74,17 +74,17 @@ * @section exceptions Exceptions * * Any unrecoverable libcurl error will be thrown as a - * mastodonpp::CURLException. Network errors will **not** be thrown, but - * reported via the return value. + * mastodonpp::CURLException. Network errors will not be thrown, but reported + * via the return value. * * @section thread_safety Thread safety * * The first time you construct an @link mastodonpp::Instance Instance @endlink - * or @link mastodonpp::Connection Connection @endlink, [curl_global_init()] + * or @link mastodonpp::Connection Connection @endlink, [curl_global_init(3)] * (https://curl.haxx.se/libcurl/c/curl_global_init.html) is called. When the * last @link mastodonpp::Instance Instance @endlink or @link * mastodonpp::Connection Connection @endlink is destroyed, - * [curl_global_cleanup()] + * [curl_global_cleanup(3)] * (https://curl.haxx.se/libcurl/c/curl_global_cleanup.html) is called. Both * are not thread safe. * From 4b118e75f13c15d8ab197ea5c2314b06956f2e76 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 17:38:27 +0100 Subject: [PATCH 04/13] Fix / ignore some clang-tidy warnings. --- include/connection.hpp | 2 +- include/curl_wrapper.hpp | 2 +- include/instance.hpp | 3 +-- src/api.cpp | 2 ++ 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/connection.hpp b/include/connection.hpp index 09a3945..5ffe969 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -104,7 +104,7 @@ public: * Sets also the proxy for the Instance you used to initialize this * Connection. */ - void set_proxy(const string_view proxy); + void set_proxy(string_view proxy); private: Instance &_instance; diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index faf79a5..dcd40ec 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -137,7 +137,7 @@ public: * * @since 0.1.0 */ - void set_proxy(const string_view proxy); + void set_proxy(string_view proxy); protected: /*! diff --git a/include/instance.hpp b/include/instance.hpp index cecb324..28f8f27 100644 --- a/include/instance.hpp +++ b/include/instance.hpp @@ -50,8 +50,7 @@ public: * * @since 0.1.0 */ - explicit Instance(const string_view hostname, - const string_view access_token); + explicit Instance(string_view hostname, string_view access_token); /*! * @brief Returns the hostname. diff --git a/src/api.cpp b/src/api.cpp index d0ffefa..cb60a8b 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -23,6 +23,8 @@ API::API(const endpoint_type &endpoint) : _endpoint{endpoint} {} +// TODO: look for a better way. +// NOLINTNEXTLINE(cert-err58-cpp) const map API::_endpoint_map { {v1::apps, "/api/v1/apps"}, From 6b5936a4b62671c2f35b528077c4cb61032499b7 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 17:39:47 +0100 Subject: [PATCH 05/13] Replace append with +=. --- src/connection.cpp | 4 ++-- src/exceptions.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/connection.cpp b/src/connection.cpp index 9bc43f0..fad710a 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -33,8 +33,8 @@ answer_type Connection::get(const endpoint_variant &endpoint, { if (holds_alternative(endpoint)) { - return string(_baseuri).append( - API{std::get(endpoint)}.to_string_view()); + return string(_baseuri) + += API{std::get(endpoint)}.to_string_view(); } return string(std::get(endpoint)); }()}; diff --git a/src/exceptions.cpp b/src/exceptions.cpp index 7c3da1a..7dd0e0f 100644 --- a/src/exceptions.cpp +++ b/src/exceptions.cpp @@ -42,7 +42,7 @@ const char *CURLException::what() const noexcept + " - " + _message}; if (!_error_buffer.empty()) { - error_string.append(" [" + _error_buffer + "]"); + error_string += " [" + _error_buffer + "]"; } return error_string.c_str(); } From c93810e24186afffeaa980f6516e8b673362440e Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 17:51:53 +0100 Subject: [PATCH 06/13] Move URI building for GET requets to add_parameters_to_uri(). --- include/curl_wrapper.hpp | 10 +++++ src/curl_wrapper.cpp | 96 ++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index dcd40ec..c7bd86e 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -173,6 +173,16 @@ private: * @since 0.1.0 */ void setup_curl(); + + /*! + * @brief Add parameters to URI. + * + * @param uri Reference to the URI. + * @param parameters The parametermap. + * + * @since 0.1.0 + */ + void add_parameters_to_uri(string &uri, const parametermap ¶meters); }; } // namespace mastodonpp diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index 0b8b49c..cb047d9 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -84,50 +84,7 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) code = curl_easy_setopt(_connection, CURLOPT_HTTPGET, 1L); - for (const auto ¶m : parameters) - { - static constexpr array replace_in_uri - { - "id", "nickname", "nickname_or_id", - "hashtag", "permission_group" - }; - if (any_of(replace_in_uri.begin(), replace_in_uri.end(), - [¶m](const auto &s) { return s == param.first; })) - { - const auto pos{uri.find('<')}; - if (pos != string::npos) - { - uri.replace(pos, param.first.size() + 2, - get(param.second)); - } - continue; - } - static bool first{true}; - if (first) - { - uri += "?"; - first = false; - } - else - { - uri += "&"; - } - if (holds_alternative(param.second)) - { - ((uri += param.first) += "=") += get(param.second); - } - else - { - for (const auto &arg : get>(param.second)) - { - ((uri += param.first) += "[]=") += arg; - if (arg != *get>(param.second).rbegin()) - { - uri += "&"; - } - } - } - } + add_parameters_to_uri(uri, parameters); break; } @@ -266,4 +223,55 @@ void CURLWrapper::setup_curl() curl_easy_setopt(_connection, CURLOPT_MAXREDIRS, 10L); } +void CURLWrapper::add_parameters_to_uri(string &uri, + const parametermap ¶meters) +{ + // Replace with the value of parameter “id” and so on. + for (const auto ¶m : parameters) + { + static constexpr array replace_in_uri + { + "id", "nickname", "nickname_or_id", + "hashtag", "permission_group" + }; + if (any_of(replace_in_uri.begin(), replace_in_uri.end(), + [¶m](const auto &s) { return s == param.first; })) + { + const auto pos{uri.find('<')}; + if (pos != string::npos) + { + uri.replace(pos, param.first.size() + 2, + get(param.second)); + continue; + } + } + + static bool first{true}; + if (first) + { + uri += "?"; + first = false; + } + else + { + uri += "&"; + } + if (holds_alternative(param.second)) + { + ((uri += param.first) += "=") += get(param.second); + } + else + { + for (const auto &arg : get>(param.second)) + { + ((uri += param.first) += "[]=") += arg; + if (arg != *get>(param.second).rbegin()) + { + uri += "&"; + } + } + } + } +} + } // namespace mastodonpp From a724006854f7364dbf305f84ee440d9664bb6307 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 19:55:34 +0100 Subject: [PATCH 07/13] Make curl writer non-satatic and add static wrapper. We need to use the mutex, a class member, from within the writer function. --- include/curl_wrapper.hpp | 27 ++++++++++++++++++-- src/curl_wrapper.cpp | 53 ++++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index c7bd86e..ddc74fb 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -164,8 +164,31 @@ private: * * @since 0.1.0 */ - static int writer(char *data, size_t size, size_t nmemb, - string *writerData); + size_t writer_body(char *data, size_t size, size_t nmemb); + + /*! + * @brief Wrapper for curl, because it can only call static member + * functions. + * + * + * + * @since 0.1.0 + */ + static inline size_t writer_body_wrapper(char *data, size_t sz, + size_t nmemb, void *f) + { + return static_cast(f)->writer_body(data, sz, nmemb); + } + + //! @copydoc writer_body + size_t writer_header(char *data, size_t size, size_t nmemb); + + //! @copydoc writer_body_wrapper + static inline size_t writer_header_wrapper(char *data, size_t sz, + size_t nmemb, void *f) + { + return static_cast(f)->writer_header(data, sz, nmemb); + } /*! * @brief Setup libcurl connection. diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index cb047d9..b72e9e9 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -151,17 +151,28 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, return answer; } -int CURLWrapper::writer(char *data, size_t size, size_t nmemb, - string *writerData) +size_t CURLWrapper::writer_body(char *data, size_t size, size_t nmemb) { - if(writerData == nullptr) + if(data == nullptr) { return 0; } - writerData->append(data, size*nmemb); + _curl_buffer_body.append(data, size * nmemb); - return static_cast(size * nmemb); + return size * nmemb; +} + +size_t CURLWrapper::writer_header(char *data, size_t size, size_t nmemb) +{ + if(data == nullptr) + { + return 0; + } + + _curl_buffer_headers.append(data, size * nmemb); + + return size * nmemb; } void CURLWrapper::setup_curl() @@ -180,29 +191,39 @@ void CURLWrapper::setup_curl() } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_WRITEFUNCTION, writer); + code = curl_easy_setopt(_connection, CURLOPT_WRITEFUNCTION, + writer_body_wrapper); if (code != CURLE_OK) { - throw CURLException{code, "Failed to set writer", _curl_buffer_error}; - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_HEADERDATA, - &_curl_buffer_headers); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set header data", + throw CURLException{code, "Failed to set write function", _curl_buffer_error}; } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_WRITEDATA, &_curl_buffer_body); + code = curl_easy_setopt(_connection, CURLOPT_WRITEDATA, this); if (code != CURLE_OK) { throw CURLException{code, "Failed to set write data", _curl_buffer_error}; } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + code = curl_easy_setopt(_connection, CURLOPT_HEADERFUNCTION, + writer_header_wrapper); + if (code != CURLE_OK) + { + throw CURLException{code, "Failed to set header function", + _curl_buffer_error}; + } + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + code = curl_easy_setopt(_connection, CURLOPT_HEADERDATA, this); + if (code != CURLE_OK) + { + throw CURLException{code, "Failed to set header data", + _curl_buffer_error}; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) code = curl_easy_setopt(_connection, CURLOPT_USERAGENT, (string("mastorss/") += version).c_str()); From d2de78ff9e0ccfc39df8c7bdf2491773342eb11e Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 21:27:27 +0100 Subject: [PATCH 08/13] Add streaming support. --- README.adoc | 2 +- include/connection.hpp | 7 +++++ include/curl_wrapper.hpp | 63 +++++++++++++++++++++++++++++++++++++--- src/CMakeLists.txt | 2 +- src/connection.cpp | 10 +++++++ src/curl_wrapper.cpp | 40 +++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index 7b40e52..1177d2e 100644 --- a/README.adoc +++ b/README.adoc @@ -42,7 +42,7 @@ Have a look at the link:{uri-reference}[reference]. * Tested OS: Linux * C++ compiler (tested: link:{uri-gcc}[GCC] 7/8/9, link:{uri-lang}[clang] 6/7) * link:{uri-cmake}[CMake] (at least: 3.9) -* link:{uri-curl}[curl] (tested: 7.66 / 7.58) +* link:{uri-curl}[curl] (at least: 7.32) * Optional ** Documentation: link:{uri-doxygen}[Doxygen] (tested: 1.8) ** Tests: link:{uri-catch}[Catch] (tested: 2.5 / 1.2) diff --git a/include/connection.hpp b/include/connection.hpp index 5ffe969..4bcdfe9 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -106,6 +106,13 @@ public: */ void set_proxy(string_view proxy); + /*! + * @brief Get new stream contents and delete them. + * + * @since 0.1.0 + */ + string get_new_stream_contents(); + private: Instance &_instance; const string_view _baseuri; diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index ddc74fb..1ff1d07 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -22,6 +22,7 @@ #include "curl/curl.h" #include +#include #include #include #include @@ -31,6 +32,7 @@ namespace mastodonpp { using std::map; +using std::mutex; using std::string; using std::string_view; using std::variant; @@ -91,10 +93,10 @@ public: CURLWrapper(); //! Copy constructor - CURLWrapper(const CURLWrapper &other) = default; + CURLWrapper(const CURLWrapper &other) = delete; //! Move constructor - CURLWrapper(CURLWrapper &&other) noexcept = default; + CURLWrapper(CURLWrapper &&other) noexcept = delete; /*! * @brief Cleans up curl and connection. @@ -108,10 +110,10 @@ public: virtual ~CURLWrapper() noexcept; //! Copy assignment operator - CURLWrapper& operator=(const CURLWrapper &other) = default; + CURLWrapper& operator=(const CURLWrapper &other) = delete; //! Move assignment operator - CURLWrapper& operator=(CURLWrapper &&other) noexcept = default; + CURLWrapper& operator=(CURLWrapper &&other) noexcept = delete; /*! * @brief Returns pointer to the CURL easy handle. @@ -139,7 +141,29 @@ public: */ void set_proxy(string_view proxy); + /*! + * @brief Cancel the stream. + * + * The stream will be cancelled, usually whithin a second. The @link + * answer_type::curl_error_code curl_error_code @endlink of the answer will + * be set to 42 (`CURLE_ABORTED_BY_CALLBACK`). + * + * @since 0.1.0 + */ + void cancel_stream(); + protected: + /*! + * @brief Mutex for #get_buffer a.k.a. _curl_buffer_body. + * + * This mutex is locked in `writer_body()` and + * Connection::get_new_stream_contents before anything is read or written + * from/to _curl_buffer_body. + * + * @since 0.1.0 + */ + mutex buffer_mutex; + /*! * @brief Make a HTTP request. * @@ -153,11 +177,23 @@ protected: answer_type make_request(const http_method &method, string uri, const parametermap ¶meters); + /*! + * @brief Returns a reference to the buffer libcurl writes into. + * + * @since 0.1.0 + */ + [[nodiscard]] + string &get_buffer() + { + return _curl_buffer_body; + } + private: CURL *_connection; char _curl_buffer_error[CURL_ERROR_SIZE]; string _curl_buffer_headers; string _curl_buffer_body; + bool _stream_cancelled; /*! * @brief libcurl write callback function. @@ -190,6 +226,25 @@ private: return static_cast(f)->writer_header(data, sz, nmemb); } + /*! + * @brief libcurl transfer info function. + * + * Used to cancel streams. + * + * @since 0.1.0 + */ + int progress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow); + + //! @copydoc writer_body_wrapper + static inline int progress_wrapper(void *f, void *clientp, + curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow) + { + return static_cast(f)->progress(clientp, dltotal, dlnow, + ultotal, ulnow); + } + /*! * @brief Setup libcurl connection. * diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cdb651b..b9ede8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,6 @@ include(GNUInstallDirs) -find_package(CURL REQUIRED) +find_package(CURL 7.32 REQUIRED) # Write version in header. configure_file ("version.hpp.in" diff --git a/src/connection.cpp b/src/connection.cpp index fad710a..3aaa236 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -48,4 +48,14 @@ void Connection::set_proxy(const string_view proxy) _instance.set_proxy(proxy); } +string Connection::get_new_stream_contents() +{ + buffer_mutex.lock(); + auto &buffer{get_buffer()}; + auto buffer_copy{buffer}; + buffer.clear(); + buffer_mutex.unlock(); + return buffer_copy; +} + } // namespace mastodonpp diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index b72e9e9..b100d21 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -40,6 +40,7 @@ static atomic curlwrapper_instances{0}; CURLWrapper::CURLWrapper() : _curl_buffer_error{} + , _stream_cancelled(false) { if (curlwrapper_instances == 0) { @@ -73,9 +74,16 @@ void CURLWrapper::set_proxy(const string_view proxy) } } +void CURLWrapper::cancel_stream() +{ + _stream_cancelled = true; +} + answer_type CURLWrapper::make_request(const http_method &method, string uri, const parametermap ¶meters) { + _stream_cancelled = false; + CURLcode code; switch (method) { @@ -158,7 +166,9 @@ size_t CURLWrapper::writer_body(char *data, size_t size, size_t nmemb) return 0; } + buffer_mutex.lock(); _curl_buffer_body.append(data, size * nmemb); + buffer_mutex.unlock(); return size * nmemb; } @@ -175,6 +185,16 @@ size_t CURLWrapper::writer_header(char *data, size_t size, size_t nmemb) return size * nmemb; } +int CURLWrapper::progress(void *, curl_off_t , curl_off_t , + curl_off_t , curl_off_t ) +{ + if (_stream_cancelled) + { + return 1; + } + return 0; +} + void CURLWrapper::setup_curl() { if (_connection == nullptr) @@ -224,6 +244,26 @@ void CURLWrapper::setup_curl() _curl_buffer_error}; } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + code = curl_easy_setopt(_connection, CURLOPT_XFERINFOFUNCTION, + progress_wrapper); + if (code != CURLE_OK) + { + throw CURLException{code, "Failed to set transfer info function", + _curl_buffer_error}; + } + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + curl_easy_setopt(_connection, CURLOPT_XFERINFODATA, this); + if (code != CURLE_OK) + { + throw CURLException{code, "Failed to set transfer info data", + _curl_buffer_error}; + } + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + curl_easy_setopt(_connection, CURLOPT_NOPROGRESS, 0L); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) code = curl_easy_setopt(_connection, CURLOPT_USERAGENT, (string("mastorss/") += version).c_str()); From 0e3b812b5467ffdaba1a7da26a4c1969fd358866 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 21:34:57 +0100 Subject: [PATCH 09/13] Only check for libcurl return code if it could return an error. --- src/curl_wrapper.cpp | 63 ++++++++------------------------------------ 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index b100d21..cc812c1 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -203,70 +203,29 @@ void CURLWrapper::setup_curl() } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - CURLcode code{curl_easy_setopt(_connection, CURLOPT_ERRORBUFFER, - _curl_buffer_error)}; - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set error buffer."}; - } + curl_easy_setopt(_connection, CURLOPT_ERRORBUFFER, _curl_buffer_error); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_WRITEFUNCTION, - writer_body_wrapper); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set write function", - _curl_buffer_error}; - } + curl_easy_setopt(_connection, CURLOPT_WRITEFUNCTION, writer_body_wrapper); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + curl_easy_setopt(_connection, CURLOPT_WRITEDATA, this); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_WRITEDATA, this); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set write data", - _curl_buffer_error}; - } + curl_easy_setopt(_connection, CURLOPT_HEADERFUNCTION, + writer_header_wrapper); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + curl_easy_setopt(_connection, CURLOPT_HEADERDATA, this); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_HEADERFUNCTION, - writer_header_wrapper); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set header function", - _curl_buffer_error}; - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_HEADERDATA, this); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set header data", - _curl_buffer_error}; - } - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_XFERINFOFUNCTION, - progress_wrapper); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set transfer info function", - _curl_buffer_error}; - } - + curl_easy_setopt(_connection, CURLOPT_XFERINFOFUNCTION, progress_wrapper); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) curl_easy_setopt(_connection, CURLOPT_XFERINFODATA, this); - if (code != CURLE_OK) - { - throw CURLException{code, "Failed to set transfer info data", - _curl_buffer_error}; - } - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) curl_easy_setopt(_connection, CURLOPT_NOPROGRESS, 0L); // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_USERAGENT, - (string("mastorss/") += version).c_str()); + CURLcode code{curl_easy_setopt(_connection, CURLOPT_USERAGENT, + (string("mastorss/") += version).c_str())}; if (code != CURLE_OK) { throw CURLException{code, "Failed to set User-Agent", From 7edcb7c507cb378bbc076982b47f23e737d6b1a9 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 21:49:48 +0100 Subject: [PATCH 10/13] Add streaming example. --- examples/CMakeLists.txt | 4 +- examples/example02_streaming.cpp | 102 +++++++++++++++++++++++++++++++ src/curl_wrapper.cpp | 3 +- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 examples/example02_streaming.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4be6be0..53c571d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,10 +1,12 @@ include(GNUInstallDirs) +find_package(Threads REQUIRED) + file(GLOB sources_examples *.cpp) foreach(src ${sources_examples}) get_filename_component(bin ${src} NAME_WE) add_executable(${bin} ${src}) - target_link_libraries(${bin} PRIVATE ${PROJECT_NAME}) + target_link_libraries(${bin} PRIVATE ${PROJECT_NAME} Threads::Threads) endforeach() if(WITH_DOC) diff --git a/examples/example02_streaming.cpp b/examples/example02_streaming.cpp new file mode 100644 index 0000000..0f95343 --- /dev/null +++ b/examples/example02_streaming.cpp @@ -0,0 +1,102 @@ +/* This file is part of mastodonpp. + * Copyright © 2020 tastytea + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Print information about an instance (/api/v1/instance). + +#include "mastodonpp.hpp" + +#include +#include +#include +#include +#include +#include + +namespace masto = mastodonpp; +using namespace std::chrono_literals; +using std::cout; +using std::cerr; +using std::endl; +using std::to_string; +using std::string_view; +using std::thread; +using std::this_thread::sleep_for; +using std::vector; + +int main(int argc, char *argv[]) +{ + const vector args(argv, argv + argc); + if (args.size() <= 1) + { + cerr << "Usage: " << args[0] << " \n"; + return 1; + } + + try + { + // Initialize an Instance. + masto::Instance instance{args[1], {}}; + + // Initialize a Connection. + masto::Connection connection{instance}; + + // Find out if the streaming service is fine. + auto answer{connection.get(masto::API::v1::streaming_health)}; + if (answer && answer.body == "OK") + { + // Make a thread, get all public events. + thread stream_thread{[&] + { + answer = connection.get(masto::API::v1::streaming_public); + }}; + + // Print new events every 2 seconds, for 10 seconds. + for (auto counter{0}; counter < 5; ++counter) + { + cout << "----------------------------------------" << endl; + sleep_for(2s); + cout << connection.get_new_stream_contents() << endl; + } + + // Cancel the stream, … + connection.cancel_stream(); + // … and get the rest of the data. + cout << connection.get_new_stream_contents() << endl; + stream_thread.join(); + } + else + { + if (answer.curl_error_code == 0) + { + // If it is no libcurl error, it must be an HTTP error. + cerr << "HTTP status: " << answer.http_status << endl; + } + else + { + // Network errors like “Couldn't resolve host.”. + cerr << "libcurl error " << to_string(answer.curl_error_code) + << ": " << answer.error_message << endl; + } + } + } + catch (const masto::CURLException &e) + { + // Only libcurl errors that are not network errors will be thrown. + // There went probably something wrong with the initialization. + cerr << e.what() << endl; + } + + return 0; +} diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index cc812c1..490af70 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -137,7 +137,8 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, answer_type answer; code = curl_easy_perform(_connection); - if (code == CURLE_OK) + if (code == CURLE_OK + || (code == CURLE_ABORTED_BY_CALLBACK && _stream_cancelled)) { long http_status; // NOLINT(google-runtime-int) // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) From f401f9d1b2375504c866b1ab23819f02c9a93191 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 21:56:54 +0100 Subject: [PATCH 11/13] Improve documentation for new_stream_contents(). --- include/connection.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/connection.hpp b/include/connection.hpp index 4bcdfe9..e2735b8 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -107,7 +107,11 @@ public: void set_proxy(string_view proxy); /*! - * @brief Get new stream contents and delete them. + * @brief Copy new stream contents and delete the “original”. + * + * Note that the last event is not necessarily complete, it could happen + * that you are calling this function mid-transfer. You have to check the + * data integrity yourself. * * @since 0.1.0 */ From 1f9ddf353c54d85a34b8be4e47804b32afafcad8 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 22:29:01 +0100 Subject: [PATCH 12/13] Add streaming example to reference. --- include/mastodonpp.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/mastodonpp.hpp b/include/mastodonpp.hpp index eecac21..fec3ecf 100644 --- a/include/mastodonpp.hpp +++ b/include/mastodonpp.hpp @@ -89,6 +89,7 @@ * are not thread safe. * * @example example01_instance_info.cpp + * @example example02_streaming.cpp */ /*! From e9b0ceb4c924dce9be3db82e8df5e19fb7409347 Mon Sep 17 00:00:00 2001 From: tastytea Date: Wed, 8 Jan 2020 22:29:31 +0100 Subject: [PATCH 13/13] Add introduction and an example to readme. --- README.adoc | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 1177d2e..c5a856d 100644 --- a/README.adoc +++ b/README.adoc @@ -15,13 +15,55 @@ :uri-curl: https://curl.haxx.se/ *{project}* is a C++ wrapper for the Mastodon API. It replaces - link:{uri-mastodon-cpp}[mastodon-cpp]. +link:{uri-mastodon-cpp}[mastodon-cpp]. + +We aim to create a library that is comfortable, yet minimal. All API endpoints +from Mastodon and Pleroma are stored in `enum class`es, to counteract typos and +make your life easier. The network-facing code is built on +link:{uri-curl}[libcurl], a mature and stable library that is available on +virtually every operating system. The library does not parse the responses +itself, but returns to you the raw data, because we know everyone has their +favorite JSON library and we don't want to impose our choice on you! + +== Features + +This is still a work in progress; here is a rough overview of the features: + +* [ ] Requests + ** [x] `GET` requests. + ** [x] Streaming `GET` requests. + ** [ ] `POST` requests. + ** [ ] `PATCH` requests. + ** [ ] `PUT` requests. + ** [ ] `DELETE` requests. +* [x] Report maximum allowed character per post. +* [ ] Comfortable access to pagination headers. +* [ ] Comfortable function to register a new “app” (get an access token). == Usage Have a look at the link:{uri-reference}[reference]. -// === Examples +=== Example + +[source,cpp] +-------------------------------------------------------------------------------- +#include "mastodonpp.hpp" +#include + +int main() +{ + mastodonpp::Instance instance{"example.com", {}}; + mastodonpp::Connection connection{instance}; + auto answer{connection.get(mastodonpp::API::v1::instance)}; + if (answer) + { + std::cout << answer << std::endl; + } +} +-------------------------------------------------------------------------------- + +link:{uri-reference}/examples.html[More examples] are included in the reference. == Install