Add tags support

This commit is contained in:
blankie 2023-11-24 22:43:53 +11:00
parent 64edd2f4a1
commit 64ff7e7350
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
9 changed files with 120 additions and 7 deletions

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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);
});

View File

@ -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 */

View File

@ -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", {

View File

@ -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);

50
routes/tags.cpp Normal file
View File

@ -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));
}

View File

@ -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));