diff --git a/README.adoc b/README.adoc index cadfe1a..37a157b 100644 --- a/README.adoc +++ b/README.adoc @@ -32,7 +32,7 @@ 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. + ** [x] `POST` requests. ** [ ] `PATCH` requests. ** [ ] `PUT` requests. ** [ ] `DELETE` requests. @@ -84,7 +84,7 @@ link:{uri-reference}/examples.html[More examples] are included in the 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-libcurl}[libcurl] (at least: 7.32) +* link:{uri-libcurl}[libcurl] (at least: 7.56) * Optional ** Documentation: link:{uri-doxygen}[Doxygen] (tested: 1.8) ** Tests: link:{uri-catch}[Catch] (tested: 2.5 / 1.2) @@ -117,8 +117,8 @@ cmake --build . -- -j$(nproc --ignore=1) * `-DCMAKE_BUILD_TYPE=Debug` for a debug build. * `-DWITH_TESTS=YES` if you want to compile the tests. * `-DWITH_EXAMPLES=YES` if you want to compile the examples. -// * One of: -// ** `-DWITH_DEB=YES` if you want to be able to generate a deb-package. -// ** `-DWITH_RPM=YES` if you want to be able to generate an rpm-package. +* One of: + ** `-DWITH_DEB=YES` if you want to be able to generate a deb-package. + ** `-DWITH_RPM=YES` if you want to be able to generate an rpm-package. include::{uri-base}/raw/branch/main/CONTRIBUTING.adoc[] diff --git a/cmake/mastodonppConfig.cmake.in b/cmake/mastodonppConfig.cmake.in index 8c1180a..e703c6a 100644 --- a/cmake/mastodonppConfig.cmake.in +++ b/cmake/mastodonppConfig.cmake.in @@ -1,5 +1,5 @@ include(CMakeFindDependencyMacro) -find_dependency(CURL REQUIRED) +find_dependency(CURL 7.56 REQUIRED) include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") diff --git a/examples/example02_streaming.cpp b/examples/example02_streaming.cpp index 389b16b..1ac1b8f 100644 --- a/examples/example02_streaming.cpp +++ b/examples/example02_streaming.cpp @@ -68,7 +68,7 @@ int main(int argc, char *argv[]) sleep_for(2s); for (const auto &event : connection.get_new_events()) { - // Print typo of event and the beginning of the data. + // Print type of event and the beginning of the data. cout << event.type << ": " << event.data.substr(0, 70) << " …" << endl; } @@ -76,8 +76,7 @@ int main(int argc, char *argv[]) // Cancel the stream, … connection.cancel_stream(); - // … and get the rest of the data. - cout << connection.get_new_stream_contents() << endl; + // … and wait for the thread. stream_thread.join(); } else diff --git a/include/connection.hpp b/include/connection.hpp index 6885931..0ed09eb 100644 --- a/include/connection.hpp +++ b/include/connection.hpp @@ -124,6 +124,43 @@ public: return get(endpoint, {}); } + /*! + * @brief Make a HTTP POST call with parameters. + * + * Example: + * @code + * auto answer{connection.post( + * mastodonpp::API::v1::statuses, + * { + * {"status", "How is the wheather?"}, + * {"poll[options]", vector{"Nice", "not nice"}}, + * {"poll[expires_in]", to_string(poll_seconds)} + * })}; + * @endcode + * + * @param endpoint Endpoint as API::endpoint_type or `std::string_view`. + * @param parameters A map of parameters. + * + * + * @since 0.1.0 + */ + [[nodiscard]] + answer_type post(const endpoint_variant &endpoint, + const parametermap ¶meters); + + /*! + * @brief Make a HTTP POST call. + * + * @param endpoint Endpoint as API::endpoint_type or `std::string_view`. + * + * @since 0.1.0 + */ + [[nodiscard]] + inline answer_type post(const endpoint_variant &endpoint) + { + return post(endpoint, {}); + } + /*! * @brief Copy new stream contents and delete the “original”. * @@ -144,9 +181,17 @@ public: */ vector get_new_events(); + //! @copydoc CURLWrapper::cancel_stream + inline void cancel_stream() + { + CURLWrapper::cancel_stream(); + } private: Instance &_instance; const string_view _baseuri; + + [[nodiscard]] + string endpoint_to_uri(const endpoint_variant &endpoint) const; }; } // namespace mastodonpp diff --git a/include/curl_wrapper.hpp b/include/curl_wrapper.hpp index fc3bf3f..97df5b7 100644 --- a/include/curl_wrapper.hpp +++ b/include/curl_wrapper.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,7 @@ using std::map; using std::mutex; using std::string; using std::string_view; +using std::pair; using std::variant; using std::vector; @@ -53,7 +55,7 @@ enum class http_method }; /*! - * @brief std::map of parameters for %API calls. + * @brief `std::map` of parameters for %API calls. * * Example: * @code @@ -66,7 +68,16 @@ enum class http_method * * @since 0.1.0 */ -using parametermap = map>>; +using parametermap = + map>>; + +/*! + * @brief A single parameter of a parametermap. + * + * @since 0.1.0 + */ +using parameterpair = + pair>>; /*! * @brief Handles the details of network connections. @@ -141,24 +152,12 @@ 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. + * This mutex is locked before anything is read or written from/to + * _curl_buffer_body. * * @since 0.1.0 */ @@ -188,6 +187,27 @@ protected: return _curl_buffer_body; } + /*! + * @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 + */ + inline void cancel_stream() + { + _stream_cancelled = true; + } + + /*! + * @brief Set OAuth 2.0 Bearer Access Token. + * + * @since 0.1.0 + */ + void set_access_token(const string_view access_token); + private: CURL *_connection; char _curl_buffer_error[CURL_ERROR_SIZE]; @@ -252,6 +272,18 @@ private: */ void setup_curl(); + /*! + * @brief Replace parameter in URI. + * + * @param uri Reference to the URI. + * @param parameter The parameter. + * + * @return true if parameter was replaced. + * + * @since 0.1.0 + */ + bool replace_parameter_in_uri(string &uri, const parameterpair ¶meter); + /*! * @brief Add parameters to URI. * @@ -261,6 +293,23 @@ private: * @since 0.1.0 */ void add_parameters_to_uri(string &uri, const parametermap ¶meters); + + /*! + * @brief Convert parametermap to `*curl_mime`. + * + * For more information consult [curl_mime_init(3)] + * (https://curl.haxx.se/libcurl/c/curl_mime_init.html). Calls + * replace_parameter_in_uri(). + * + * @param uri Reference to the URI. + * @param parameters The parametermap. + * + * @return `*curl_mime`. + * + * @since 0.1.0 + */ + curl_mime *parameters_to_curl_mime(string &uri, + const parametermap ¶meters); }; } // namespace mastodonpp diff --git a/include/exceptions.hpp b/include/exceptions.hpp index 9e51d97..5cbad42 100644 --- a/include/exceptions.hpp +++ b/include/exceptions.hpp @@ -55,6 +55,13 @@ public: explicit CURLException(const CURLcode &error, string message, string error_buffer); + /*! + * @brief Constructor with message. + * + * @since 0.1.0 + */ + explicit CURLException(string message); + /*! * @brief The error code returned by libcurl. * diff --git a/include/instance.hpp b/include/instance.hpp index 5af177c..1aa76be 100644 --- a/include/instance.hpp +++ b/include/instance.hpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace mastodonpp { @@ -29,6 +30,7 @@ namespace mastodonpp using std::uint64_t; using std::string; using std::string_view; +using std::move; /*! * @brief Holds the access data of an instance. @@ -87,6 +89,19 @@ public: return _access_token; } + /*! + * @brief Set OAuth 2.0 Bearer Access Token. + * + * Sets also the access token for all Connection%s that are initialized + * with this Instance afterwards. + * + * @since 0.1.0 + */ + inline void set_access_token(string access_token) + { + _access_token = move(access_token); + } + /*! * @brief Returns the maximum number of characters per post. * @@ -113,6 +128,7 @@ public: * * @since 0.1.0 */ + [[nodiscard]] string_view get_proxy() const { return _proxy; diff --git a/pkg-config/mastodonpp.pc.in b/pkg-config/mastodonpp.pc.in index 49a4fde..d5aa215 100644 --- a/pkg-config/mastodonpp.pc.in +++ b/pkg-config/mastodonpp.pc.in @@ -1,10 +1,8 @@ -name=@PROJECT_NAME@ prefix=@CMAKE_INSTALL_PREFIX@ -exec_prefix=${prefix} libdir=${prefix}/@CMAKE_INSTALL_LIBDIR@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -Name: ${name} +Name: @PROJECT_NAME@ Description: @PROJECT_DESCRIPTION@ Version: @PROJECT_VERSION@ Cflags: -I${includedir} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b9ede8d..a7c16ef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,6 @@ include(GNUInstallDirs) -find_package(CURL 7.32 REQUIRED) +find_package(CURL 7.56 REQUIRED) # Write version in header. configure_file ("version.hpp.in" diff --git a/src/connection.cpp b/src/connection.cpp index 662c333..f74a816 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -30,22 +30,35 @@ Connection::Connection(Instance &instance) { CURLWrapper::set_proxy(proxy); } + + if (!_instance.get_access_token().empty()) + { + CURLWrapper::set_access_token(_instance.get_access_token()); + } +} + +string Connection::endpoint_to_uri(const endpoint_variant &endpoint) const +{ + if (holds_alternative(endpoint)) + { + return string(_baseuri) + += API{std::get(endpoint)}.to_string_view(); + } + return string(_baseuri) += std::get(endpoint); } answer_type Connection::get(const endpoint_variant &endpoint, const parametermap ¶meters) { - const string uri{[&] - { - if (holds_alternative(endpoint)) - { - return string(_baseuri) - += API{std::get(endpoint)}.to_string_view(); - } - return string(_baseuri) += std::get(endpoint); - }()}; + return make_request(http_method::GET, + endpoint_to_uri(endpoint), parameters); +} - return make_request(http_method::GET, uri, parameters); +answer_type Connection::post(const endpoint_variant &endpoint, + const parametermap ¶meters) +{ + return make_request(http_method::POST, + endpoint_to_uri(endpoint), parameters); } string Connection::get_new_stream_contents() diff --git a/src/curl_wrapper.cpp b/src/curl_wrapper.cpp index f675cca..91fb39e 100644 --- a/src/curl_wrapper.cpp +++ b/src/curl_wrapper.cpp @@ -23,7 +23,6 @@ #include #include #include -#include namespace mastodonpp { @@ -68,18 +67,13 @@ 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); + CURLcode code{curl_easy_setopt(_connection, CURLOPT_PROXY, proxy.data())}; if (code != CURLE_OK) { throw CURLException{code, "Failed to set proxy", _curl_buffer_error}; } } -void CURLWrapper::cancel_stream() -{ - _stream_cancelled = true; -} - answer_type CURLWrapper::make_request(const http_method &method, string uri, const parametermap ¶meters) { @@ -101,8 +95,18 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, } case http_method::POST: { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) - code = curl_easy_setopt(_connection, CURLOPT_POST, 1L); + if (parameters.empty()) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + code = curl_easy_setopt(_connection, CURLOPT_POST, 1L); + } + else + { + curl_mime *mime{parameters_to_curl_mime(uri, parameters)}; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + code = curl_easy_setopt(_connection, CURLOPT_MIMEPOST, mime); + } + break; } case http_method::PATCH: @@ -163,6 +167,32 @@ answer_type CURLWrapper::make_request(const http_method &method, string uri, return answer; } +void CURLWrapper::set_access_token(const string_view access_token) +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg, hicpp-signed-bitwise) + CURLcode code{curl_easy_setopt(_connection, CURLOPT_XOAUTH2_BEARER, + access_token.data())}; + if (code != CURLE_OK) + { + throw CURLException{code, "Could not set authorization token.", + _curl_buffer_error}; + } + +#if (LIBCURL_VERSION_NUM < 0x073d00) // libcurl < 7.61.0. +#define CURLAUTH_BEARER CURLAUTH_ANY +#endif + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg, hicpp-signed-bitwise) + code = curl_easy_setopt(_connection, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + if (code != CURLE_OK) + { + throw CURLException{code, "Could not set authorization token.", + _curl_buffer_error}; + } + + debuglog << "Set authorization token.\n"; +} + size_t CURLWrapper::writer_body(char *data, size_t size, size_t nmemb) { if(data == nullptr) @@ -229,7 +259,7 @@ void CURLWrapper::setup_curl() // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) CURLcode code{curl_easy_setopt(_connection, CURLOPT_USERAGENT, - (string("mastorss/") += version).c_str())}; + (string("mastodonpp/") += version).c_str())}; if (code != CURLE_OK) { throw CURLException{code, "Failed to set User-Agent", @@ -247,27 +277,38 @@ void CURLWrapper::setup_curl() curl_easy_setopt(_connection, CURLOPT_MAXREDIRS, 10L); } +bool CURLWrapper::replace_parameter_in_uri(string &uri, + const parameterpair ¶meter) +{ + static constexpr array replace + { + "id", "nickname", "nickname_or_id", + "hashtag", "permission_group" + }; + if (any_of(replace.begin(), replace.end(), + [¶meter](const auto &s) { return s == parameter.first; })) + { + const auto pos{uri.find('<')}; + if (pos != string::npos) + { + uri.replace(pos, parameter.first.size() + 2, + get(parameter.second)); + return true; + } + } + + return false; +} + 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; })) + if (replace_parameter_in_uri(uri, param)) { - const auto pos{uri.find('<')}; - if (pos != string::npos) - { - uri.replace(pos, param.first.size() + 2, - get(param.second)); - continue; - } + continue; } static bool first{true}; @@ -298,4 +339,70 @@ void CURLWrapper::add_parameters_to_uri(string &uri, } } +curl_mime *CURLWrapper::parameters_to_curl_mime(string &uri, + const parametermap ¶meters) +{ + debuglog << "Building HTTP form.\n"; + + curl_mime *mime{curl_mime_init(_connection)}; + for (const auto ¶m : parameters) + { + if (replace_parameter_in_uri(uri, param)) + { + continue; + } + + CURLcode code; + if (holds_alternative(param.second)) + { + curl_mimepart *part{curl_mime_addpart(mime)}; + if (part == nullptr) + { + throw CURLException{"Could not build HTTP form."}; + } + + code = curl_mime_name(part, param.first.data()); + if (code != CURLE_OK) + { + throw CURLException{code, "Could not build HTTP form."}; + } + + code = curl_mime_data(part, get(param.second).data(), + CURL_ZERO_TERMINATED); + if (code != CURLE_OK) + { + throw CURLException{code, "Could not build HTTP form."}; + } + debuglog << "Set form part: " << param.first << " = " + << get(param.second) << '\n'; + } + else + { + for (const auto &arg : get>(param.second)) + { + curl_mimepart *part{curl_mime_addpart(mime)}; + if (part == nullptr) + { + throw CURLException{"Could not build HTTP form."}; + } + + const string name{string(param.first) += "[]"}; + code = curl_mime_name(part, name.c_str()); + if (code != CURLE_OK) + { + throw CURLException{code, "Could not build HTTP form."}; + } + code = curl_mime_data(part, arg.data(), CURL_ZERO_TERMINATED); + if (code != CURLE_OK) + { + throw CURLException{code, "Could not build HTTP form."}; + } + debuglog << "Set form part: " << name << " = " << arg << '\n'; + } + } + } + + return mime; +} + } // namespace mastodonpp diff --git a/src/exceptions.cpp b/src/exceptions.cpp index 7dd0e0f..f4630a4 100644 --- a/src/exceptions.cpp +++ b/src/exceptions.cpp @@ -36,10 +36,19 @@ CURLException::CURLException(const CURLcode &error, string message, , _error_buffer{move(error_buffer)} {} +CURLException::CURLException(string message) + : error_code{CURLE_OK} + , _message{move(message)} +{} + const char *CURLException::what() const noexcept { - static string error_string{"libCURL error: " + to_string(error_code) - + " - " + _message}; + static string error_string{"libCURL error: "}; + if (error_code != CURLE_OK) + { + error_string += to_string(error_code) + " - "; + } + error_string += _message; if (!_error_buffer.empty()) { error_string += " [" + _error_buffer + "]"; diff --git a/tests/test_instanciation.cpp b/tests/test_connection.cpp similarity index 73% rename from tests/test_instanciation.cpp rename to tests/test_connection.cpp index bde806a..db8f223 100644 --- a/tests/test_instanciation.cpp +++ b/tests/test_connection.cpp @@ -20,39 +20,20 @@ #include #include -#include namespace mastodonpp { -using std::string; -SCENARIO ("Instantiations.") +SCENARIO ("mastodonpp::Connection.") { bool exception = false; - WHEN ("Instance is instantiated.") - { - try - { - Instance instance{"example.com", ""}; - } - catch (const std::exception &e) - { - exception = true; - } - - THEN ("No exception is thrown") - { - REQUIRE_FALSE(exception); - } - } - WHEN ("Connection is instantiated.") { try { - Instance instance{"example.com", ""}; + Instance instance{"example.com", {}}; Connection connection{instance}; } catch (const std::exception &e) diff --git a/tests/test_instance.cpp b/tests/test_instance.cpp new file mode 100644 index 0000000..8e38275 --- /dev/null +++ b/tests/test_instance.cpp @@ -0,0 +1,82 @@ +/* This file is part of mastodonpp. + * Copyright © 2020 tastytea + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#include "instance.hpp" + +#include + +#include +#include + +namespace mastodonpp +{ + +using std::string; + +SCENARIO ("mastopp::Instance") +{ + bool exception = false; + + WHEN ("Instance is instantiated.") + { + try + { + Instance instance{"example.com", {}}; + } + catch (const std::exception &e) + { + exception = true; + } + + THEN ("No exception is thrown") + { + REQUIRE_FALSE(exception); + } + } + + WHEN ("Variables are set.") + { + constexpr auto hostname{"likeable.space"}; + constexpr auto proxy{"socks4a://[::1]:9050"}; + constexpr auto access_token{"abc123"}; + Instance instance{hostname, {}}; + + try + { + instance.set_proxy(proxy); + instance.set_access_token(access_token); + } + catch (const std::exception &e) + { + exception = true; + } + + THEN ("No exception is thrown") + AND_THEN ("get_proxy() returns the set value.") + AND_THEN ("get_access_token() returns the set value.") + AND_THEN ("get_hostname() returns the set value.") + AND_THEN ("get_baseuri() returns the expected value.") + { + REQUIRE_FALSE(exception); + REQUIRE(instance.get_proxy() == proxy); + REQUIRE(instance.get_access_token() == access_token); + REQUIRE(instance.get_hostname() == hostname); + REQUIRE(instance.get_baseuri() == (string("https://") += hostname)); + } + } +} + +} // namespace mastodonpp