Add post context support

This commit is contained in:
blankie 2023-11-24 10:46:47 +11:00
parent e861ab7ba6
commit 452ac756e6
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
8 changed files with 90 additions and 42 deletions

View File

@ -7,6 +7,7 @@
MastodonClient mastodon_client; 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_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 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); static size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata);
@ -82,26 +83,6 @@ std::optional<Account> MastodonClient::get_account_by_username(const std::string
} }
} }
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));
post.account.same_server = host == post.account.server;
if (post.reblog) {
post.reblog->account.same_server = host == post.reblog->account.server;
}
return post;
} 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) { std::vector<Post> MastodonClient::get_pinned_posts(const std::string& host, const std::string& account_id) {
using namespace std::string_literals; using namespace std::string_literals;
@ -109,10 +90,7 @@ std::vector<Post> MastodonClient::get_pinned_posts(const std::string& host, cons
std::vector<Post> posts = nlohmann::json::parse(std::move(resp)); std::vector<Post> posts = nlohmann::json::parse(std::move(resp));
for (Post& post : posts) { for (Post& post : posts) {
post.account.same_server = host == post.account.server; handle_post_server(post, host);
if (post.reblog) {
post.reblog->account.same_server = host == post.reblog->account.server;
}
} }
return posts; return posts;
@ -140,15 +118,45 @@ std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::
std::vector<Post> posts = nlohmann::json::parse(std::move(resp)); std::vector<Post> posts = nlohmann::json::parse(std::move(resp));
for (Post& post : posts) { for (Post& post : posts) {
post.account.same_server = host == post.account.server; handle_post_server(post, host);
if (post.reblog) {
post.reblog->account.same_server = host == post.reblog->account.server;
}
} }
return posts; 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* MastodonClient::_get_easy() {
CURL* curl = pthread_getspecific(this->_easy_key); CURL* curl = pthread_getspecific(this->_easy_key);
if (!curl) { if (!curl) {
@ -203,6 +211,13 @@ long MastodonClient::_response_status_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) { static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) {
(void)curl; (void)curl;
(void)access; (void)access;

View File

@ -50,10 +50,12 @@ public:
} }
std::optional<Account> get_account_by_username(const std::string& host, const std::string& username); std::optional<Account> get_account_by_username(const std::string& host, const std::string& username);
std::optional<Post> get_post(const std::string& host, const std::string& id);
std::vector<Post> get_pinned_posts(const std::string& host, const std::string& account_id); std::vector<Post> get_pinned_posts(const std::string& host, const std::string& account_id);
std::vector<Post> get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id); std::vector<Post> get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id);
std::optional<Post> get_post(const std::string& host, const std::string& id);
PostContext get_post_context(const std::string& host, const std::string& id);
private: private:
CURL* _get_easy(); CURL* _get_easy();
std::string _send_request(const std::string& url); std::string _send_request(const std::string& url);

View File

@ -95,6 +95,11 @@ void from_json(const json& j, Post& post) {
j.at("emojis").get_to(post.emojis); j.at("emojis").get_to(post.emojis);
} }
void from_json(const json& j, PostContext& context) {
j.at("ancestors").get_to(context.ancestors);
j.at("descendants").get_to(context.descendants);
}
static std::regex rfc3339_re(R"EOF((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:(Z)|([+-]\d{2}):(\d{2})))EOF", std::regex::ECMAScript | std::regex::icase); static std::regex rfc3339_re(R"EOF((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:(Z)|([+-]\d{2}):(\d{2})))EOF", std::regex::ECMAScript | std::regex::icase);
time_t parse_rfc3339(const std::string& str) { time_t parse_rfc3339(const std::string& str) {

View File

@ -76,8 +76,14 @@ struct Post {
std::vector<Emoji> emojis; std::vector<Emoji> emojis;
}; };
struct PostContext {
std::vector<Post> ancestors;
std::vector<Post> descendants;
};
void from_json(const nlohmann::json& j, Emoji& emoji); void from_json(const nlohmann::json& j, Emoji& emoji);
void from_json(const nlohmann::json& j, AccountField& field); void from_json(const nlohmann::json& j, AccountField& field);
void from_json(const nlohmann::json& j, Account& account); void from_json(const nlohmann::json& j, Account& account);
void from_json(const nlohmann::json& j, Media& media); void from_json(const nlohmann::json& j, Media& media);
void from_json(const nlohmann::json& j, Post& post); void from_json(const nlohmann::json& j, Post& post);
void from_json(const nlohmann::json& j, PostContext& context);

View File

@ -62,6 +62,9 @@ svg {
} }
/* POST */ /* POST */
.main_post .post-contents {
font-size: 110%;
}
.post { .post {
margin-top: 1em; margin-top: 1em;
} }

View File

@ -8,8 +8,12 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
std::string id = req.matches.str(2); std::string id = req.matches.str(2);
std::optional<Post> post; std::optional<Post> post;
PostContext context;
try { try {
post = mastodon_client.get_post(server, id); post = mastodon_client.get_post(server, id);
if (post) {
context = mastodon_client.get_post_context(server, id);
}
} catch (const std::exception& e) { } catch (const std::exception& e) {
res.status = 500; res.status = 500;
serve_error(req, res, "500: Internal server error", "Failed to fetch post information", e.what()); serve_error(req, res, "500: Internal server error", "Failed to fetch post information", e.what());
@ -27,8 +31,18 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
return; return;
} }
Element body("body", { Element body("body");
serialize_post(req, server, *post), body.nodes.reserve(context.ancestors.size() * 2 + 1 + context.descendants.size() * 2);
});
for (const Post& i : context.ancestors) {
body.nodes.push_back(serialize_post(req, server, i));
body.nodes.push_back(Element("hr"));
}
body.nodes.push_back(serialize_post(req, server, *post, false, true));
for (const Post& i : context.descendants) {
body.nodes.push_back(Element("hr"));
body.nodes.push_back(serialize_post(req, server, i));
}
serve(req, res, "", std::move(body)); serve(req, res, "", std::move(body));
} }

