Add post context support
This commit is contained in:
		
							parent
							
								
									e861ab7ba6
								
							
						
					
					
						commit
						452ac756e6
					
				
							
								
								
									
										71
									
								
								client.cpp
								
								
								
								
							
							
						
						
									
										71
									
								
								client.cpp
								
								
								
								
							| 
						 | 
				
			
			@ -7,6 +7,7 @@
 | 
			
		|||
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
    for (Post& post : posts) {
 | 
			
		||||
        post.account.same_server = host == post.account.server;
 | 
			
		||||
        if (post.reblog) {
 | 
			
		||||
            post.reblog->account.same_server = host == post.reblog->account.server;
 | 
			
		||||
        }
 | 
			
		||||
        handle_post_server(post, host);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
    for (Post& post : posts) {
 | 
			
		||||
        post.account.same_server = host == post.account.server;
 | 
			
		||||
        if (post.reblog) {
 | 
			
		||||
            post.reblog->account.same_server = host == post.reblog->account.server;
 | 
			
		||||
        }
 | 
			
		||||
        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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
    (void)curl;
 | 
			
		||||
    (void)access;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								client.h
								
								
								
								
							
							
						
						
									
										4
									
								
								client.h
								
								
								
								
							| 
						 | 
				
			
			@ -50,10 +50,12 @@ public:
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    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_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:
 | 
			
		||||
    CURL* _get_easy();
 | 
			
		||||
    std::string _send_request(const std::string& url);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,11 @@ void from_json(const json& j, Post& post) {
 | 
			
		|||
    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);
 | 
			
		||||
time_t parse_rfc3339(const std::string& str) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										6
									
								
								models.h
								
								
								
								
							
							
						
						
									
										6
									
								
								models.h
								
								
								
								
							| 
						 | 
				
			
			@ -76,8 +76,14 @@ struct Post {
 | 
			
		|||
    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, AccountField& field);
 | 
			
		||||
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, Post& post);
 | 
			
		||||
void from_json(const nlohmann::json& j, PostContext& context);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,6 +62,9 @@ svg {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/* POST */
 | 
			
		||||
.main_post .post-contents {
 | 
			
		||||
    font-size: 110%;
 | 
			
		||||
}
 | 
			
		||||
.post {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,12 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
 | 
			
		|||
    std::string id = req.matches.str(2);
 | 
			
		||||
 | 
			
		||||
    std::optional<Post> post;
 | 
			
		||||
    PostContext context;
 | 
			
		||||
    try {
 | 
			
		||||
        post = mastodon_client.get_post(server, id);
 | 
			
		||||
        if (post) {
 | 
			
		||||
            context = mastodon_client.get_post_context(server, id);
 | 
			
		||||
        }
 | 
			
		||||
    } catch (const std::exception& e) {
 | 
			
		||||
        res.status = 500;
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Element body("body", {
 | 
			
		||||
        serialize_post(req, server, *post),
 | 
			
		||||
    });
 | 
			
		||||
    Element body("body");
 | 
			
		||||
    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));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ struct PostStatus {
 | 
			
		|||
    const char* icon_html;
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
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] != '/');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
    if (post.reblog) {
 | 
			
		||||
| 
						 | 
				
			
			@ -180,21 +180,21 @@ Element serialize_post(const httplib::Request& req, const std::string& server, c
 | 
			
		|||
            fa_retweet,
 | 
			
		||||
            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) {
 | 
			
		||||
        PostStatus post_status = {
 | 
			
		||||
            fa_thumbtack,
 | 
			
		||||
            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) {
 | 
			
		||||
        PostStatus post_status = {
 | 
			
		||||
            fa_reply,
 | 
			
		||||
            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 {
 | 
			
		||||
        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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    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"}}, {});
 | 
			
		||||
    post_attachments.nodes.reserve(post.media_attachments.size());
 | 
			
		||||
    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("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}),
 | 
			
		||||
            }),
 | 
			
		||||
        }),
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
    if (main_post) {
 | 
			
		||||
        div.attributes = {{"class", "post main_post"}, {"id", "m"}};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return div;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
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::vector<Emoji>& emojis, const std::string& str);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue