Merge branch 'develop' into main

This commit is contained in:
tastytea 2020-01-14 22:53:44 +01:00
commit f2594d2c46
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
11 changed files with 169 additions and 96 deletions

View File

@ -31,9 +31,9 @@ This is still a work in progress; here is a rough overview of the features:
* [x] `GET`, Streaming `GET`, `POST`, `PATCH`, `PUT` and `DELETE` requests. * [x] `GET`, Streaming `GET`, `POST`, `PATCH`, `PUT` and `DELETE` requests.
* [x] Report maximum allowed character per post. * [x] Report maximum allowed character per post.
* [ ] Comfortable access to pagination headers. * [x] Comfortable access to pagination headers.
* [ ] Comfortable function to register a new “app” (get an access token). * [x] Comfortable function to register a new “app” (get an access token).
* [ ] Report which mime types are allowed for posting statuses. * [x] Report which mime types are allowed for posting statuses.
== Usage == Usage

View File

@ -38,7 +38,7 @@ if(WITH_RPM)
set(CPACK_GENERATOR "RPM") set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "AGPL-3") set(CPACK_RPM_PACKAGE_LICENSE "AGPL-3")
set(CPACK_RPM_PACKAGE_URL "https://schlomp.space/tastytea/${PROJECT_NAME}") set(CPACK_RPM_PACKAGE_URL "https://schlomp.space/tastytea/${PROJECT_NAME}")
set(CPACK_RPM_PACKAGE_REQUIRES "libcurl >= 7.32") set(CPACK_RPM_PACKAGE_REQUIRES "libcurl >= 7.56")
list(APPEND CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION list(APPEND CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION
"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake" "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake"
"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig") "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig")

View File

@ -17,10 +17,10 @@
#ifndef MASTODONPP_CONNECTION_HPP #ifndef MASTODONPP_CONNECTION_HPP
#define MASTODONPP_CONNECTION_HPP #define MASTODONPP_CONNECTION_HPP
#include "answer.hpp"
#include "api.hpp" #include "api.hpp"
#include "curl_wrapper.hpp" #include "curl_wrapper.hpp"
#include "instance.hpp" #include "instance.hpp"
#include "types.hpp"
#include <string> #include <string>
#include <string_view> #include <string_view>

View File