View File

@ -23,7 +23,7 @@ struct PostStatus {
const char* icon_html; const char* icon_html;
Node info_node; Node info_node;
}; };
static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, const std::optional<PostStatus>& post_status); static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional<PostStatus>& post_status);
static inline Element serialize_media(const Media& media); static inline Element serialize_media(const Media& media);
class CurlUrlException : public std::exception { class CurlUrlException : public std::exception {
@ -172,7 +172,7 @@ bool should_send_304(const httplib::Request& req, uint64_t hash) {
return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/'); return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/');
} }
Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned) { Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned, bool main_post) {
using namespace std::string_literals; using namespace std::string_literals;
if (post.reblog) { if (post.reblog) {
@ -180,21 +180,21 @@ Element serialize_post(const httplib::Request& req, const std::string& server, c
fa_retweet, fa_retweet,
preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"), preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"),
}; };
return serialize_post(req, server, *post.reblog, post_status); return serialize_post(req, server, *post.reblog, main_post, post_status);
} else if (pinned) { } else if (pinned) {
PostStatus post_status = { PostStatus post_status = {
fa_thumbtack, fa_thumbtack,
blankie::html::HTMLString("Pinned post"), blankie::html::HTMLString("Pinned post"),
}; };
return serialize_post(req, server, post, post_status); return serialize_post(req, server, post, main_post, post_status);
} else if (post.in_reply_to_id && post.in_reply_to_account_id && post.account.id == *post.in_reply_to_account_id) { } else if (post.in_reply_to_id && post.in_reply_to_account_id && post.account.id == *post.in_reply_to_account_id) {
PostStatus post_status = { PostStatus post_status = {
fa_reply, fa_reply,
preprocess_html(req, post.account.emojis, "Replied to "s + post.account.display_name), preprocess_html(req, post.account.emojis, "Replied to "s + post.account.display_name),
}; };
return serialize_post(req, server, post, post_status); return serialize_post(req, server, post, main_post, post_status);
} else { } else {
return serialize_post(req, server, post, std::nullopt); return serialize_post(req, server, post, main_post, std::nullopt);
} }
} }
@ -361,7 +361,7 @@ static inline std::vector<lxb_dom_node_t*> emojify(lxb_dom_document_t* document,
return res; return res;
} }
static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, const std::optional<PostStatus>& post_status) { static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional<PostStatus>& post_status) {
using namespace std::string_literals; using namespace std::string_literals;
std::string time_title = post.edited_at < 0 std::string time_title = post.edited_at < 0
@ -369,7 +369,7 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
: "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at); : "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at);
const char* time_badge = post.edited_at < 0 ? "" : " (edited)"; const char* time_badge = post.edited_at < 0 ? "" : " (edited)";
Element contents("div", {preprocess_html(req, server, post.emojis, post.content)}); Element contents("div", {{"class", "post-contents"}}, {preprocess_html(req, server, post.emojis, post.content)});
Element post_attachments("div", {{"class", "post-attachments"}}, {}); Element post_attachments("div", {{"class", "post-attachments"}}, {});
post_attachments.nodes.reserve(post.media_attachments.size()); post_attachments.nodes.reserve(post.media_attachments.size());
for (const Media& media : post.media_attachments) { for (const Media& media : post.media_attachments) {
@ -394,7 +394,7 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
Element("br"), "@", post.account.acct(), Element("br"), "@", post.account.acct(),
}), }),
}), }),
Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id}, {"title", time_title}}, { Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id + "#m"}, {"title", time_title}}, {
Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}), Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}),
}), }),
}), }),
@ -406,6 +406,9 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
blankie::html::HTMLString(post_status->icon_html), " ", post_status->info_node, blankie::html::HTMLString(post_status->icon_html), " ", post_status->info_node,
})); }));
} }
if (main_post) {
div.attributes = {{"class", "post main_post"}, {"id", "m"}};
}
return div; return div;
} }

View File

@ -20,7 +20,7 @@ std::string get_origin(const httplib::Request& req);
std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str); std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str);
bool should_send_304(const httplib::Request& req, uint64_t hash); bool should_send_304(const httplib::Request& req, uint64_t hash);
Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned = false); Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned = false, bool main_post = false);
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str); blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str);
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str); blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str);