Merge branch 'develop' into main

This commit is contained in:
tastytea 2020-01-10 19:10:09 +01:00
commit e134745dd4
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
14 changed files with 392 additions and 86 deletions

View File

@ -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[]

View File

@ -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")

View File

@ -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

View File

@ -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<string_view>{"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 &parameters);
/*!
* @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<event_type> 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

View File

@ -25,6 +25,7 @@
#include <mutex>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
@ -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<string_view, variant<string_view, vector<string_view>>>;
using parametermap =
map<string_view, variant<string_view, vector<string_view>>>;
/*!
* @brief A single parameter of a parametermap.
*
* @since 0.1.0
*/
using parameterpair =
pair<string_view, variant<string_view, vector<string_view>>>;
/*!
* @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 &parameter);
/*!
* @brief Add parameters to URI.
*
@ -261,6 +293,23 @@ private:
* @since 0.1.0
*/
void add_parameters_to_uri(string &uri, const parametermap &parameters);
/*!
* @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 &parameters);
};
} // namespace mastodonpp

View File

@ -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.
*

View File

@ -22,6 +22,7 @@
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
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;

View File

@ -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}

View File

@ -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"

View File

@ -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<API::endpoint_type>(endpoint))
{
return string(_baseuri)
+= API{std::get<API::endpoint_type>(endpoint)}.to_string_view();
}
return string(_baseuri) += std::get<string_view>(endpoint);
}
answer_type Connection::get(const endpoint_variant &endpoint,
const parametermap &parameters)
{
const string uri{[&]
{
if (holds_alternative<API::endpoint_type>(endpoint))
{
return string(_baseuri)
+= API{std::get<API::endpoint_type>(endpoint)}.to_string_view();
}
return string(_baseuri) += std::get<string_view>(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 &parameters)
{
return make_request(http_method::POST,
endpoint_to_uri(endpoint), parameters);
}
string Connection::get_new_stream_contents()

View File

@ -23,7 +23,6 @@
#include <array>
#include <atomic>
#include <cstdint>
#include <cstring>
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 &parameters)
{
@ -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 &parameter)
{
static constexpr array replace
{
"id", "nickname", "nickname_or_id",
"hashtag", "permission_group"
};
if (any_of(replace.begin(), replace.end(),
[&parameter](const auto &s) { return s == parameter.first; }))
{
const auto pos{uri.find('<')};
if (pos != string::npos)
{
uri.replace(pos, parameter.first.size() + 2,
get<string_view>(parameter.second));
return true;
}
}
return false;
}
void CURLWrapper::add_parameters_to_uri(string &uri,
const parametermap &parameters)
{
// Replace <ID> with the value of parameter “id” and so on.
for (const auto &param : 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(),
[&param](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<string_view>(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 &parameters)
{
debuglog << "Building HTTP form.\n";
curl_mime *mime{curl_mime_init(_connection)};
for (const auto &param : parameters)
{
if (replace_parameter_in_uri(uri, param))
{
continue;
}
CURLcode code;
if (holds_alternative<string_view>(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<string_view>(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<string_view>(param.second) << '\n';
}
else
{
for (const auto &arg : get<vector<string_view>>(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

View File

@ -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 + "]";

View File

@ -20,39 +20,20 @@
#include <catch.hpp>
#include <exception>
#include <string>
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)

82
tests/test_instance.cpp Normal file
View File

@ -0,0 +1,82 @@
/* This file is part of mastodonpp.
* Copyright © 2020 tastytea <tastytea@tastytea.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "instance.hpp"
#include <catch.hpp>
#include <exception>
#include <string>
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