@ -17,28 +17,20 @@
#ifndef MASTODONPP_CURL_WRAPPER_HPP #ifndef MASTODONPP_CURL_WRAPPER_HPP
#define MASTODONPP_CURL_WRAPPER_HPP #define MASTODONPP_CURL_WRAPPER_HPP
#include "answer.hpp" #include "types.hpp"
#include "curl/curl.h" #include "curl/curl.h"
#include <map>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
#include <variant>
#include <vector>
namespace mastodonpp namespace mastodonpp
{ {
using std::map;
using std::mutex; using std::mutex;
using std::string; using std::string;
using std::string_view; using std::string_view;
using std::pair;
using std::variant;
using std::vector;
/*! /*!
* @brief The HTTP method. * @brief The HTTP method.
@ -54,36 +46,6 @@ enum class http_method
DELETE DELETE
}; };
/*!
* @brief `std::map` of parameters for %API calls.
*
* Note that arrays always have to be specified as vectors, even if they have
* only 1 element. To send a file, use <tt>\@file:</tt> followed by the file
* name as value.
*
* Example:
* @code
* parametermap parameters
* {
* {"poll[expires_in]", "86400"},
* {"poll[options]", vector<string_view>{"Yes", "No", "Maybe"}},
* {"status", "How is the weather?"}
* };
* @endcode
*
* @since 0.1.0
*/
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. * @brief Handles the details of network connections.
* *

View File

@ -18,7 +18,7 @@
#define MASTODONPP_INSTANCE_HPP #define MASTODONPP_INSTANCE_HPP
#include "curl_wrapper.hpp" #include "curl_wrapper.hpp"
#include "answer.hpp" #include "types.hpp"
#include <cstdint> #include <cstdint>
#include <string> #include <string>

View File

@ -17,11 +17,11 @@
#ifndef MASTODONPP_HPP #ifndef MASTODONPP_HPP
#define MASTODONPP_HPP #define MASTODONPP_HPP
#include "answer.hpp"
#include "api.hpp" #include "api.hpp"
#include "connection.hpp" #include "connection.hpp"
#include "exceptions.hpp" #include "exceptions.hpp"
#include "instance.hpp" #include "instance.hpp"
#include "types.hpp"
/*! /*!
* @headerfile mastodonpp.hpp mastodonpp/mastodonpp.hpp * @headerfile mastodonpp.hpp mastodonpp/mastodonpp.hpp

View File

@ -14,22 +14,62 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef MASTODONPP_ANSWER_HPP // Types that are used in more than one file.
#define MASTODONPP_ANSWER_HPP
#ifndef MASTODONPP_TYPES_HPP
#define MASTODONPP_TYPES_HPP
#include <cstdint> #include <cstdint>
#include <map>
#include <ostream> #include <ostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
#include <variant>
#include <vector>
namespace mastodonpp namespace mastodonpp
{ {
using std::uint8_t; using std::uint8_t;
using std::uint16_t; using std::uint16_t;
using std::map;
using std::ostream; using std::ostream;
using std::string; using std::string;
using std::string_view; using std::string_view;
using std::pair;
using std::variant;
using std::vector;
/*!
* @brief `std::map` of parameters for %API calls.
*
* Note that arrays always have to be specified as vectors, even if they have
* only 1 element. To send a file, use <tt>\@file:</tt> followed by the file
* name as value.
*
* Example:
* @code
* parametermap parameters
* {
* {"poll[expires_in]", "86400"},
* {"poll[options]", vector<string_view>{"Yes", "No", "Maybe"}},
* {"status", "How is the weather?"}
* };
* @endcode
*
* @since 0.1.0
*/
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 Return type for Request%s. * @brief Return type for Request%s.
@ -110,9 +150,48 @@ struct answer_type
* *
* @since 0.1.0 * @since 0.1.0
*/ */
[[nodiscard]]
string_view get_header(string_view field) const; string_view get_header(string_view field) const;
/*!
* @brief Returns the parameters needed for the next entries.
*
* Parses the `Link` header.
*
* @since 0.3.0
*/
[[nodiscard]]
inline parametermap next() const
{
return parse_pagination(true);
}
/*!
* @brief Returns the parameters needed for the previous entries.
*
*
* Parses the `Link` header.
*
* @since 0.3.0
*/
[[nodiscard]]
inline parametermap prev() const
{
return parse_pagination(false);
}
private:
/*!
* @brief Returns the parameters needed for the next or previous entries.
*
*
* Parses the `Link` header.
*
* @since 0.3.0
*/
parametermap parse_pagination(bool next) const;
}; };
} // namespace mastodonpp } // namespace mastodonpp
#endif // MASTODONPP_ANSWER_HPP #endif // MASTODONPP_TYPES_HPP

View File

