Add tags support
This commit is contained in:
		
							parent
							
								
									64edd2f4a1
								
							
						
					
					
						commit
						64ff7e7350
					
				| 
						 | 
				
			
			@ -30,7 +30,7 @@ add_link_options(${FLAGS})
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp config.cpp models.cpp client.cpp servehelper.cpp timeutils.cpp
 | 
			
		||||
    routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp
 | 
			
		||||
    routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp routes/tags.cpp
 | 
			
		||||
    blankie/serializer.cpp blankie/escape.cpp)
 | 
			
		||||
set_target_properties(${PROJECT_NAME}
 | 
			
		||||
    PROPERTIES
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										57
									
								
								client.cpp
								
								
								
								
							
							
						
						
									
										57
									
								
								client.cpp
								
								
								
								
							| 
						 | 
				
			
			@ -8,6 +8,9 @@
 | 
			
		|||
MastodonClient mastodon_client;
 | 
			
		||||
 | 
			
		||||
static void handle_post_server(Post& post, const std::string& host);
 | 
			
		||||
static std::string url_encode(const std::string& in);
 | 
			
		||||
static inline void hexencode(char c, char out[2]);
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +73,7 @@ std::optional<Account> MastodonClient::get_account_by_username(const std::string
 | 
			
		|||
    using namespace std::string_literals;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        Account account = this->_send_request("https://"s + host + "/api/v1/accounts/lookup?acct=" + username);
 | 
			
		||||
        Account account = this->_send_request("https://"s + host + "/api/v1/accounts/lookup?acct=" + url_encode(username));
 | 
			
		||||
        account.same_server = host == account.server;
 | 
			
		||||
        return account;
 | 
			
		||||
    } catch (const MastodonException& e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +155,22 @@ PostContext MastodonClient::get_post_context(const std::string& host, const std:
 | 
			
		|||
    return context;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
    std::string url = "https://"s + host + "/api/v1/timelines/tag/" + url_encode(tag);
 | 
			
		||||
    if (max_id) {
 | 
			
		||||
        url += "?max_id=";
 | 
			
		||||
        url += std::move(*max_id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::vector<Post> posts = this->_send_request(url);
 | 
			
		||||
    for (Post& post : posts) {
 | 
			
		||||
        handle_post_server(post, host);
 | 
			
		||||
    }
 | 
			
		||||
    return posts;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CURL* MastodonClient::_get_easy() {
 | 
			
		||||
    CURL* curl = pthread_getspecific(this->_easy_key);
 | 
			
		||||
    if (!curl) {
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +237,42 @@ static void handle_post_server(Post& post, const std::string& host) {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) {
 | 
			
		||||
    (void)curl;
 | 
			
		||||
    (void)access;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								client.h
								
								
								
								
							
							
						
						
									
										2
									
								
								client.h
								
								
								
								
							| 
						 | 
				
			
			@ -71,6 +71,8 @@ public:
 | 
			
		|||
    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);
 | 
			
		||||
 | 
			
		||||
    std::vector<Post> get_tag_timeline(const std::string& host, const std::string& tag, std::optional<std::string> max_id);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    CURL* _get_easy();
 | 
			
		||||
    nlohmann::json _send_request(const std::string& url);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								main.cpp
								
								
								
								
							
							
						
						
									
										3
									
								
								main.cpp
								
								
								
								
							| 
						 | 
				
			
			@ -46,6 +46,9 @@ int main(int argc, char** argv) {
 | 
			
		|||
        serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1) + "/@" + req.matches.str(2) + '/' + req.matches.str(3), true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // protect against ..
 | 
			
		||||
    server.Get("/(" DOMAIN_RE ")/tags/(?!\\.|%2E|%2e)(.+)", tags_route);
 | 
			
		||||
 | 
			
		||||
    server.Get("/https://?(.+)", [](const httplib::Request& req, httplib::Response& res) {
 | 
			
		||||
        serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1), true);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,8 +74,8 @@ svg {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/* POST */
 | 
			
		||||
.main_post .post-contents {
 | 
			
		||||
    font-size: 110%;
 | 
			
		||||
.main_post :is(.post-contents, details) {
 | 
			
		||||
    font-size: 1.1rem;
 | 
			
		||||
}
 | 
			
		||||
.post {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
| 
						 | 
				
			
			@ -230,7 +230,8 @@ svg {
 | 
			
		|||
    text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.user_page-more_posts {
 | 
			
		||||
/* USER PAGE and TAGS PAGE */
 | 
			
		||||
.more_posts {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    /* don't ask why, but making it a block element just works */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ void home_route(const httplib::Request& req, httplib::Response& res) {
 | 
			
		|||
            Element("li", {Element("a", {{"href", get_origin(req) + "/vt.social/@lina"}}, {"Asahi Lina (朝日リナ) // nullptr::live"})}),
 | 
			
		||||
            Element("li", {Element("a", {{"href", get_origin(req) + "/vt.social/@LucydiaLuminous/111290028216105435"}}, {"\"I love kids and their creativity. So I was explain…\""})}),
 | 
			
		||||
            Element("li", {Element("a", {{"href", get_origin(req) + "/ruby.social/@CoralineAda/109951421922797743"}}, {"\"My partner just said \"I'm starting to think nostal…\""})}),
 | 
			
		||||
            Element("li", {Element("a", {{"href", get_origin(req) + "/pawoo.net/tags/OMORIFANART"}}, {"#OMORIFANART"})}),
 | 
			
		||||
        }),
 | 
			
		||||
        Element("hr"),
 | 
			
		||||
        Element("p", {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,3 +8,4 @@ void home_route(const httplib::Request& req, httplib::Response& res);
 | 
			
		|||
void css_route(const httplib::Request& req, httplib::Response& res);
 | 
			
		||||
void user_route(const httplib::Request& req, httplib::Response& res);
 | 
			
		||||
void status_route(const httplib::Request& req, httplib::Response& res);
 | 
			
		||||
void tags_route(const httplib::Request& req, httplib::Response& res);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
#include "routes.h"
 | 
			
		||||
#include "../servehelper.h"
 | 
			
		||||
#include "../client.h"
 | 
			
		||||
#include "../models.h"
 | 
			
		||||
 | 
			
		||||
void tags_route(const httplib::Request& req, httplib::Response& res) {
 | 
			
		||||
    using namespace std::string_literals;
 | 
			
		||||
 | 
			
		||||
    std::string server = req.matches.str(1);
 | 
			
		||||
    std::string tag = req.matches.str(2);
 | 
			
		||||
    std::optional<std::string> max_id;
 | 
			
		||||
    if (req.has_param("max_id")) {
 | 
			
		||||
        max_id = req.get_param_value("max_id");
 | 
			
		||||
        if (max_id->empty() || max_id->find_first_not_of("0123456789") != std::string::npos) {
 | 
			
		||||
            res.status = 400;
 | 
			
		||||
            serve_error(req, res, "400: Bad Request", "Invalid max_id query paramter");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::vector<Post> posts;
 | 
			
		||||
    try {
 | 
			
		||||
        posts = mastodon_client.get_tag_timeline(server, tag, max_id);
 | 
			
		||||
    } catch (const std::exception& e) {
 | 
			
		||||
        res.status = 500;
 | 
			
		||||
        serve_error(req, res, "500: Internal server error", "Failed to fetch tag timeline", e.what());
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Element body("body", {Element("h2", {"Posts for #", tag})});
 | 
			
		||||
    if (!posts.empty()) {
 | 
			
		||||
        body.nodes.reserve(body.nodes.size() + posts.size() * 2 - 1 + 2);
 | 
			
		||||
 | 
			
		||||
        for (size_t i = 0; i < posts.size(); i++) {
 | 
			
		||||
            if (i != 0) {
 | 
			
		||||
                body.nodes.push_back(Element("hr"));
 | 
			
		||||
            }
 | 
			
		||||
            body.nodes.push_back(serialize_post(req, server, posts[i]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        body.nodes.push_back(Element("hr"));
 | 
			
		||||
        body.nodes.push_back(Element("a", {{"class", "more_posts"}, {"href", "?max_id="s + posts[posts.size() - 1].id}}, {"See more"}));
 | 
			
		||||
    } else {
 | 
			
		||||
        body.nodes.push_back(Element("p", {{"class", "more_posts"}}, {
 | 
			
		||||
            max_id ? "There are no more posts" : "No results found",
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    serve(req, res, "Posts for #"s + tag, std::move(body));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -66,9 +66,9 @@ void user_route(const httplib::Request& req, httplib::Response& res) {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (!posts.empty()) {
 | 
			
		||||
        body.nodes.push_back(Element("a", {{"class", "user_page-more_posts"}, {"href", "?max_id="s + posts[posts.size() - 1].id + "#user_posts_nav"}}, {"See more"}));
 | 
			
		||||
        body.nodes.push_back(Element("a", {{"class", "more_posts"}, {"href", "?max_id="s + posts[posts.size() - 1].id + "#user_posts_nav"}}, {"See more"}));
 | 
			
		||||
    } else if (max_id) {
 | 
			
		||||
        body.nodes.push_back(Element("p", {{"class", "user_page-more_posts"}}, {"There are no more posts"}));
 | 
			
		||||
        body.nodes.push_back(Element("p", {{"class", "more_posts"}}, {"There are no more posts"}));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    serve(req, res, account->display_name + " (@" + account->acct() + ')', std::move(body));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue