summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--include/instance.hpp78
-rw-r--r--src/instance.cpp112
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