diff options
-rw-r--r-- | include/instance.hpp | 78 | ||||
-rw-r--r-- | src/instance.cpp | 112 |
2 files changed, 178 insertions, 12 deletions
diff --git a/include/instance.hpp b/include/instance.hpp index 7f74c46..874ea6a 100644 --- a/include/instance.hpp +++ b/include/instance.hpp @@ -192,6 +192,84 @@ public: return _cainfo; } + /*! + * @brief Simplifies obtaining an OAuth 2.0 Bearer Access Token. + * + * * Create an Instance() and initialize this class with it. + * * Call step_1() to get the URI your user has to visit. + * * Get the authorization code from your user. + * * Call step_2() with the code. + * + * Example: + * @code + * mastodonpp::Instance instance("example.com", {}); + * mastodonpp::Instance::ObtainToken token(instance); + * auto answer{token.step1("Good program", "read:blocks read:mutes", "")}; + * if (answer) + * { + * std::cout << "Please visit " << answer << "\nand paste the code: "; + * std::string code; + * std::cin >> code; + * answer = access_token{token.step2(code)}; + * if (answer) + * { + * std::cout << "Success!\n"; + * } + * } + * @endcode + * + * @since 0.3.0 + */ + class ObtainToken : public CURLWrapper + { + public: + ObtainToken(Instance &instance); + + /*! + * @brief Creates an application via `/api/v1/apps`. + * + * The `body` of the returned @link answer_type answer @endlink + * contains only the URI, not the whole JSON response. + * + * @param client_name The name of your application. + * @param scopes Space separated list of scopes. Defaults to + * “read” if empty. + * @param website The URI to the homepage of your application. Can + * be an empty string. + * + * @return The URI your user has to visit. + * + * @since 0.3.0 + */ + [[nodiscard]] + answer_type step_1(string_view client_name, string_view scopes, + string_view website); + + /*! + * @brief Creates a token via `/oauth/token`. + * + * The `body` of the returned @link answer_type answer @endlink + * contains only the access token, not the whole JSON response. + * + * The access token will be set in the parent Instance. + * + * @param code The authorization code you got from the user. + * + * @return The access token. + * + * @since 0.3.0 + */ + [[nodiscard]] + answer_type step_2(string_view code); + + private: + Instance &_instance; + const string _baseuri; + string _scopes; + string _client_id; + string _client_secret; + }; + private: const string _hostname; const string _baseuri; diff --git a/src/instance.cpp b/src/instance.cpp index cc8ffeb..19c6b8b 100644 --- a/src/instance.cpp +++ b/src/instance.cpp @@ -20,6 +20,7 @@ #include <algorithm> #include <exception> +#include <regex> namespace mastodonpp { @@ -27,6 +28,9 @@ namespace mastodonpp using std::sort; using std::stoull; using std::exception; +using std::regex; +using std::regex_search; +using std::smatch; Instance::Instance(const string_view hostname, const string_view access_token) : _hostname{hostname} @@ -158,24 +162,108 @@ vector<string> Instance::get_post_formats() noexcept return _post_formats; } +Instance::ObtainToken::ObtainToken(Instance &instance) + : _instance{instance} + , _baseuri{instance.get_baseuri()} +{ + auto proxy{_instance.get_proxy()}; + if (!proxy.empty()) { - debuglog << "Couldn't find metadata.postFormats.\n"; - _post_formats = {default_value}; - return _post_formats; + CURLWrapper::set_proxy(proxy); } - pos += searchstring.size(); - auto endpos{answer.body.find("],", pos)}; - string formats{answer.body.substr(pos, endpos - pos)}; - debuglog << "Extracted postFormats: " << formats << '\n'; - while ((pos = formats.find('"', 1)) != string::npos) + if (!_instance.get_access_token().empty()) { - _post_formats.push_back(formats.substr(1, pos - 1)); - formats.erase(0, pos + 2); // 2 is the length of: ", - debuglog << "Found postFormat: " << _post_formats.back() << '\n'; + CURLWrapper::set_access_token(_instance.get_access_token()); } + if (!_instance.get_cainfo().empty()) + { + CURLWrapper::set_cainfo(_instance.get_cainfo()); + } +} - return _post_formats; +answer_type Instance::ObtainToken::step_1(const string_view client_name, + const string_view scopes, + const string_view website) +{ + parametermap parameters + { + {"client_name", client_name}, + {"redirect_uris", "urn:ietf:wg:oauth:2.0:oob"} + }; + if (!scopes.empty()) + { + _scopes = scopes; + parameters.insert({"scopes", scopes}); + } + if (!website.empty()) + { + parameters.insert({"website", website}); + } + + auto answer{make_request(http_method::POST, _baseuri + "/api/v1/apps", + parameters)}; + if (answer) + { + const regex re_id{R"("client_id"\s*:\s*"([^"]+)\")"}; + const regex re_secret{R"("client_secret"\s*:\s*"([^"]+)\")"}; + smatch match; + + if (regex_search(answer.body, match, re_id)) + { + _client_id = match[1].str(); + } + if (regex_search(answer.body, match, re_secret)) + { + _client_secret = match[1].str(); + } + + string uri{_baseuri + "/oauth/authorize?scope=" + escape_url(scopes) + + "&response_type=code" + "&redirect_uri=" + escape_url("urn:ietf:wg:oauth:2.0:oob") + + "&client_id=" + _client_id}; + if (!website.empty()) + { + uri += "&website=" + escape_url(website); + } + answer.body = uri; + debuglog << "Built URI."; + } + + return answer; +} + +answer_type Instance::ObtainToken::step_2(const string_view code) +{ + parametermap parameters + { + {"client_id", _client_id}, + {"client_secret", _client_secret}, + {"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"}, + {"code", code}, + {"grant_type", "client_credentials"} + }; + if (!_scopes.empty()) + { + parameters.insert({"scope", _scopes}); + } + + auto answer{make_request(http_method::POST, _baseuri + "/oauth/token", + parameters)}; + if (answer) + { + const regex re_token{R"("access_token"\s*:\s*"([^"]+)\")"}; + smatch match; + + if (regex_search(answer.body, match, re_token)) + { + answer.body = match[1].str(); + debuglog << "Got access token.\n"; + _instance.set_access_token(answer.body); + } + } + + return answer; } } // namespace mastodonpp |