Merge branch 'develop' into main
continuous-integration/drone/push Build is passing Details

This commit is contained in:
tastytea 2020-01-08 22:42:47 +01:00
commit c9ef98353c
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
14 changed files with 433 additions and 106 deletions

View File

@ -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 <iostream>
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
@ -42,7 +84,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)

View File

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

View File

@ -0,0 +1,102 @@
/* This file is part of mastodonpp.
* Copyright © 2020 tastytea <tastytea@tastytea.de>
*
* 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 <chrono>
#include <iostream>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
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<string_view> args(argv, argv + argc);
if (args.size() <= 1)
{
cerr << "Usage: " << args[0] << " <instance hostname>\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;
}

View File

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

View File

@ -33,7 +33,12 @@ using std::string;
using std::string_view;
using std::variant;
using endpoint_variant = variant<API::endpoint_type,string>;
/*!
* @brief An endpoint. Either API::endpoint_type or `std::string_view`.
*
* @since 0.1.0
*/
using endpoint_variant = variant<API::endpoint_type,string_view>;
/*!
* @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.
*
*
@ -78,12 +83,13 @@ public:
/*!
* @brief Make a HTTP GET call.
*
* Example:
* @code
* 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
*/
@ -93,6 +99,24 @@ 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(string_view proxy);
/*!
* @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
*/
string get_new_stream_contents();
private:
Instance &_instance;
const string_view _baseuri;

View File

@ -22,6 +22,7 @@
#include "curl/curl.h"
#include <map>
#include <mutex>
#include <string>
#include <string_view>
#include <variant>
@ -31,6 +32,7 @@ namespace mastodonpp
{
using std::map;
using std::mutex;
using std::string;
using std::string_view;
using std::variant;
@ -58,13 +60,13 @@ enum class http_method
* parametermap parameters
* {
* {"id", "12"},
* {"poll[options]", vector<string>{"Yes", "No", "Maybe"}}
* {"poll[options]", vector<string_view>{"Yes", "No", "Maybe"}}
* };
* @endcode
*
* @since 0.1.0
*/
using parametermap = map<string, variant<string, vector<string>>>;
using parametermap = map<string_view, variant<string_view, vector<string_view>>>;
/*!
* @brief Handles the details of network connections.
@ -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.
@ -127,7 +129,41 @@ 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(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.
*
@ -141,19 +177,73 @@ protected:
answer_type make_request(const http_method &method, string uri,
const parametermap &parameters);
/*!
* @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.
*
* @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.
*
* <https://curl.haxx.se/docs/faq.html#Using_C_non_static_functions_f>
*
* @since 0.1.0
*/
static inline size_t writer_body_wrapper(char *data, size_t sz,
size_t nmemb, void *f)
{
return static_cast<CURLWrapper*>(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<CURLWrapper*>(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<CURLWrapper*>(f)->progress(clientp, dltotal, dlnow,
ultotal, ulnow);
}
/*!
* @brief Setup libcurl connection.
@ -161,6 +251,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 &parameters);
};
} // namespace mastodonpp

View File

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

View File

@ -74,21 +74,22 @@
* @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.
*
* @example example01_instance_info.cpp
* @example example02_streaming.cpp
*/
/*!

View File

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

View File

@ -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_type,string_view> API::_endpoint_map
{
{v1::apps, "/api/v1/apps"},

View File

@ -29,17 +29,33 @@ Connection::Connection(Instance &instance)
answer_type Connection::get(const endpoint_variant &endpoint,
const parametermap &parameters)
{
string uri{[&]
const string uri{[&]
{
if (holds_alternative<API::endpoint_type>(endpoint))
{
return string(_baseuri).append(
API{std::get<API::endpoint_type>(endpoint)}.to_string_view());
return string(_baseuri)
+= API{std::get<API::endpoint_type>(endpoint)}.to_string_view();
}
return std::get<string>(endpoint);
return string(std::get<string_view>(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);
}
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

View File

@ -40,6 +40,7 @@ static atomic<uint16_t> curlwrapper_instances{0};
CURLWrapper::CURLWrapper()
: _curl_buffer_error{}
, _stream_cancelled(false)
{
if (curlwrapper_instances == 0)
{
@ -63,9 +64,26 @@ 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};
}
}
void CURLWrapper::cancel_stream()
{
_stream_cancelled = true;
}
answer_type CURLWrapper::make_request(const http_method &method, string uri,
const parametermap &parameters)
{
_stream_cancelled = false;
CURLcode code;
switch (method)
{
@ -74,50 +92,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 &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; }))
{
const auto pos{uri.find('<')};
if (pos != string::npos)
{
uri.replace(pos, param.first.size() + 2,
get<string>(param.second));
}
continue;
}
static bool first{true};
if (first)
{
uri.append("?");
first = false;
}
else
{
uri.append("&");
}
if (holds_alternative<string>(param.second))
{
uri.append(param.first + '=' + get<string>(param.second));
}
else
{
for (const auto &arg : get<vector<string>>(param.second))
{
uri.append(param.first + "[]=" + arg);
if (arg != *get<vector<string>>(param.second).rbegin())
{
uri.append("&");
}
}
}
}
add_parameters_to_uri(uri, parameters);
break;
}
@ -162,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)
@ -184,17 +160,40 @@ 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);
buffer_mutex.lock();
_curl_buffer_body.append(data, size * nmemb);
buffer_mutex.unlock();
return static_cast<int>(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;
}
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()
@ -205,40 +204,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);
if (code != CURLE_OK)
{
throw CURLException{code, "Failed to set writer", _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_HEADERDATA,
&_curl_buffer_headers);
if (code != CURLE_OK)
{
throw CURLException{code, "Failed to set header 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_WRITEDATA, &_curl_buffer_body);
if (code != CURLE_OK)
{
throw CURLException{code, "Failed to set write data",
_curl_buffer_error};
}
curl_easy_setopt(_connection, CURLOPT_XFERINFOFUNCTION, progress_wrapper);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
curl_easy_setopt(_connection, CURLOPT_XFERINFODATA, this);
// 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/").append(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",
@ -256,4 +244,55 @@ void CURLWrapper::setup_curl()
curl_easy_setopt(_connection, CURLOPT_MAXREDIRS, 10L);
}
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; }))
{
const auto pos{uri.find('<')};
if (pos != string::npos)
{
uri.replace(pos, param.first.size() + 2,
get<string_view>(param.second));
continue;
}
}
static bool first{true};
if (first)
{
uri += "?";
first = false;
}
else
{
uri += "&";
}
if (holds_alternative<string_view>(param.second))
{
((uri += param.first) += "=") += get<string_view>(param.second);
}
else
{
for (const auto &arg : get<vector<string_view>>(param.second))
{
((uri += param.first) += "[]=") += arg;
if (arg != *get<vector<string_view>>(param.second).rbegin())
{
uri += "&";
}
}
}
}
}
} // namespace mastodonpp

View File

@ -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();
}

View File

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