Show user's posts on their page
This commit is contained in:
parent
b8f037765b
commit
aab50e06b2
31
client.cpp
31
client.cpp
|
@ -102,6 +102,37 @@ std::optional<Post> MastodonClient::get_post(const std::string& host, const std:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
const char* sorting_parameters[3] = {"exclude_replies=true", "", "only_media=true"};
|
||||||
|
std::string query = sorting_parameters[sorting_method];
|
||||||
|
if (max_id) {
|
||||||
|
if (!query.empty()) {
|
||||||
|
query += '&';
|
||||||
|
}
|
||||||
|
query += "max_id=";
|
||||||
|
query += std::move(*max_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url = "https://"s + host + "/api/v1/accounts/" + account_id + "/statuses";
|
||||||
|
if (!query.empty()) {
|
||||||
|
url += '?';
|
||||||
|
url += query;
|
||||||
|
}
|
||||||
|
std::string resp = this->_send_request(url);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
6
client.h
6
client.h
|
@ -4,10 +4,9 @@
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
struct Account; // forward declaration from models.h
|
|
||||||
struct Post; // forward declaration from models.h
|
#include "models.h"
|
||||||
|
|
||||||
class CurlException : public std::exception {
|
class CurlException : public std::exception {
|
||||||
public:
|
public:
|
||||||
|
@ -52,6 +51,7 @@ 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::optional<Post> get_post(const std::string& host, const std::string& id);
|
||||||
|
std::vector<Post> get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CURL* _get_easy();
|
CURL* _get_easy();
|
||||||
|
|
|
@ -36,6 +36,7 @@ void from_json(const json& j, Account& account) {
|
||||||
account.created_at = parse_rfc3339(j.at("created_at").get_ref<const std::string&>());
|
account.created_at = parse_rfc3339(j.at("created_at").get_ref<const std::string&>());
|
||||||
account.note_html = j.at("note").get<std::string>();
|
account.note_html = j.at("note").get<std::string>();
|
||||||
j.at("avatar").get_to(account.avatar);
|
j.at("avatar").get_to(account.avatar);
|
||||||
|
j.at("avatar_static").get_to(account.avatar_static);
|
||||||
j.at("header").get_to(account.header);
|
j.at("header").get_to(account.header);
|
||||||
j.at("followers_count").get_to(account.followers_count);
|
j.at("followers_count").get_to(account.followers_count);
|
||||||
j.at("following_count").get_to(account.following_count);
|
j.at("following_count").get_to(account.following_count);
|
||||||
|
|
1
models.h
1
models.h
|
@ -32,6 +32,7 @@ struct Account {
|
||||||
time_t created_at;
|
time_t created_at;
|
||||||
blankie::html::HTMLString note_html;
|
blankie::html::HTMLString note_html;
|
||||||
std::string avatar;
|
std::string avatar;
|
||||||
|
std::string avatar_static;
|
||||||
std::string header;
|
std::string header;
|
||||||
uint64_t followers_count;
|
uint64_t followers_count;
|
||||||
uint64_t following_count;
|
uint64_t following_count;
|
||||||
|
|
|
@ -50,6 +50,9 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* POST */
|
/* POST */
|
||||||
|
.post {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
.post-header, .post-header a {
|
.post-header, .post-header a {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -147,6 +150,20 @@ a:hover {
|
||||||
.user_page-user_posts_nav a {
|
.user_page-user_posts_nav a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.user_page-user_posts_nav .selected {
|
||||||
|
font-weight: bold;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.user_page-user_posts_nav .selected:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_page-more_posts {
|
||||||
|
margin-top: 1em;
|
||||||
|
text-align: center;
|
||||||
|
/* don't ask why, but making it a block element just works */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
)EOF";
|
)EOF";
|
||||||
// one for \0, one for trailing newline
|
// one for \0, one for trailing newline
|
||||||
#define CSS_LEN sizeof(css) / sizeof(css[0]) - 2
|
#define CSS_LEN sizeof(css) / sizeof(css[0]) - 2
|
||||||
|
|
|
@ -22,6 +22,11 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (post->reblog) {
|
||||||
|
serve_redirect(req, res, get_origin(req) + '/' + server + "/@" + post->reblog->account.acct(false) + '/' + post->reblog->id, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Element body("body", {
|
Element body("body", {
|
||||||
serialize_post(req, server, *post),
|
serialize_post(req, server, *post),
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,13 +14,28 @@ static inline Element sorting_method_link(const httplib::Request& req, const std
|
||||||
|
|
||||||
|
|
||||||
void user_route(const httplib::Request& req, httplib::Response& res) {
|
void user_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
|
using namespace std::string_literals;
|
||||||
|
|
||||||
std::string server = req.matches.str(1);
|
std::string server = req.matches.str(1);
|
||||||
std::string username = req.matches.str(2);
|
std::string username = req.matches.str(2);
|
||||||
PostSortingMethod sorting_method = get_sorting_method(req.matches.str(3));
|
PostSortingMethod sorting_method = get_sorting_method(req.matches.str(3));
|
||||||
|
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::optional<Account> account;
|
std::optional<Account> account;
|
||||||
|
std::vector<Post> posts;
|
||||||
try {
|
try {
|
||||||
account = mastodon_client.get_account_by_username(server, username);
|
account = mastodon_client.get_account_by_username(server, username);
|
||||||
|
if (account) {
|
||||||
|
posts = mastodon_client.get_posts(server, account->id, sorting_method, std::move(max_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 user information", e.what());
|
serve_error(req, res, "500: Internal server error", "Failed to fetch user information", e.what());
|
||||||
|
@ -36,6 +51,17 @@ void user_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
Element body("body", {
|
Element body("body", {
|
||||||
user_header(req, server, *account, sorting_method),
|
user_header(req, server, *account, sorting_method),
|
||||||
});
|
});
|
||||||
|
body.nodes.reserve(body.nodes.size() + 2 * posts.size() + 1);
|
||||||
|
for (const Post& post : posts) {
|
||||||
|
body.nodes.push_back(serialize_post(req, server, post));
|
||||||
|
body.nodes.push_back(Element("hr"));
|
||||||
|
}
|
||||||
|
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"}));
|
||||||
|
} else {
|
||||||
|
body.nodes.push_back(Element("p", {{"class", "user_page-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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +109,7 @@ static inline Element user_header(const httplib::Request& req, const std::string
|
||||||
std::move(user_links),
|
std::move(user_links),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Element("nav", {{"class", "user_page-user_posts_nav"}}, {
|
Element("nav", {{"class", "user_page-user_posts_nav"}, {"id", "user_posts_nav"}}, {
|
||||||
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Posts),
|
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Posts),
|
||||||
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::PostsAndReplies),
|
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::PostsAndReplies),
|
||||||
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Media),
|
sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Media),
|
||||||
|
@ -109,11 +135,11 @@ static inline Element user_link_field(const httplib::Request& req, const Account
|
||||||
static inline Element sorting_method_link(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod current_method, PostSortingMethod new_method) {
|
static inline Element sorting_method_link(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod current_method, PostSortingMethod new_method) {
|
||||||
const char* method_name = sorting_method_names[new_method];
|
const char* method_name = sorting_method_names[new_method];
|
||||||
|
|
||||||
|
Element a("a", {{"href", get_origin(req) + '/' + server + "/@" + account.acct(false) + sorting_method_suffixes[new_method] + "#user_posts_nav"}}, {
|
||||||
|
method_name,
|
||||||
|
});
|
||||||
if (current_method == new_method) {
|
if (current_method == new_method) {
|
||||||
return Element("b", {method_name});
|
a.attributes.push_back({"class", "selected"});
|
||||||
} else {
|
|
||||||
return Element("a", {{"href", get_origin(req) + '/' + server + "/@" + account.acct(false) + sorting_method_suffixes[new_method]}}, {
|
|
||||||
method_name,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element);
|
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element);
|
||||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element);
|
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element);
|
||||||
static inline bool should_fix_link(lxb_dom_element_t* element);
|
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& cls);
|
||||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis);
|
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis);
|
||||||
static inline std::vector<lxb_dom_node*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis);
|
static inline std::vector<lxb_dom_node*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis);
|
||||||
|
|
||||||
|
@ -167,15 +167,19 @@ 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) {
|
Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post) {
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
|
|
||||||
|
if (post.reblog) {
|
||||||
|
return serialize_post(req, server, *post.reblog);
|
||||||
|
}
|
||||||
|
|
||||||
std::string time_title = post.edited_at < 0
|
std::string time_title = post.edited_at < 0
|
||||||
? full_time(post.created_at)
|
? full_time(post.created_at)
|
||||||
: "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 div("div", {
|
Element div("div", {{"class", "post"}}, {
|
||||||
Element("div", {{"class", "post-header"}}, {
|
Element("div", {{"class", "post-header"}}, {
|
||||||
Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false)}}, {
|
Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false)}}, {
|
||||||
Element("img", {{"class", "post-avatar"}, {"alt", "User profile picture"}, {"src", post.account.avatar}}, {}),
|
Element("img", {{"class", "post-avatar"}, {"alt", "User profile picture"}, {"src", post.account.avatar_static}}, {}),
|
||||||
Element("span", {
|
Element("span", {
|
||||||
Element("b", {preprocess_html(req, post.account.emojis, post.account.display_name)}),
|
Element("b", {preprocess_html(req, post.account.emojis, post.account.display_name)}),
|
||||||
Element("br"), "@", post.account.acct(),
|
Element("br"), "@", post.account.acct(),
|
||||||
|
@ -248,7 +252,7 @@ static inline void preprocess_link(const httplib::Request& req, const std::strin
|
||||||
lxb_dom_element_set_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
lxb_dom_element_set_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (should_fix_link(element)) {
|
if (should_fix_link(element, cls)) {
|
||||||
// Set the content of each <a> to its href
|
// Set the content of each <a> to its href
|
||||||
lxb_status_t status = lxb_dom_node_text_content_set(lxb_dom_interface_node(element), reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
lxb_status_t status = lxb_dom_node_text_content_set(lxb_dom_interface_node(element), reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||||
if (status != LXB_STATUS_OK) {
|
if (status != LXB_STATUS_OK) {
|
||||||
|
@ -257,7 +261,13 @@ static inline void preprocess_link(const httplib::Request& req, const std::strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool should_fix_link(lxb_dom_element_t* element) {
|
static std::regex unhandled_link_re("\\bunhandled-link\\b");
|
||||||
|
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& cls) {
|
||||||
|
// https://vt.social/@LucydiaLuminous/111448085044245037
|
||||||
|
if (std::regex_search(cls, unhandled_link_re)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
auto expected_element = [](lxb_dom_node_t* node, const char* expected_cls) {
|
auto expected_element = [](lxb_dom_node_t* node, const char* expected_cls) {
|
||||||
if (node->type != LXB_DOM_NODE_TYPE_ELEMENT) {
|
if (node->type != LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -46,10 +46,12 @@ std::string relative_time(time_t from, time_t to) {
|
||||||
return std::to_string(diff) + 's';
|
return std::to_string(diff) + 's';
|
||||||
} else if (diff < 60 * 60) {
|
} else if (diff < 60 * 60) {
|
||||||
return std::to_string(diff / 60) + 'm';
|
return std::to_string(diff / 60) + 'm';
|
||||||
} else if (diff < 60 * 60 * 60) {
|
} else if (diff < 24 * 60 * 60) {
|
||||||
return std::to_string(diff / (60 * 60)) + 'h';
|
return std::to_string(diff / (60 * 60)) + 'h';
|
||||||
} else if (diff < 24 * 60 * 60 * 60) {
|
} else if (diff < 7 * 24 * 60 * 60) {
|
||||||
return std::to_string(diff / (24 * 60 * 60)) + 'd';
|
return std::to_string(diff / (24 * 60 * 60)) + 'd';
|
||||||
|
} else if (diff < 4 * 7 * 24 * 60 * 60) {
|
||||||
|
return std::to_string(diff / (7 * 24 * 60 * 60)) + 'w';
|
||||||
} else {
|
} else {
|
||||||
return short_time(from);
|
return short_time(from);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue