2020-01-03 12:42:10 +01:00
|
|
|
/* This file is part of mastodonpp.
|
|
|
|
* Copyright © 2020 tastytea <tastytea@tastytea.de>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "instance.hpp"
|
2020-11-13 14:00:03 +01:00
|
|
|
|
2020-01-05 11:06:50 +01:00
|
|
|
#include "log.hpp"
|
2020-01-03 12:42:10 +01:00
|
|
|
|
2020-01-11 19:21:34 +01:00
|
|
|
#include <algorithm>
|
2020-01-11 18:13:15 +01:00
|
|
|
#include <exception>
|
2020-01-12 16:28:04 +01:00
|
|
|
#include <regex>
|
2020-01-11 18:13:15 +01:00
|
|
|
|
2020-01-03 12:42:10 +01:00
|
|
|
namespace mastodonpp
|
|
|
|
{
|
|
|
|
|
2020-01-11 18:13:15 +01:00
|
|
|
using std::exception;
|
2020-01-12 16:28:04 +01:00
|
|
|
using std::regex;
|
|
|
|
using std::regex_search;
|
|
|
|
using std::smatch;
|
2020-11-13 14:00:03 +01:00
|
|
|
using std::sort;
|
|
|
|
using std::stoull;
|
2020-01-03 12:42:10 +01:00
|
|
|
|
2020-03-21 11:18:22 +01:00
|
|
|
Instance::Instance(const string_view hostname, const string_view access_token)
|
|
|
|
: _hostname{hostname}
|
|
|
|
, _baseuri{"https://" + _hostname}
|
|
|
|
, _max_chars{0}
|
|
|
|
{
|
|
|
|
set_access_token(access_token);
|
|
|
|
}
|
|
|
|
|
2020-03-21 11:27:02 +01:00
|
|
|
Instance::Instance(const Instance &other)
|
|
|
|
: CURLWrapper{other}
|
|
|
|
, _hostname{other._hostname}
|
|
|
|
, _baseuri{other._baseuri}
|
|
|
|
, _access_token{other._access_token}
|
|
|
|
, _max_chars{other._max_chars}
|
|
|
|
, _proxy{other._proxy}
|
|
|
|
, _post_formats{other._post_formats}
|
|
|
|
, _cainfo{other._cainfo}
|
|
|
|
, _useragent{other._useragent}
|
|
|
|
{
|
2020-11-13 14:00:03 +01:00
|
|
|
CURLWrapper::setup_connection_properties(_proxy, _access_token, _cainfo,
|
|
|
|
_useragent);
|
2020-03-21 11:27:02 +01:00
|
|
|
}
|
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
uint64_t Instance::get_max_chars() noexcept
|
2020-01-05 11:06:50 +01:00
|
|
|
{
|
2020-01-06 09:31:05 +01:00
|
|
|
constexpr uint64_t default_max_chars{500};
|
2020-01-05 20:47:34 +01:00
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
if (_max_chars != 0)
|
2020-01-06 09:31:05 +01:00
|
|
|
{
|
2020-01-12 15:27:11 +01:00
|
|
|
return _max_chars;
|
|
|
|
}
|
2020-01-05 11:06:50 +01:00
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
try
|
|
|
|
{
|
|
|
|
debuglog << "Querying " << _hostname << " for max_toot_chars…\n";
|
2020-11-13 14:00:03 +01:00
|
|
|
const auto answer{
|
|
|
|
make_request(http_method::GET, _baseuri + "/api/v1/instance", {})};
|
2020-01-12 15:27:11 +01:00
|
|
|
if (!answer)
|
2020-01-06 09:31:05 +01:00
|
|
|
{
|
2020-01-12 15:27:11 +01:00
|
|
|
debuglog << "Could not get instance info.\n";
|
2020-01-11 23:04:40 +01:00
|
|
|
return default_max_chars;
|
2020-01-06 09:31:05 +01:00
|
|
|
}
|
2020-01-12 15:27:11 +01:00
|
|
|
|
2020-11-13 14:00:03 +01:00
|
|
|
// clang-format off
|
2020-01-12 15:27:11 +01:00
|
|
|
_max_chars = [&answer]
|
|
|
|
{
|
2020-11-13 14:00:03 +01:00
|
|
|
// clang-format on
|
2020-01-14 21:03:04 +01:00
|
|
|
const regex re_chars{R"("max_toot_chars"\s*:\s*([^"]+))"};
|
|
|
|
smatch match;
|
|
|
|
|
|
|
|
if (regex_search(answer.body, match, re_chars))
|
2020-01-12 15:27:11 +01:00
|
|
|
{
|
2020-01-14 21:03:04 +01:00
|
|
|
return static_cast<uint64_t>(stoull(match[1].str()));
|
2020-01-12 15:27:11 +01:00
|
|
|
}
|
|
|
|
|
2020-01-14 21:03:04 +01:00
|
|
|
debuglog << "max_toot_chars not found.\n";
|
|
|
|
return default_max_chars;
|
2020-01-12 15:27:11 +01:00
|
|
|
}();
|
|
|
|
debuglog << "Set _max_chars to: " << _max_chars << '\n';
|
|
|
|
}
|
|
|
|
catch (const exception &e)
|
|
|
|
{
|
|
|
|
debuglog << "Unexpected exception: " << e.what() << '\n';
|
|
|
|
return default_max_chars;
|
2020-01-05 11:06:50 +01:00
|
|
|
}
|
2020-01-06 09:31:05 +01:00
|
|
|
|
|
|
|
return _max_chars;
|
2020-01-05 11:06:50 +01:00
|
|
|
}
|
2020-01-03 12:42:10 +01:00
|
|
|
|
2020-01-11 19:21:34 +01:00
|
|
|
answer_type Instance::get_nodeinfo()
|
|
|
|
{
|
2020-11-13 14:00:03 +01:00
|
|
|
auto answer{
|
|
|
|
make_request(http_method::GET, _baseuri + "/.well-known/nodeinfo", {})};
|
2020-01-11 19:21:34 +01:00
|
|
|
if (!answer)
|
|
|
|
{
|
|
|
|
debuglog << "NodeInfo not found.\n";
|
|
|
|
return answer;
|
|
|
|
}
|
|
|
|
|
|
|
|
vector<string> hrefs;
|
2020-01-14 21:53:42 +01:00
|
|
|
const regex re_href{R"("href"\s*:\s*"([^"]+)\")"};
|
|
|
|
smatch match;
|
|
|
|
string body = answer.body;
|
|
|
|
while (regex_search(body, match, re_href))
|
2020-01-11 19:21:34 +01:00
|
|
|
{
|
2020-01-14 21:53:42 +01:00
|
|
|
hrefs.push_back(match[1].str());
|
2020-01-11 19:21:34 +01:00
|
|
|
debuglog << "Found href: " << hrefs.back() << '\n';
|
2020-01-14 21:53:42 +01:00
|
|
|
body = match.suffix();
|
2020-01-11 19:21:34 +01:00
|
|
|
}
|
|
|
|
sort(hrefs.begin(), hrefs.end()); // We assume they are sortable strings.
|
|
|
|
debuglog << "Selecting href: " << hrefs.back() << '\n';
|
|
|
|
|
|
|
|
return make_request(http_method::GET, hrefs.back(), {});
|
|
|
|
}
|
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
vector<string> Instance::get_post_formats() noexcept
|
2020-01-11 20:05:11 +01:00
|
|
|
{
|
|
|
|
constexpr auto default_value{"text/plain"};
|
|
|
|
|
|
|
|
if (!_post_formats.empty())
|
|
|
|
{
|
|
|
|
return _post_formats;
|
|
|
|
}
|
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
try
|
2020-01-11 20:05:11 +01:00
|
|
|
{
|
2020-01-12 15:27:11 +01:00
|
|
|
debuglog << "Querying " << _hostname << " for postFormats…\n";
|
|
|
|
const auto answer{get_nodeinfo()};
|
|
|
|
if (!answer)
|
|
|
|
{
|
|
|
|
debuglog << "Couldn't get NodeInfo.\n";
|
|
|
|
_post_formats = {default_value};
|
|
|
|
return _post_formats;
|
|
|
|
}
|
|
|
|
|
2020-01-14 22:28:14 +01:00
|
|
|
const regex re_allformats(R"("postFormats"\s*:\s*\[([^\]]+)\])");
|
|
|
|
smatch match;
|
|
|
|
if (!regex_search(answer.body, match, re_allformats))
|
2020-01-12 15:27:11 +01:00
|
|
|
{
|
|
|
|
debuglog << "Couldn't find metadata.postFormats.\n";
|
|
|
|
_post_formats = {default_value};
|
|
|
|
return _post_formats;
|
|
|
|
}
|
2020-01-14 22:28:14 +01:00
|
|
|
string allformats{match[1].str()};
|
|
|
|
debuglog << "Found postFormats: " << allformats << '\n';
|
2020-01-12 15:27:11 +01:00
|
|
|
|
2020-01-14 22:28:14 +01:00
|
|
|
const regex re_format(R"(\s*"([^"]+)\"\s*,?)");
|
|
|
|
|
|
|
|
while (regex_search(allformats, match, re_format))
|
2020-01-12 15:27:11 +01:00
|
|
|
{
|
2020-01-14 22:28:14 +01:00
|
|
|
_post_formats.push_back(match[1].str());
|
|
|
|
allformats = match.suffix();
|
2020-01-12 15:27:11 +01:00
|
|
|
debuglog << "Found postFormat: " << _post_formats.back() << '\n';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (const std::exception &e)
|
|
|
|
{
|
|
|
|
debuglog << "Unexpected exception: " << e.what() << '\n';
|
|
|
|
return {default_value};
|
2020-01-11 20:05:11 +01:00
|
|
|
}
|
|
|
|
|
2020-01-12 15:27:11 +01:00
|
|
|
return _post_formats;
|
|
|
|
}
|
|
|
|
|
2020-01-12 16:28:04 +01:00
|
|
|
answer_type Instance::ObtainToken::step_1(const string_view client_name,
|
|
|
|
const string_view scopes,
|
|
|
|
const string_view website)
|
|
|
|
{
|
2020-11-13 14:00:03 +01:00
|
|
|
parametermap parameters{{"client_name", client_name},
|
|
|
|
{"redirect_uris", "urn:ietf:wg:oauth:2.0:oob"}};
|
2020-01-12 16:28:04 +01:00
|
|
|
if (!scopes.empty())
|
|
|
|
{
|
|
|
|
_scopes = scopes;
|
|
|
|
parameters.insert({"scopes", scopes});
|
|
|
|
}
|
|
|
|
if (!website.empty())
|
|
|
|
{
|
|
|
|
parameters.insert({"website", website});
|
|
|
|
}
|
|
|
|
|
2020-11-13 14:00:03 +01:00
|
|
|
auto answer{
|
|
|
|
make_request(http_method::POST, _baseuri + "/api/v1/apps", parameters)};
|
2020-01-12 16:28:04 +01:00
|
|
|
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)
|
2020-11-13 14:00:03 +01:00
|
|
|
+ "&response_type=code"
|
|
|
|
"&redirect_uri="
|
|
|
|
+ escape_url("urn:ietf:wg:oauth:2.0:oob")
|
|
|
|
+ "&client_id=" + _client_id};
|
2020-01-12 16:28:04 +01:00
|
|
|
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)
|
|
|
|
{
|
2020-11-13 13:03:49 +01:00
|
|
|
parametermap parameters{{"client_id", _client_id},
|
|
|
|
{"client_secret", _client_secret},
|
|
|
|
{"redirect_uri", "urn:ietf:wg:oauth:2.0:oob"},
|
|
|
|
{"code", code},
|
|
|
|
{"grant_type", "authorization_code"}};
|
2020-01-12 16:28:04 +01:00
|
|
|
if (!_scopes.empty())
|
|
|
|
{
|
|
|
|
parameters.insert({"scope", _scopes});
|
|
|
|
}
|
|
|
|
|
2020-11-13 14:00:03 +01:00
|
|
|
auto answer{
|
|
|
|
make_request(http_method::POST, _baseuri + "/oauth/token", parameters)};
|
2020-01-12 16:28:04 +01:00
|
|
|
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;
|
2020-01-11 20:05:11 +01:00
|
|
|
}
|
|
|
|
|
2020-01-03 12:42:10 +01:00
|
|
|
} // namespace mastodonpp
|