Show user's posts on their page

This commit is contained in:
blankie 2023-11-23 19:49:27 +11:00
parent b8f037765b
commit aab50e06b2
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
9 changed files with 109 additions and 16 deletions

View File

@ -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* curl = pthread_getspecific(this->_easy_key);
if (!curl) {

View File

@ -4,10 +4,9 @@
#include <optional>
#include <exception>
#include <pthread.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 {
public:
@ -52,6 +51,7 @@ 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_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id);
private:
CURL* _get_easy();

View File

@ -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.note_html = j.at("note").get<std::string>();
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("followers_count").get_to(account.followers_count);
j.at("following_count").get_to(account.following_count);

View File

@ -32,6 +32,7 @@ struct Account {
time_t created_at;
blankie::html::HTMLString note_html;
std::string avatar;
std::string avatar_static;
std::string header;
uint64_t followers_count;
uint64_t following_count;

View File

@ -50,6 +50,9 @@ a:hover {
}
/* POST */
.post {
margin-top: 1em;
}
.post-header, .post-header a {
display: flex;
}
@ -147,6 +150,20 @@ a:hover {
.user_page-user_posts_nav a {
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";
// one for \0, one for trailing newline
#define CSS_LEN sizeof(css) / sizeof(css[0]) - 2

View File

@ -22,6 +22,11 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
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", {
serialize_post(req, server, *post),
});

View File

@ -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) {
using namespace std::string_literals;
std::string server = req.matches.str(1);
std::string username = req.matches.str(2);
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::vector<Post> posts;
try {
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) {
res.status = 500;
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", {
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));
}
@ -83,7 +109,7 @@ static inline Element user_header(const httplib::Request& req, const std::string
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::PostsAndReplies),
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) {
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) {
return Element("b", {method_name});
} else {
return Element("a", {{"href", get_origin(req) + '/' + server + "/@" + account.acct(false) + sorting_method_suffixes[new_method]}}, {
method_name,
});
a.attributes.push_back({"class", "selected"});
}
return a;
}

View File

@ -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_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 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) {
using namespace std::string_literals;
if (post.reblog) {
return serialize_post(req, server, *post.reblog);
}
std::string time_title = post.edited_at < 0
? full_time(post.created_at)
: "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at);
const char* time_badge = post.edited_at < 0 ? "" : " (edited)";
Element div("div", {
Element div("div", {{"class", "post"}}, {
Element("div", {{"class", "post-header"}}, {
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("b", {preprocess_html(req, post.account.emojis, post.account.display_name)}),
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());
}
if (should_fix_link(element)) {
if (should_fix_link(element, cls)) {
// 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());
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) {
if (node->type != LXB_DOM_NODE_TYPE_ELEMENT) {
return false;

View File

@ -46,10 +46,12 @@ std::string relative_time(time_t from, time_t to) {
return std::to_string(diff) + 's';
} else if (diff < 60 * 60) {
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';
} else if (diff < 24 * 60 * 60 * 60) {
} else if (diff < 7 * 24 * 60 * 60) {
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 {
return short_time(from);
}