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] Report maximum allowed character per post.
* [ ] Comfortable access to pagination headers.
* [ ] Comfortable function to register a new “app” (get an access token).
* [ ] Report which mime types are allowed for posting statuses.
* [x] Comfortable access to pagination headers.
* [x] Comfortable function to register a new “app” (get an access token).
* [x] Report which mime types are allowed for posting statuses.
== Usage

View File

@ -38,7 +38,7 @@ if(WITH_RPM)
set(CPACK_GENERATOR "RPM")
set(CPACK_RPM_PACKAGE_LICENSE "AGPL-3")
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
"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake"
"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig")

View File

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

View File

@ -17,28 +17,20 @@
#ifndef MASTODONPP_CURL_WRAPPER_HPP
#define MASTODONPP_CURL_WRAPPER_HPP
#include "answer.hpp"
#include "types.hpp"
#include "curl/curl.h"
#include <map>
#include <mutex>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
namespace mastodonpp
{
using std::map;
using std::mutex;
using std::string;
using std::string_view;
using std::pair;
using std::variant;
using std::vector;
/*!
* @brief The HTTP method.
@ -54,36 +46,6 @@ enum class http_method
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.
*

View File

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

View File

@ -17,11 +17,11 @@
#ifndef MASTODONPP_HPP
#define MASTODONPP_HPP
#include "answer.hpp"
#include "api.hpp"
#include "connection.hpp"
#include "exceptions.hpp"
#include "instance.hpp"
#include "types.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/>.
*/
#ifndef MASTODONPP_ANSWER_HPP
#define MASTODONPP_ANSWER_HPP
// Types that are used in more than one file.
#ifndef MASTODONPP_TYPES_HPP
#define MASTODONPP_TYPES_HPP
#include <cstdint>
#include <map>
#include <ostream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
namespace mastodonpp
{
using std::uint8_t;
using std::uint16_t;
using std::map;
using std::ostream;
using std::string;
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.
@ -110,9 +150,48 @@ struct answer_type
*
* @since 0.1.0
*/
[[nodiscard]]
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
#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_following, "/api/v1/accounts/<ID>/following"},
{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_unfollow, "/api/v1/accounts/<ID>/unfollow"},
{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::domain_blocks, "/api/v1/domain/blocks"},
{v1::domain_blocks, "/api/v1/domain_blocks"},
{v1::filters, "/api/v1/filters"},
{v1::filters_id, "/api/v1/filters/<ID>"},
{v1::reports, "/api/v1/reports"},
{v1::follow_requests, "/api/v1/follow/requests"},
{v1::follow_requests, "/api/v1/follow_requests"},
{v1::follow_requests_id_authorize,
"/api/v1/follow/requests/<ID>/authorize"},
{v1::follow_requests_id_reject, "/api/v1/follow/requests/<ID>/reject"},
"/api/v1/follow_requests/<ID>/authorize"},
{v1::follow_requests_id_reject, "/api/v1/follow_requests/<ID>/reject"},
{v1::endorsements, "/api/v1/endorsements"},
{v1::featured_tags, "/api/v1/featured/tags"},
{v1::featured_tags_id, "/api/v1/featured/tags/<ID>"},
{v1::featured_tags_suggestions, "/api/v1/featured/tags/suggestions"},
{v1::featured_tags_id, "/api/v1/featured_tags/<ID>"},
{v1::featured_tags_suggestions, "/api/v1/featured_tags/suggestions"},
{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_id, "/api/v1/statuses/<ID>"},
{v1::statuses_id_context, "/api/v1/statuses/<ID>/context"},
{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_reblogged_by, "/api/v1/statuses/<ID>/reblogged_by"},
{v1::statuses_id_favourited_by, "/api/v1/statuses/<ID>/favourited_by"},
{v1::statuses_id_favourite, "/api/v1/statuses/<ID>/favourite"},
{v1::statuses_id_unfavourite, "/api/v1/statuses/<ID>/unfavourite"},
{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_votes, "/api/v1/polls/<ID>/votes"},
{v1::scheduled_statuses, "/api/v1/scheduled/statuses"},
{v1::scheduled_statuses_id, "/api/v1/scheduled/statuses/<ID>"},
{v1::scheduled_statuses, "/api/v1/scheduled_statuses"},
{v1::scheduled_statuses_id, "/api/v1/scheduled_statuses/<ID>"},
{v1::timelines_public, "/api/v1/timelines/public"},
{v1::timelines_tag_hashtag, "/api/v1/timelines/tag/<HASHTAG>"},
{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_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_id, "/api/v1/admin/accounts/<ID>"},
{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_reject, "/api/v1/admin/accounts/<ID>/reject"},
{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_id, "/api/v1/admin/reports/<ID>"},
{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_resolve, "/api/v1/admin/reports/<ID>/resolve"},
{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
{
"id", "nickname", "nickname_or_id",
"hashtag", "permission_group"
"id", "nickname", "nickname_or_id", "account_id",
"list_id", "hashtag", "permission_group"
};
if (any_of(replace.begin(), replace.end(),
[&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/>.
*/
#include "answer.hpp"
#include "instance.hpp"
#include "log.hpp"
@ -54,19 +53,16 @@ uint64_t Instance::get_max_chars() noexcept
_max_chars = [&answer]
{
auto &body{answer.body};
size_t pos_start{body.find("max_toot_chars")};
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 regex re_chars{R"("max_toot_chars"\s*:\s*([^"]+))"};
smatch match;
const auto max_toot_chars{body.substr(pos_start,
pos_end - pos_start)};
return static_cast<uint64_t>(stoull(max_toot_chars));
if (regex_search(answer.body, match, re_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';
}
@ -81,7 +77,6 @@ uint64_t Instance::get_max_chars() noexcept
answer_type Instance::get_nodeinfo()
{
debuglog << "Finding location of NodeInfo on " << _hostname << "\n";
auto answer{make_request(http_method::GET,
_baseuri + "/.well-known/nodeinfo", {})};
if (!answer)
@ -90,15 +85,15 @@ answer_type Instance::get_nodeinfo()
return answer;
}
size_t pos{0};
vector<string> hrefs;
constexpr string_view searchstring{R"("href":")"};
while ((pos = answer.body.find(searchstring, pos)) != string::npos)
const regex re_href{R"("href"\s*:\s*"([^"]+)\")"};
smatch match;
string body = answer.body;
while (regex_search(body, match, re_href))
{
pos += searchstring.size();
auto endpos{answer.body.find('"', pos)};
hrefs.push_back(answer.body.substr(pos, endpos - pos));
hrefs.push_back(match[1].str());
debuglog << "Found href: " << hrefs.back() << '\n';
body = match.suffix();
}
sort(hrefs.begin(), hrefs.end()); // We assume they are sortable strings.
debuglog << "Selecting href: " << hrefs.back() << '\n';
@ -126,23 +121,23 @@ vector<string> Instance::get_post_formats() noexcept
return _post_formats;
}
constexpr string_view searchstring{R"("postFormats":[)"};
auto pos{answer.body.find(searchstring)};
if (pos == string::npos)
const regex re_allformats(R"("postFormats"\s*:\s*\[([^\]]+)\])");
smatch match;
if (!regex_search(answer.body, match, re_allformats))
{
debuglog << "Couldn't find metadata.postFormats.\n";
_post_formats = {default_value};
return _post_formats;
}
pos += searchstring.size();
auto endpos{answer.body.find("],", pos)};
string formats{answer.body.substr(pos, endpos - pos)};
debuglog << "Extracted postFormats: " << formats << '\n';
string allformats{match[1].str()};
debuglog << "Found postFormats: " << allformats << '\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));
formats.erase(0, pos + 2); // 2 is the length of: ",
_post_formats.push_back(match[1].str());
allformats = match.suffix();
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/>.
*/
#include "answer.hpp"
#include "log.hpp"
#include "types.hpp"
#include <algorithm>
#include <cctype>
@ -60,4 +61,40 @@ string_view answer_type::get_header(const string_view field) const
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