Add rudimentary post support
This commit is contained in:
parent
1808df8f75
commit
b8f037765b
|
@ -28,8 +28,8 @@ list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-s
|
|||
add_link_options(${FLAGS})
|
||||
|
||||
|
||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp config.cpp models.cpp client.cpp servehelper.cpp
|
||||
routes/css.cpp routes/user.cpp
|
||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp config.cpp models.cpp client.cpp servehelper.cpp timeutils.cpp
|
||||
routes/css.cpp routes/user.cpp routes/status.cpp
|
||||
blankie/serializer.cpp blankie/escape.cpp)
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
PROPERTIES
|
||||
|
|
24
client.cpp
24
client.cpp
|
@ -70,7 +70,29 @@ std::optional<Account> MastodonClient::get_account_by_username(const std::string
|
|||
|
||||
try {
|
||||
std::string resp = this->_send_request("https://"s + host + "/api/v1/accounts/lookup?acct=" + username);
|
||||
return nlohmann::json::parse(std::move(resp));
|
||||
Account res = nlohmann::json::parse(std::move(resp));
|
||||
res.same_server = host == res.server;
|
||||
return res;
|
||||
} catch (const CurlException& e) {
|
||||
if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) {
|
||||
throw;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Post> MastodonClient::get_post(const std::string& host, const std::string& id) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
try {
|
||||
std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id);
|
||||
Post post = nlohmann::json::parse(std::move(resp));
|
||||
post.account.same_server = host == post.account.server;
|
||||
if (post.reblog) {
|
||||
post.reblog->account.same_server = host == post.reblog->account.server;
|
||||
}
|
||||
return post;
|
||||
} catch (const CurlException& e) {
|
||||
if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) {
|
||||
throw;
|
||||
|
|
2
client.h
2
client.h
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include <curl/curl.h>
|
||||
struct Account; // forward declaration from models.h
|
||||
struct Post; // forward declaration from models.h
|
||||
|
||||
class CurlException : public std::exception {
|
||||
public:
|
||||
|
@ -50,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);
|
||||
|
||||
private:
|
||||
CURL* _get_easy();
|
||||
|
|
7
main.cpp
7
main.cpp
|
@ -45,6 +45,13 @@ int main(int argc, char** argv) {
|
|||
serve_redirect(req, res, "/"s + req.matches.str(1) + "/@" + req.matches.str(2) + req.matches.str(3), true);
|
||||
});
|
||||
|
||||
server.Get("/(" DOMAIN_RE ")/@" ACCT_RE "/(\\d+)", status_route);
|
||||
server.Get("/(" DOMAIN_RE ")/users/(" ACCT_RE ")/statuses/(\\d+)", [](const httplib::Request& req, httplib::Response& res) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
serve_redirect(req, res, "/"s + req.matches.str(1) + "/@" + req.matches.str(2) + '/' + req.matches.str(3), true);
|
||||
});
|
||||
|
||||
#ifndef NDEBUG
|
||||
server.Get("/debug/exception/known", [](const httplib::Request& req, httplib::Response& res) {
|
||||
throw std::runtime_error("awoo");
|
||||
|
|
30
models.cpp
30
models.cpp
|
@ -48,7 +48,35 @@ void from_json(const json& j, Account& account) {
|
|||
if (!std::regex_match(url, sm, host_regex)) {
|
||||
throw std::runtime_error("failed to find host in url: "s + url);
|
||||
}
|
||||
account.domain_name = sm.str(1);
|
||||
account.server = sm.str(1);
|
||||
}
|
||||
|
||||
void from_json(const json& j, Post& post) {
|
||||
j.at("id").get_to(post.id);
|
||||
post.created_at = parse_rfc3339(j.at("created_at").get_ref<const std::string&>());
|
||||
if (!j.at("in_reply_to_id").is_null()) {
|
||||
post.in_reply_to_id = j["in_reply_to_id"].get<std::string>();
|
||||
}
|
||||
if (!j.at("in_reply_to_account_id").is_null()) {
|
||||
post.in_reply_to_account_id = j["in_reply_to_account_id"].get<std::string>();
|
||||
}
|
||||
j.at("sensitive").get_to(post.sensitive);
|
||||
j.at("spoiler_text").get_to(post.spoiler_text);
|
||||
j.at("replies_count").get_to(post.replies_count);
|
||||
j.at("reblogs_count").get_to(post.reblogs_count);
|
||||
j.at("favourites_count").get_to(post.favorites_count);
|
||||
if (!j.at("edited_at").is_null()) {
|
||||
post.edited_at = parse_rfc3339(j["edited_at"].get_ref<const std::string&>());
|
||||
} else {
|
||||
post.edited_at = -1;
|
||||
}
|
||||
post.content = j.at("content").get<std::string>();
|
||||
if (!j.at("reblog").is_null()) {
|
||||
post.reblog = std::make_unique<Post>();
|
||||
from_json(j["reblog"].get<json>(), *post.reblog.get());
|
||||
}
|
||||
j.at("account").get_to(post.account);
|
||||
j.at("emojis").get_to(post.emojis);
|
||||
}
|
||||
|
||||
|
||||
|
|
31
models.h
31
models.h
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
#include "blankie/serializer.h"
|
||||
|
@ -25,7 +26,8 @@ struct AccountField {
|
|||
struct Account {
|
||||
std::string id;
|
||||
std::string username;
|
||||
std::string domain_name;
|
||||
std::string server;
|
||||
bool same_server;
|
||||
std::string display_name;
|
||||
time_t created_at;
|
||||
blankie::html::HTMLString note_html;
|
||||
|
@ -36,8 +38,35 @@ struct Account {
|
|||
uint64_t statuses_count;
|
||||
std::vector<Emoji> emojis;
|
||||
std::vector<AccountField> fields;
|
||||
|
||||
constexpr std::string acct(bool always_show_domain = true) const {
|
||||
std::string res = this->username;
|
||||
if (always_show_domain || !this->same_server) {
|
||||
res += '@';
|
||||
res += this->server;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
struct Post {
|
||||
std::string id;
|
||||
time_t created_at;
|
||||
std::optional<std::string> in_reply_to_id;
|
||||
std::optional<std::string> in_reply_to_account_id;
|
||||
bool sensitive;
|
||||
std::string spoiler_text;
|
||||
uint64_t replies_count;
|
||||
uint64_t reblogs_count;
|
||||
uint64_t favorites_count;
|
||||
time_t edited_at; // negative is not edited
|
||||
blankie::html::HTMLString content;
|
||||
std::unique_ptr<Post> reblog;
|
||||
Account account;
|
||||
std::vector<Emoji> emojis;
|
||||
};
|
||||
|
||||
void from_json(const nlohmann::json& j, Emoji& emoji);
|
||||
void from_json(const nlohmann::json& j, AccountField& field);
|
||||
void from_json(const nlohmann::json& j, Account& account);
|
||||
void from_json(const nlohmann::json& j, Post& post);
|
||||
|
|
|
@ -41,6 +41,7 @@ a:hover {
|
|||
color: var(--bright-accent-color);
|
||||
}
|
||||
|
||||
/* CUSTOM EMOJI */
|
||||
.custom_emoji {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
@ -48,6 +49,30 @@ a:hover {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* POST */
|
||||
.post-header, .post-header a {
|
||||
display: flex;
|
||||
}
|
||||
.post-header a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.post-header a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.post-avatar {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
}
|
||||
.post-header span, .post-header time {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.post-header a:has(time) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ERROR PAGE */
|
||||
.error {
|
||||
text-align: center;
|
||||
|
|
|
@ -6,3 +6,4 @@ extern const uint64_t css_hash;
|
|||
|
||||
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);
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
|
||||
void status_route(const httplib::Request& req, httplib::Response& res) {
|
||||
std::string server = req.matches.str(1);
|
||||
std::string id = req.matches.str(2);
|
||||
|
||||
std::optional<Post> post;
|
||||
try {
|
||||
post = mastodon_client.get_post(server, id);
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
serve_error(req, res, "500: Internal server error", "Failed to fetch post information", e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
res.status = 404;
|
||||
serve_error(req, res, "404: Post not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Element body("body", {
|
||||
serialize_post(req, server, *post),
|
||||
});
|
||||
serve(req, res, "", std::move(body));
|
||||
}
|
|
@ -2,14 +2,15 @@
|
|||
#include "../servehelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
#include "../timeutils.h"
|
||||
|
||||
static const char* sorting_method_names[3] = {"Posts", "Posts and replies", "Media"};
|
||||
static const char* sorting_method_suffixes[3] = {"", "/with_replies", "/media"};
|
||||
static inline PostSortingMethod get_sorting_method(const std::string& method);
|
||||
|
||||
static inline Element user_header(const httplib::Request& req, const Account& account, PostSortingMethod sorting_method);
|
||||
static inline Element user_header(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod sorting_method);
|
||||
static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field);
|
||||
static inline Element sorting_method_link(const httplib::Request& req, 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);
|
||||
|
||||
|
||||
void user_route(const httplib::Request& req, httplib::Response& res) {
|
||||
|
@ -33,9 +34,9 @@ void user_route(const httplib::Request& req, httplib::Response& res) {
|
|||
}
|
||||
|
||||
Element body("body", {
|
||||
user_header(req, *account, sorting_method),
|
||||
user_header(req, server, *account, sorting_method),
|
||||
});
|
||||
serve(req, res, account->display_name + " (" + account->username + '@' + account->domain_name + ')', std::move(body));
|
||||
serve(req, res, account->display_name + " (" + account->acct() + ')', std::move(body));
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,12 +51,7 @@ static inline PostSortingMethod get_sorting_method(const std::string& method) {
|
|||
}
|
||||
|
||||
|
||||
static inline Element user_header(const httplib::Request& req, const Account& account, PostSortingMethod sorting_method) {
|
||||
struct tm created_at;
|
||||
char created_at_str[16];
|
||||
gmtime_r(&account.created_at, &created_at);
|
||||
strftime(created_at_str, 16, "%Y-%m-%d", &created_at);
|
||||
|
||||
static inline Element user_header(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod sorting_method) {
|
||||
Element user_links("table", {{"class", "user_page-user_links"}}, {});
|
||||
user_links.nodes.reserve(account.fields.size());
|
||||
for (const AccountField& i : account.fields) {
|
||||
|
@ -71,9 +67,9 @@ static inline Element user_header(const httplib::Request& req, const Account& ac
|
|||
Element("img", {{"class", "user_page-profile"}, {"alt", "User profile picture"}, {"src", account.avatar}}, {}),
|
||||
}),
|
||||
Element("span", {
|
||||
Element("b", {preprocess_html(req, account.emojis, account.display_name)}), " (", account.username, "@", account.domain_name, ")",
|
||||
Element("b", {preprocess_html(req, account.emojis, account.display_name)}), " (", account.acct(), ")",
|
||||
Element("br"),
|
||||
Element("br"), Element("b", {"Joined: "}), std::string(created_at_str),
|
||||
Element("br"), Element("b", {"Joined: "}), short_time(account.created_at),
|
||||
Element("br"),
|
||||
Element("b", {std::to_string(account.statuses_count)}), " Posts", " / ",
|
||||
Element("b", {std::to_string(account.following_count)}), " Following", " / ",
|
||||
|
@ -82,15 +78,15 @@ static inline Element user_header(const httplib::Request& req, const Account& ac
|
|||
}),
|
||||
|
||||
Element("div", {{"class", "user_page-user_description"}}, {
|
||||
Element("div", {{"class", "user_page-user_bio"}}, {preprocess_html(req, account.domain_name, account.emojis, account.note_html)}),
|
||||
Element("div", {{"class", "user_page-user_bio"}}, {preprocess_html(req, account.server, account.emojis, account.note_html)}),
|
||||
|
||||
std::move(user_links),
|
||||
}),
|
||||
|
||||
Element("nav", {{"class", "user_page-user_posts_nav"}}, {
|
||||
sorting_method_link(req, account, sorting_method, PostSortingMethod::Posts),
|
||||
sorting_method_link(req, account, sorting_method, PostSortingMethod::PostsAndReplies),
|
||||
sorting_method_link(req, account, sorting_method, PostSortingMethod::Media),
|
||||
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),
|
||||
}),
|
||||
});
|
||||
return header;
|
||||
|
@ -101,27 +97,22 @@ static inline Element user_link_field(const httplib::Request& req, const Account
|
|||
|
||||
Element tr("tr", {
|
||||
Element("th", {preprocess_html(req, account.emojis, field.name)}),
|
||||
Element("td", {preprocess_html(req, account.domain_name, account.emojis, field.value_html)}),
|
||||
Element("td", {preprocess_html(req, account.server, account.emojis, field.value_html)}),
|
||||
});
|
||||
if (field.verified_at >= 0) {
|
||||
struct tm verified_at;
|
||||
char verified_at_str[32];
|
||||
gmtime_r(&field.verified_at, &verified_at);
|
||||
strftime(verified_at_str, 32, "%Y-%m-%d %H:%M:%S %Z", &verified_at);
|
||||
|
||||
tr.attributes = {{"class", "verified"}, {"title", "Verified at "s + verified_at_str}};
|
||||
tr.attributes = {{"class", "verified"}, {"title", "Verified at "s + full_time(field.verified_at)}};
|
||||
}
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
static inline Element sorting_method_link(const httplib::Request& req, 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];
|
||||
|
||||
if (current_method == new_method) {
|
||||
return Element("b", {method_name});
|
||||
} else {
|
||||
return Element("a", {{"href", get_origin(req) + '/' + account.domain_name + "/@" + account.username + sorting_method_suffixes[new_method]}}, {
|
||||
return Element("a", {{"href", get_origin(req) + '/' + server + "/@" + account.acct(false) + sorting_method_suffixes[new_method]}}, {
|
||||
method_name,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "config.h"
|
||||
#include "models.h"
|
||||
#include "timeutils.h"
|
||||
#include "servehelper.h"
|
||||
#include "lxb_wrapper.h"
|
||||
#include "routes/routes.h"
|
||||
|
@ -163,6 +164,33 @@ bool should_send_304(const httplib::Request& req, uint64_t hash) {
|
|||
return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/');
|
||||
}
|
||||
|
||||
Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
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", {{"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("span", {
|
||||
Element("b", {preprocess_html(req, post.account.emojis, post.account.display_name)}),
|
||||
Element("br"), "@", post.account.acct(),
|
||||
}),
|
||||
}),
|
||||
Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id}, {"title", time_title}}, {
|
||||
Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}),
|
||||
}),
|
||||
}),
|
||||
|
||||
preprocess_html(req, server, post.emojis, post.content),
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str) {
|
||||
LXB::HTML::Document document(str.str);
|
||||
preprocess_html(req, domain_name, emojis, document.body_element());
|
||||
|
@ -195,6 +223,7 @@ static inline void preprocess_html(const httplib::Request& req, const std::strin
|
|||
}
|
||||
}
|
||||
|
||||
static std::regex mention_class_re("\\bmention\\b");
|
||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
|
@ -205,8 +234,14 @@ static inline void preprocess_link(const httplib::Request& req, const std::strin
|
|||
}
|
||||
std::string href(reinterpret_cast<const char*>(href_c), href_c_len);
|
||||
|
||||
size_t cls_c_len;
|
||||
const lxb_char_t* cls_c = lxb_dom_element_class(element, &cls_c_len);
|
||||
std::string cls = cls_c ? std::string(reinterpret_cast<const char*>(cls_c), cls_c_len) : "";
|
||||
|
||||
std::string instance_url_base = "https://"s + domain_name;
|
||||
if (href.starts_with(instance_url_base + '/') || href == instance_url_base) {
|
||||
// .mention is used in note and posts
|
||||
// Instance base is used for link fields
|
||||
if (std::regex_search(cls, mention_class_re) || href.starts_with(instance_url_base + '/') || href == instance_url_base) {
|
||||
// Proxy this instance's URLs to Coyote
|
||||
href = proxy_mastodon_url(req, std::move(href));
|
||||
|
||||
|
@ -290,7 +325,7 @@ static inline std::vector<lxb_dom_node_t*> emojify(lxb_dom_document_t* document,
|
|||
res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast<const lxb_char_t*>(buf.data()), buf.size())));
|
||||
buf.clear();
|
||||
|
||||
lxb_dom_element_t* img = lxb_dom_element_create(document, reinterpret_cast<const lxb_char_t*>("img"), 3, nullptr, 0, nullptr, 0, nullptr, 0, false);
|
||||
lxb_dom_element_t* img = lxb_dom_element_create(document, reinterpret_cast<const lxb_char_t*>("IMG"), 3, nullptr, 0, nullptr, 0, nullptr, 0, false);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("class"), 5, reinterpret_cast<const lxb_char_t*>("custom_emoji"), 12);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("alt"), 3, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("title"), 5, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <httplib/httplib.h>
|
||||
#include "blankie/serializer.h"
|
||||
struct Post; // forward declaration from models.h
|
||||
struct Emoji; // forward declaration from models.h
|
||||
|
||||
using Element = blankie::html::Element;
|
||||
|
@ -19,5 +20,7 @@ std::string get_origin(const httplib::Request& req);
|
|||
std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str);
|
||||
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);
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str);
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
#include <system_error>
|
||||
|
||||
#include "timeutils.h"
|
||||
|
||||
time_t current_time() {
|
||||
struct timespec tp;
|
||||
|
||||
if (clock_gettime(CLOCK_REALTIME, &tp)) {
|
||||
throw std::system_error(errno, std::generic_category(), "clock_gettime()");
|
||||
}
|
||||
|
||||
return tp.tv_sec;
|
||||
}
|
||||
|
||||
std::string short_time(time_t time) {
|
||||
struct tm tm;
|
||||
gmtime_r(&time, &tm);
|
||||
|
||||
char buf[16];
|
||||
size_t len = strftime(buf, 16, "%Y-%m-%d", &tm);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
std::string full_time(time_t time) {
|
||||
struct tm tm;
|
||||
gmtime_r(&time, &tm);
|
||||
|
||||
char buf[32];
|
||||
size_t len = strftime(buf, 32, "%Y-%m-%d %H:%M:%S GMT", &tm);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
std::string to_rfc3339(time_t time) {
|
||||
struct tm tm;
|
||||
gmtime_r(&time, &tm);
|
||||
|
||||
char buf[32];
|
||||
size_t len = strftime(buf, 32, "%Y-%m-%dT%H:%M:%SZ", &tm);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
std::string relative_time(time_t from, time_t to) {
|
||||
time_t diff = to - from;
|
||||
|
||||
if (diff < 60) {
|
||||
return std::to_string(diff) + 's';
|
||||
} else if (diff < 60 * 60) {
|
||||
return std::to_string(diff / 60) + 'm';
|
||||
} else if (diff < 60 * 60 * 60) {
|
||||
return std::to_string(diff / (60 * 60)) + 'h';
|
||||
} else if (diff < 24 * 60 * 60 * 60) {
|
||||
return std::to_string(diff / (24 * 60 * 60)) + 'd';
|
||||
} else {
|
||||
return short_time(from);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
time_t current_time();
|
||||
|
||||
std::string short_time(time_t time);
|
||||
std::string full_time(time_t time);
|
||||
std::string to_rfc3339(time_t time);
|
||||
std::string relative_time(time_t from, time_t to);
|
Loading…
Reference in New Issue