@ -38,7 +38,7 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::accounts_id_followers, "/api/v1/accounts/<ID>/followers"}, {v1::accounts_id_followers, "/api/v1/accounts/<ID>/followers"},
{v1::accounts_id_following, "/api/v1/accounts/<ID>/following"}, {v1::accounts_id_following, "/api/v1/accounts/<ID>/following"},
{v1::accounts_id_lists, "/api/v1/accounts/<ID>/lists"}, {v1::accounts_id_lists, "/api/v1/accounts/<ID>/lists"},
{v1::accounts_id_identity_proofs, "/api/v1/accounts/<ID>/identity/proofs"}, {v1::accounts_id_identity_proofs, "/api/v1/accounts/<ID>/identity_proofs"},
{v1::accounts_id_follow, "/api/v1/accounts/<ID>/follow"}, {v1::accounts_id_follow, "/api/v1/accounts/<ID>/follow"},
{v1::accounts_id_unfollow, "/api/v1/accounts/<ID>/unfollow"}, {v1::accounts_id_unfollow, "/api/v1/accounts/<ID>/unfollow"},
{v1::accounts_id_block, "/api/v1/accounts/<ID>/block"}, {v1::accounts_id_block, "/api/v1/accounts/<ID>/block"},
@ -58,23 +58,23 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::blocks, "/api/v1/blocks"}, {v1::blocks, "/api/v1/blocks"},
{v1::domain_blocks, "/api/v1/domain/blocks"}, {v1::domain_blocks, "/api/v1/domain_blocks"},
{v1::filters, "/api/v1/filters"}, {v1::filters, "/api/v1/filters"},
{v1::filters_id, "/api/v1/filters/<ID>"}, {v1::filters_id, "/api/v1/filters/<ID>"},
{v1::reports, "/api/v1/reports"}, {v1::reports, "/api/v1/reports"},
{v1::follow_requests, "/api/v1/follow/requests"}, {v1::follow_requests, "/api/v1/follow_requests"},
{v1::follow_requests_id_authorize, {v1::follow_requests_id_authorize,
"/api/v1/follow/requests/<ID>/authorize"}, "/api/v1/follow_requests/<ID>/authorize"},
{v1::follow_requests_id_reject, "/api/v1/follow/requests/<ID>/reject"}, {v1::follow_requests_id_reject, "/api/v1/follow_requests/<ID>/reject"},
{v1::endorsements, "/api/v1/endorsements"}, {v1::endorsements, "/api/v1/endorsements"},
{v1::featured_tags, "/api/v1/featured/tags"}, {v1::featured_tags, "/api/v1/featured/tags"},
{v1::featured_tags_id, "/api/v1/featured/tags/<ID>"}, {v1::featured_tags_id, "/api/v1/featured_tags/<ID>"},
{v1::featured_tags_suggestions, "/api/v1/featured/tags/suggestions"}, {v1::featured_tags_suggestions, "/api/v1/featured_tags/suggestions"},
{v1::preferences, "/api/v1/preferences"}, {v1::preferences, "/api/v1/preferences"},
@ -84,8 +84,8 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::statuses, "/api/v1/statuses"}, {v1::statuses, "/api/v1/statuses"},
{v1::statuses_id, "/api/v1/statuses/<ID>"}, {v1::statuses_id, "/api/v1/statuses/<ID>"},
{v1::statuses_id_context, "/api/v1/statuses/<ID>/context"}, {v1::statuses_id_context, "/api/v1/statuses/<ID>/context"},
{v1::statuses_id_reblogged_by, "/api/v1/statuses/<ID>/reblogged/by"}, {v1::statuses_id_reblogged_by, "/api/v1/statuses/<ID>/reblogged_by"},
{v1::statuses_id_favourited_by, "/api/v1/statuses/<ID>/favourited/by"}, {v1::statuses_id_favourited_by, "/api/v1/statuses/<ID>/favourited_by"},
{v1::statuses_id_favourite, "/api/v1/statuses/<ID>/favourite"}, {v1::statuses_id_favourite, "/api/v1/statuses/<ID>/favourite"},
{v1::statuses_id_unfavourite, "/api/v1/statuses/<ID>/unfavourite"}, {v1::statuses_id_unfavourite, "/api/v1/statuses/<ID>/unfavourite"},
{v1::statuses_id_reblog, "/api/v1/statuses/<ID>/reblog"}, {v1::statuses_id_reblog, "/api/v1/statuses/<ID>/reblog"},
@ -103,13 +103,13 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::polls_id, "/api/v1/polls/<ID>"}, {v1::polls_id, "/api/v1/polls/<ID>"},
{v1::polls_id_votes, "/api/v1/polls/<ID>/votes"}, {v1::polls_id_votes, "/api/v1/polls/<ID>/votes"},
{v1::scheduled_statuses, "/api/v1/scheduled/statuses"}, {v1::scheduled_statuses, "/api/v1/scheduled_statuses"},
{v1::scheduled_statuses_id, "/api/v1/scheduled/statuses/<ID>"}, {v1::scheduled_statuses_id, "/api/v1/scheduled_statuses/<ID>"},
{v1::timelines_public, "/api/v1/timelines/public"}, {v1::timelines_public, "/api/v1/timelines/public"},
{v1::timelines_tag_hashtag, "/api/v1/timelines/tag/<HASHTAG>"}, {v1::timelines_tag_hashtag, "/api/v1/timelines/tag/<HASHTAG>"},
{v1::timelines_home, "/api/v1/timelines/home"}, {v1::timelines_home, "/api/v1/timelines/home"},
{v1::timelines_list_list_id, "/api/v1/timelines/list/list/<ID>"}, {v1::timelines_list_list_id, "/api/v1/timelines/list/<LIST_ID>"},
{v1::conversations, "/api/v1/conversations"}, {v1::conversations, "/api/v1/conversations"},
{v1::conversations_id, "/api/v1/conversations/<ID>"}, {v1::conversations_id, "/api/v1/conversations/<ID>"},
@ -150,7 +150,7 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::admin_accounts, "/api/v1/admin/accounts"}, {v1::admin_accounts, "/api/v1/admin/accounts"},
{v1::admin_accounts_id, "/api/v1/admin/accounts/<ID>"}, {v1::admin_accounts_id, "/api/v1/admin/accounts/<ID>"},
{v1::admin_accounts_account_id_action, {v1::admin_accounts_account_id_action,
"/api/v1/admin/accounts/account/<ID>/action"}, "/api/v1/admin/accounts/<ACCOUNT_ID>/action"},
{v1::admin_accounts_id_approve, "/api/v1/admin/accounts/<ID>/approve"}, {v1::admin_accounts_id_approve, "/api/v1/admin/accounts/<ID>/approve"},
{v1::admin_accounts_id_reject, "/api/v1/admin/accounts/<ID>/reject"}, {v1::admin_accounts_id_reject, "/api/v1/admin/accounts/<ID>/reject"},
{v1::admin_accounts_id_enable, "/api/v1/admin/accounts/<ID>/enable"}, {v1::admin_accounts_id_enable, "/api/v1/admin/accounts/<ID>/enable"},
@ -159,7 +159,7 @@ const map<API::endpoint_type,string_view> API::_endpoint_map
{v1::admin_reports, "/api/v1/admin/reports"}, {v1::admin_reports, "/api/v1/admin/reports"},
{v1::admin_reports_id, "/api/v1/admin/reports/<ID>"}, {v1::admin_reports_id, "/api/v1/admin/reports/<ID>"},
{v1::admin_reports_id_assign_to_self, {v1::admin_reports_id_assign_to_self,
"/api/v1/admin/reports/<ID>/assign/to/self"}, "/api/v1/admin/reports/<ID>/assign_to_self"},
{v1::admin_reports_id_unassign, "/api/v1/admin/reports/<ID>/unassign"}, {v1::admin_reports_id_unassign, "/api/v1/admin/reports/<ID>/unassign"},
{v1::admin_reports_id_resolve, "/api/v1/admin/reports/<ID>/resolve"}, {v1::admin_reports_id_resolve, "/api/v1/admin/reports/<ID>/resolve"},
{v1::admin_reports_id_reopen, "/api/v1/admin/reports/<ID>/reopen"}, {v1::admin_reports_id_reopen, "/api/v1/admin/reports/<ID>/reopen"},

