247 lines
7.5 KiB
C++
247 lines
7.5 KiB
C++
#include <cassert>
|
|
#include <stdexcept>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "client.h"
|
|
#include "models.h"
|
|
|
|
MastodonClient mastodon_client;
|
|
|
|
static void handle_post_server(Post& post, const std::string& host);
|
|
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp);
|
|
static void share_unlock(CURL* curl, curl_lock_data data, void* clientp);
|
|
static size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata);
|
|
|
|
template<typename A>
|
|
void setopt(CURLSH* share, CURLSHoption option, A parameter) {
|
|
CURLSHcode code = curl_share_setopt(share, option, parameter);
|
|
if (code) {
|
|
throw CurlShareException(code);
|
|
}
|
|
}
|
|
|
|
template<typename A>
|
|
void setopt(CURL* curl, CURLoption option, A parameter) {
|
|
CURLcode code = curl_easy_setopt(curl, option, parameter);
|
|
if (code) {
|
|
throw CurlException(code);
|
|
}
|
|
}
|
|
|
|
|
|
MastodonClient::MastodonClient() {
|
|
this->_share = curl_share_init();
|
|
if (!this->_share) {
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
try {
|
|
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
|
|
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
|
|
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
|
|
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
|
|
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
|
|
|
|
setopt(this->_share, CURLSHOPT_LOCKFUNC, share_lock);
|
|
setopt(this->_share, CURLSHOPT_UNLOCKFUNC, share_unlock);
|
|
setopt(this->_share, CURLSHOPT_USERDATA, this->_share_locks);
|
|
} catch (const std::exception& e) {
|
|
curl_share_cleanup(this->_share);
|
|
throw;
|
|
}
|
|
|
|
int err = pthread_key_create(&this->_easy_key, curl_easy_cleanup);
|
|
if (err) {
|
|
curl_share_cleanup(this->_share);
|
|
throw std::system_error(err, std::generic_category(), "pthread_key_create()");
|
|
}
|
|
}
|
|
|
|
MastodonClient::~MastodonClient() {
|
|
curl_share_cleanup(this->_share);
|
|
|
|
int err = pthread_key_delete(this->_easy_key);
|
|
if (err) {
|
|
perror("pthread_key_delete()");
|
|
}
|
|
}
|
|
|
|
std::optional<Account> MastodonClient::get_account_by_username(const std::string& host, const std::string& username) {
|
|
using namespace std::string_literals;
|
|
|
|
try {
|
|
std::string resp = this->_send_request("https://"s + host + "/api/v1/accounts/lookup?acct=" + username);
|
|
Account res = nlohmann::json::parse(std::move(resp));
|
|
res.same_server = host == res.server;
|
|
return res;
|
|
} catch (const CurlException& e) {
|
|
if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) {
|
|
throw;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::vector<Post> MastodonClient::get_pinned_posts(const std::string& host, const std::string& account_id) {
|
|
using namespace std::string_literals;
|
|
|
|
std::string resp = this->_send_request("https://"s + host + "/api/v1/accounts/" + account_id + "/statuses?pinned=true");
|
|
std::vector<Post> posts = nlohmann::json::parse(std::move(resp));
|
|
|
|
for (Post& post : posts) {
|
|
handle_post_server(post, host);
|
|
}
|
|
|
|
return posts;
|
|
}
|
|
|
|
std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id) {
|
|
using namespace std::string_literals;
|
|
|
|
const char* sorting_parameters[3] = {"exclude_replies=true", "", "only_media=true"};
|
|
std::string query = sorting_parameters[sorting_method];
|
|
if (max_id) {
|
|
if (!query.empty()) {
|
|
query += '&';
|
|
}
|
|
query += "max_id=";
|
|
query += std::move(*max_id);
|
|
}
|
|
|
|
std::string url = "https://"s + host + "/api/v1/accounts/" + account_id + "/statuses";
|
|
if (!query.empty()) {
|
|
url += '?';
|
|
url += query;
|
|
}
|
|
std::string resp = this->_send_request(url);
|
|
std::vector<Post> posts = nlohmann::json::parse(std::move(resp));
|
|
|
|
for (Post& post : posts) {
|
|
handle_post_server(post, host);
|
|
}
|
|
|
|
return posts;
|
|
}
|
|
|
|
std::optional<Post> MastodonClient::get_post(const std::string& host, const std::string& id) {
|
|
using namespace std::string_literals;
|
|
|
|
try {
|
|
std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id);
|
|
Post post = nlohmann::json::parse(std::move(resp));
|
|
handle_post_server(post, host);
|
|
return post;
|
|
} catch (const CurlException& e) {
|
|
if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) {
|
|
throw;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
PostContext MastodonClient::get_post_context(const std::string& host, const std::string& id) {
|
|
using namespace std::string_literals;
|
|
|
|
std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id + "/context");
|
|
PostContext context = nlohmann::json::parse(std::move(resp));
|
|
|
|
for (Post& post : context.ancestors) {
|
|
handle_post_server(post, host);
|
|
}
|
|
for (Post& post : context.descendants) {
|
|
handle_post_server(post, host);
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
CURL* MastodonClient::_get_easy() {
|
|
CURL* curl = pthread_getspecific(this->_easy_key);
|
|
if (!curl) {
|
|
curl = curl_easy_init();
|
|
if (!curl) {
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
try {
|
|
setopt(curl, CURLOPT_FAILONERROR, 1L);
|
|
setopt(curl, CURLOPT_TIMEOUT_MS, 10000L);
|
|
setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
|
setopt(curl, CURLOPT_USERAGENT, "Coyote (https://gitlab.com/blankX/coyote; blankie@nixnetmail.com)");
|
|
setopt(curl, CURLOPT_SHARE, this->_share);
|
|
} catch (const std::exception& e) {
|
|
curl_easy_cleanup(curl);
|
|
throw;
|
|
}
|
|
|
|
int err = pthread_setspecific(this->_easy_key, curl);
|
|
if (err) {
|
|
curl_easy_cleanup(curl);
|
|
throw std::system_error(err, std::generic_category(), "pthread_setspecific()");
|
|
}
|
|
}
|
|
|
|
return curl;
|
|
}
|
|
|
|
std::string MastodonClient::_send_request(const std::string& url) {
|
|
std::string res;
|
|
CURL* curl = this->_get_easy();
|
|
|
|
setopt(curl, CURLOPT_URL, url.c_str());
|
|
setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
|
|
setopt(curl, CURLOPT_WRITEDATA, &res);
|
|
CURLcode code = curl_easy_perform(curl);
|
|
if (code) {
|
|
throw CurlException(code);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
long MastodonClient::_response_status_code() {
|
|
long response_code;
|
|
CURLcode code = curl_easy_getinfo(this->_get_easy(), CURLINFO_RESPONSE_CODE, &response_code);
|
|
if (code) {
|
|
throw CurlException(code);
|
|
}
|
|
return response_code;
|
|
}
|
|
|
|
|
|
static void handle_post_server(Post& post, const std::string& host) {
|
|
post.account.same_server = host == post.account.server;
|
|
if (post.reblog) {
|
|
post.reblog->account.same_server = host == post.reblog->account.server;
|
|
}
|
|
}
|
|
|
|
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) {
|
|
(void)curl;
|
|
(void)access;
|
|
|
|
std::mutex* mutexes = reinterpret_cast<std::mutex*>(clientp);
|
|
assert(CURL_LOCK_DATA_LAST > data);
|
|
mutexes[data].lock();
|
|
}
|
|
|
|
static void share_unlock(CURL* curl, curl_lock_data data, void* clientp) {
|
|
(void)curl;
|
|
|
|
std::mutex* mutexes = reinterpret_cast<std::mutex*>(clientp);
|
|
assert(CURL_LOCK_DATA_LAST > data);
|
|
mutexes[data].unlock();
|
|
}
|
|
|
|
static size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
|
size_t realsize = size * nmemb;
|
|
std::string* str = reinterpret_cast<std::string*>(userdata);
|
|
|
|
// XXX should we bother with exceptions
|
|
str->append(ptr, realsize);
|
|
|
|
return realsize;
|
|
}
|