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
|
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)
|
blankie/serializer.cpp blankie/escape.cpp)
|
||||||
set_target_properties(${PROJECT_NAME}
|
set_target_properties(${PROJECT_NAME}
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
|
|
57
client.cpp
57
client.cpp
|
@ -8,6 +8,9 @@
|
||||||
MastodonClient mastodon_client;
|
MastodonClient mastodon_client;
|
||||||
|
|
||||||
static void handle_post_server(Post& post, const std::string& host);
|
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_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);
|
||||||
|
@ -70,7 +73,7 @@ std::optional<Account> MastodonClient::get_account_by_username(const std::string
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
|
|
||||||
try {
|
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;
|
account.same_server = host == account.server;
|
||||||
return account;
|
return account;
|
||||||
} catch (const MastodonException& e) {
|
} catch (const MastodonException& e) {
|
||||||
|
@ -152,6 +155,22 @@ PostContext MastodonClient::get_post_context(const std::string& host, const std:
|
||||||
return context;
|
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* MastodonClient::_get_easy() {
|
||||||
CURL* curl = pthread_getspecific(this->_easy_key);
|
CURL* curl = pthread_getspecific(this->_easy_key);
|
||||||
if (!curl) {
|
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) {
|
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) {
|
||||||
(void)curl;
|
(void)curl;
|
||||||
(void)access;
|
(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);
|
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);
|
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:
|
private:
|
||||||
CURL* _get_easy();
|
CURL* _get_easy();
|
||||||
nlohmann::json _send_request(const std::string& url);
|
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);
|
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) {
|
server.Get("/https://?(.+)", [](const httplib::Request& req, httplib::Response& res) {
|
||||||
serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1), true);
|
serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1), true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,8 +74,8 @@ svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* POST */
|
/* POST */
|
||||||
.main_post .post-contents {
|
.main_post :is(.post-contents, details) {
|
||||||
font-size: 110%;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
.post {
|
.post {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
@ -230,7 +230,8 @@ svg {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user_page-more_posts {
|
/* USER PAGE and TAGS PAGE */
|
||||||
|
.more_posts {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
/* don't ask why, but making it a block element just works */
|
/* 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/@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) + "/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) + "/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("hr"),
|
||||||
Element("p", {
|
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 css_route(const httplib::Request& req, httplib::Response& res);
|
||||||
void user_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 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()) {
|
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) {
|
} 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));
|
serve(req, res, account->display_name + " (@" + account->acct() + ')', std::move(body));
|
||||||
|
|
Loading…
Reference in New Issue