View File

@ -353,8 +353,8 @@ bool CURLWrapper::replace_parameter_in_uri(string &uri,
{ {
static constexpr array replace static constexpr array replace
{ {
"id", "nickname", "nickname_or_id", "id", "nickname", "nickname_or_id", "account_id",
"hashtag", "permission_group" "list_id", "hashtag", "permission_group"
}; };
if (any_of(replace.begin(), replace.end(), if (any_of(replace.begin(), replace.end(),
[&parameter](const auto &s) { return s == parameter.first; })) [&parameter](const auto &s) { return s == parameter.first; }))

View File

@ -14,7 +14,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "answer.hpp"
#include "instance.hpp" #include "instance.hpp"
#include "log.hpp" #include "log.hpp"
@ -54,19 +53,16 @@ uint64_t Instance::get_max_chars() noexcept
_max_chars = [&answer] _max_chars = [&answer]
{ {
auto &body{answer.body}; const regex re_chars{R"("max_toot_chars"\s*:\s*([^"]+))"};
size_t pos_start{body.find("max_toot_chars")}; smatch match;
if (pos_start == string::npos)
{
debuglog << "max_toot_chars not found.\n";
return default_max_chars;
}
pos_start = body.find(':', pos_start) + 1;
const size_t pos_end{body.find(',', pos_start)};
const auto max_toot_chars{body.substr(pos_start, if (regex_search(answer.body, match, re_chars))
pos_end - pos_start)}; {
return static_cast<uint64_t>(stoull(max_toot_chars)); return static_cast<uint64_t>(stoull(match[1].str()));
}
debuglog << "max_toot_chars not found.\n";
return default_max_chars;
}(); }();
debuglog << "Set _max_chars to: " << _max_chars << '\n'; debuglog << "Set _max_chars to: " << _max_chars << '\n';
} }
@ -81,7 +77,6 @@ uint64_t Instance::get_max_chars() noexcept
answer_type Instance::get_nodeinfo() answer_type Instance::get_nodeinfo()
{ {
debuglog << "Finding location of NodeInfo on " << _hostname << "\n";
auto answer{make_request(http_method::GET, auto answer{make_request(http_method::GET,
_baseuri + "/.well-known/nodeinfo", {})}; _baseuri + "/.well-known/nodeinfo", {})};
if (!answer) if (!answer)
@ -90,15 +85,15 @@ answer_type Instance::get_nodeinfo()
return answer; return answer;
} }
size_t pos{0};
vector<string> hrefs; vector<string> hrefs;
constexpr string_view searchstring{R"("href":")"}; const regex re_href{R"("href"\s*:\s*"([^"]+)\")"};
while ((pos = answer.body.find(searchstring, pos)) != string::npos) smatch match;
string body = answer.body;
while (regex_search(body, match, re_href))
{ {
pos += searchstring.size(); hrefs.push_back(match[1].str());
auto endpos{answer.body.find('"', pos)};
hrefs.push_back(answer.body.substr(pos, endpos - pos));
debuglog << "Found href: " << hrefs.back() << '\n'; debuglog << "Found href: " << hrefs.back() << '\n';
body = match.suffix();
} }
sort(hrefs.begin(), hrefs.end()); // We assume they are sortable strings. sort(hrefs.begin(), hrefs.end()); // We assume they are sortable strings.
debuglog << "Selecting href: " << hrefs.back() << '\n'; debuglog << "Selecting href: " << hrefs.back() << '\n';
@ -126,23 +121,23 @@ vector<string> Instance::get_post_formats() noexcept
return _post_formats; return _post_formats;
} }
constexpr string_view searchstring{R"("postFormats":[)"}; const regex re_allformats(R"("postFormats"\s*:\s*\[([^\]]+)\])");
auto pos{answer.body.find(searchstring)}; smatch match;
if (pos == string::npos) if (!regex_search(answer.body, match, re_allformats))
{ {
debuglog << "Couldn't find metadata.postFormats.\n"; debuglog << "Couldn't find metadata.postFormats.\n";
_post_formats = {default_value}; _post_formats = {default_value};
return _post_formats; return _post_formats;
} }
pos += searchstring.size(); string allformats{match[1].str()};
auto endpos{answer.body.find("],", pos)}; debuglog << "Found postFormats: " << allformats << '\n';
string formats{answer.body.substr(pos, endpos - pos)};
debuglog << "Extracted postFormats: " << formats << '\n';
while ((pos = formats.find('"', 1)) != string::npos) const regex re_format(R"(\s*"([^"]+)\"\s*,?)");
while (regex_search(allformats, match, re_format))
{ {
_post_formats.push_back(formats.substr(1, pos - 1)); _post_formats.push_back(match[1].str());
formats.erase(0, pos + 2); // 2 is the length of: ", allformats = match.suffix();
debuglog << "Found postFormat: " << _post_formats.back() << '\n'; debuglog << "Found postFormat: " << _post_formats.back() << '\n';
} }
} }

View File

@ -14,7 +14,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "answer.hpp" #include "log.hpp"
#include "types.hpp"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
@ -60,4 +61,40 @@ string_view answer_type::get_header(const string_view field) const
return {}; return {};
} }
parametermap answer_type::parse_pagination(const bool next) const
{
const string_view link{get_header("Link")};
if (link.empty())
{
return {};
}
const auto direction{next ? R"(rel="next")" : R"(rel="prev")"};
auto endpos{link.find(direction)};
endpos = link.rfind('>', endpos);
auto startpos{link.rfind('?', endpos) + 1};
const string_view paramstr{link.substr(startpos, endpos - startpos)};
debuglog << "Found parameters in Link header: " << paramstr << '\n';
startpos = 0;
parametermap parameters;
while ((endpos = paramstr.find('=', startpos)) != string_view::npos)
{
parameterpair param;
param.first = paramstr.substr(startpos, endpos - startpos);
startpos = endpos + 1;
endpos = paramstr.find('&', startpos);
param.second = paramstr.substr(startpos, endpos - startpos);
parameters.insert(param);
if (endpos == string_view::npos)
{
break;
}
startpos = endpos + 1;
}
return parameters;
}
} // namespace mastodonpp } // namespace mastodonpp