coyote/client.cpp

378 lines
12 KiB
C++
Raw Normal View History

2023-11-22 08:39:24 +00:00
#include <cassert>
#include <stdexcept>
#include <nlohmann/json.hpp>
#include "client.h"
#include "models.h"
2023-11-29 11:36:52 +00:00
#include "curlu_wrapper.h"
2023-11-25 03:39:35 +00:00
#include "hiredis_wrapper.h"
2023-11-22 08:39:24 +00:00
MastodonClient mastodon_client;
2023-11-25 03:39:35 +00:00
static void lowercase(std::string& str);
2023-11-23 23:46:47 +00:00
static void handle_post_server(Post& post, const std::string& host);
2023-11-24 11:43:53 +00:00
static std::string url_encode(const std::string& in);
static inline void hexencode(char c, char out[2]);
2023-11-22 08:39:24 +00:00
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);
2023-11-25 04:50:49 +00:00
#if CURL_AT_LEAST_VERSION(7, 57, 0)
2023-11-22 08:39:24 +00:00
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
2023-11-25 04:50:49 +00:00
#endif
#if CURL_AT_LEAST_VERSION(7, 61, 0)
2023-11-22 08:39:24 +00:00
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL);
2023-11-25 04:50:49 +00:00
#endif
#if CURL_AT_LEAST_VERSION(7, 88, 0)
2023-11-22 08:39:24 +00:00
setopt(this->_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS);
2023-11-25 04:50:49 +00:00
#endif
2023-11-22 08:39:24 +00:00
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()");
}
}
2023-11-25 03:39:35 +00:00
std::optional<Account> MastodonClient::get_account_by_username(std::string host, std::string username) {
2023-11-22 08:39:24 +00:00
using namespace std::string_literals;
2023-11-25 03:39:35 +00:00
lowercase(host);
lowercase(username);
if (username.size() > host.size() && username.ends_with(host) && username[username.size() - host.size() - 1] == '@') {
username.erase(username.size() - host.size() - 1);
}
2023-11-22 08:39:24 +00:00
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/accounts/lookup");
url.set(CURLUPART_QUERY, "acct="s + url_encode(username));
2023-11-22 08:39:24 +00:00
try {
2023-11-29 11:36:52 +00:00
Account account = this->_send_request("coyote:"s + host + ":@" + username, url);
account.same_server = host == account.server;
return account;
} catch (const MastodonException& e) {
if (e.response_code != 404) {
2023-11-23 06:05:17 +00:00
throw;
}
return std::nullopt;
}
}
2023-11-25 03:39:35 +00:00
std::vector<Post> MastodonClient::get_pinned_posts(std::string host, const std::string& account_id) {
2023-11-23 13:43:55 +00:00
using namespace std::string_literals;
2023-11-25 03:39:35 +00:00
lowercase(host);
2023-11-23 13:43:55 +00:00
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + url_encode(account_id) + "/statuses");
url.set(CURLUPART_QUERY, "pinned=true");
std::vector<Post> posts = this->_send_request("coyote:"s + host + ':' + account_id + ":pinned", url);
2023-11-23 13:43:55 +00:00
for (Post& post : posts) {
2023-11-23 23:46:47 +00:00
handle_post_server(post, host);
2023-11-23 13:43:55 +00:00
}
return posts;
}
2023-11-23 08:49:27 +00:00
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"};
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + url_encode(account_id) + "/statuses");
url.set(CURLUPART_QUERY, sorting_parameters[sorting_method]);
2023-11-23 08:49:27 +00:00
if (max_id) {
2023-11-29 11:36:52 +00:00
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
2023-11-23 08:49:27 +00:00
}
2023-11-25 03:39:35 +00:00
std::vector<Post> posts = this->_send_request(std::nullopt, url);
2023-11-23 08:49:27 +00:00
for (Post& post : posts) {
2023-11-23 23:46:47 +00:00
handle_post_server(post, host);
2023-11-23 08:49:27 +00:00
}
return posts;
}
std::optional<Post> MastodonClient::get_post(const std::string& host, std::string id) {
2023-11-23 23:46:47 +00:00
using namespace std::string_literals;
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + url_encode(std::move(id)));
2023-11-23 23:46:47 +00:00
try {
2023-11-29 11:36:52 +00:00
Post post = this->_send_request(std::nullopt, url);
2023-11-23 23:46:47 +00:00
handle_post_server(post, host);
return post;
} catch (const MastodonException& e) {
if (e.response_code != 404) {
2023-11-23 23:46:47 +00:00
throw;
}
return std::nullopt;
}
}
PostContext MastodonClient::get_post_context(const std::string& host, std::string id) {
2023-11-23 23:46:47 +00:00
using namespace std::string_literals;
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + url_encode(std::move(id)) + "/context");
PostContext context = this->_send_request(std::nullopt, url);
2023-11-23 23:46:47 +00:00
for (Post& post : context.ancestors) {
handle_post_server(post, host);
}
for (Post& post : context.descendants) {
handle_post_server(post, host);
}
return context;
}
2023-11-24 11:43:53 +00:00
std::vector<Post> MastodonClient::get_tag_timeline(const std::string& host, const std::string& tag, std::optional<std::string> max_id) {
using namespace std::string_literals;
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/timelines/tag/"s + url_encode(tag));
2023-11-24 11:43:53 +00:00
if (max_id) {
2023-11-29 11:36:52 +00:00
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
2023-11-24 11:43:53 +00:00
}
2023-11-25 03:39:35 +00:00
std::vector<Post> posts = this->_send_request(std::nullopt, url);
2023-11-24 11:43:53 +00:00
for (Post& post : posts) {
handle_post_server(post, host);
}
return posts;
}
2023-11-25 03:39:35 +00:00
Instance MastodonClient::get_instance(std::string host) {
2023-11-24 12:46:00 +00:00
using namespace std::string_literals;
2023-11-25 03:39:35 +00:00
lowercase(host);
2023-11-24 12:46:00 +00:00
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v2/instance");
Instance instance = this->_send_request("coyote:"s + host + ":instance", url);
2023-11-24 12:46:00 +00:00
instance.contact_account.same_server = instance.contact_account.server == host;
return instance;
}
2023-11-25 03:39:35 +00:00
blankie::html::HTMLString MastodonClient::get_extended_description(std::string host) {
2023-11-24 12:46:00 +00:00
using namespace std::string_literals;
2023-11-25 03:39:35 +00:00
lowercase(host);
2023-11-24 12:46:00 +00:00
2023-11-29 11:36:52 +00:00
CurlUrl url;
url.set(CURLUPART_SCHEME, "https");
url.set(CURLUPART_HOST, host);
url.set(CURLUPART_PATH, "/api/v1/instance/extended_description");
nlohmann::json j = this->_send_request("coyote:"s + host + ":desc", url);
2023-11-24 12:46:00 +00:00
return blankie::html::HTMLString(j.at("content").get<std::string>());
}
2023-11-22 08:39:24 +00:00
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_TIMEOUT_MS, 30000L);
2023-11-25 04:50:49 +00:00
#if CURL_AT_LEAST_VERSION(7, 85, 0)
2023-11-22 10:54:56 +00:00
setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
2023-11-25 04:50:49 +00:00
#else
setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
#endif
2023-11-22 10:54:56 +00:00
setopt(curl, CURLOPT_USERAGENT, "Coyote (https://gitlab.com/blankX/coyote; blankie@nixnetmail.com)");
setopt(curl, CURLOPT_SHARE, this->_share);
2023-11-22 08:39:24 +00:00
} 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;
}
2023-11-29 11:36:52 +00:00
nlohmann::json MastodonClient::_send_request(std::optional<std::string> cache_key, const CurlUrl& url) {
2023-11-25 03:39:35 +00:00
std::optional<std::string> cached;
if (redis && cache_key && (cached = redis->get(*cache_key))) {
return nlohmann::json::parse(std::move(*cached));
}
2023-11-22 08:39:24 +00:00
std::string res;
CURL* curl = this->_get_easy();
2023-11-29 11:36:52 +00:00
setopt(curl, CURLOPT_CURLU, url.get());
2023-11-22 08:39:24 +00:00
setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
setopt(curl, CURLOPT_WRITEDATA, &res);
CURLcode code = curl_easy_perform(curl);
2023-11-29 11:36:52 +00:00
setopt(curl, CURLOPT_CURLU, nullptr);
2023-11-22 08:39:24 +00:00
if (code) {
throw CurlException(code);
}
long response_code = this->_response_status_code();
2023-11-25 03:39:35 +00:00
nlohmann::json j = nlohmann::json::parse(res);
if (response_code != 200) {
throw MastodonException(response_code, j.at("error").get<std::string>());
}
2023-11-25 03:39:35 +00:00
if (redis && cache_key) {
redis->set(*cache_key, std::move(res), 60 * 60);
}
return j;
2023-11-22 08:39:24 +00:00
}
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;
}
2023-11-25 03:39:35 +00:00
static void lowercase(std::string& str) {
for (size_t i = 0; i < str.size(); i++) {
if (str[i] >= 'A' && str[i] <= 'Z') {
2023-11-25 04:50:49 +00:00
str[i] = static_cast<char>(str[i] - 'A' + 'a');
2023-11-25 03:39:35 +00:00
}
}
}
2023-11-23 23:46:47 +00:00
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;
}
}
2023-11-24 11:43:53 +00:00
static std::string url_encode(const std::string& in) {
std::string out;
char encoded[2];
size_t pos = 0;
size_t last_pos = 0;
out.reserve(in.size());
while ((pos = in.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", pos)) != std::string::npos) {
out.append(in, last_pos, pos - last_pos);
hexencode(in[pos], encoded);
out += '%';
out.append(encoded, 2);
pos++;
last_pos = pos;
}
if (in.size() > last_pos) {
out.append(in, last_pos);
}
return out;
}
static inline void hexencode(char c, char out[2]) {
char nibble1 = (c >> 4) & 0xF;
char nibble2 = c & 0xF;
auto hexencode = [](char nibble) {
return static_cast<char>(nibble < 10
? '0' + nibble
: 'A' + nibble - 10);
};
out[0] = hexencode(nibble1);
out[1] = hexencode(nibble2);
}
2023-11-22 08:39:24 +00:00
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;
}