diff --git a/CMakeLists.txt b/CMakeLists.txt index c71fee55..3b4f2e7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.5.1 + VERSION 1.6.0 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 08c47fbc..e71b1bcf 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -28,7 +28,7 @@ typedef struct contacts_contact { int priority; CONVO_NOTIFY_MODE notifications; - int64_t mute_until; + int64_t mute_until; // unix timestamp (seconds) CONVO_EXPIRATION_MODE exp_mode; int exp_seconds; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 8f38ef06..1d80e014 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -82,12 +82,13 @@ struct contact_info { // conversation is hidden. Otherwise (0) this is a regular, unpinned // conversation. notify_mode notifications = notify_mode::defaulted; - int64_t mute_until = 0; // If non-zero, disable notifications until the given unix timestamp - // (seconds, overriding whatever the current `notifications` value is - // until the timestamp expires). + std::chrono::sys_seconds mute_until{0s}; // If timestamp is non-zero, disable notifications + // until the given unix timestamp (seconds, overriding + // whatever the current `notifications` value is until + // the timestamp expires). expiration_mode exp_mode = expiration_mode::none; // The expiry time; none if not expiring. std::chrono::seconds exp_timer{0}; // The expiration timer (in seconds) - int64_t created = 0; // Unix timestamp (seconds) when this contact was added + std::chrono::sys_seconds created{0s}; // Unix timestamp (seconds) when this contact was added explicit contact_info(std::string sid); @@ -384,7 +385,14 @@ class Contacts : public ConfigBase { /// Inputs: /// - `session_id` -- hex string of the session id /// - `timestamp` -- standard unix timestamp of the time contact was created - void set_created(std::string_view session_id, int64_t timestamp); + void set_created(std::string_view session_id, std::chrono::sys_seconds timestamp); + + /// Deprecated: takes timestamp as an integer and guess whether it is seconds, milliseconds, or + /// microseconds. + [[deprecated( + "pass a std::chrono::sys_seconds instead (perhaps using " + "session::to_sys_seconds)")]] void + set_created(std::string_view session_id, int64_t timestamp); /// API: contacts/contacts::erase /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index ce954ddb..a713b8e2 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -67,7 +67,7 @@ class val_loader; namespace convo { struct base { - int64_t last_read = 0; + sys_milliseconds last_read{}; bool unread = false; protected: diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 64c2e48f..8d405560 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include "../base.hpp" @@ -220,7 +219,11 @@ class Info : public ConfigBase { /// Inputs: /// - `session_id` -- hex string of the session id /// - `timestamp` -- standard unix timestamp when the group was created - void set_created(int64_t timestamp); + void set_created(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether the input is seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_created(int64_t timestamp); /// API: groups/Info::get_created /// @@ -229,9 +232,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `std::optional` -- the unix timestamp when the group was created, or nullopt if - /// the creation timestamp is not set. - std::optional get_created() const; + /// - `std::chrono::sys_seconds` -- the unix timestamp when the group was + /// created, or nullopt if the creation timestamp is not set. + std::optional get_created() const; /// API: groups/Info::set_delete_before /// @@ -239,13 +242,14 @@ class Info : public ConfigBase { /// the closed group history with a timestamp earlier than this value. Returns nullopt if no /// delete-before timestamp is set. /// - /// The given value is checked for sanity (e.g. if you pass milliseconds it will be - /// interpreted as such) - /// /// Inputs: /// - `timestamp` -- the new unix timestamp before which clients should delete messages. Pass 0 /// (or negative) to disable the delete-before timestamp. - void set_delete_before(int64_t timestamp); + void set_delete_before(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether you meant seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_delete_before(int64_t timestamp); /// API: groups/Info::get_delete_before /// @@ -257,8 +261,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `int64_t` -- the unix timestamp for which all older messages shall be delete - std::optional get_delete_before() const; + /// - `sys_seconds` -- the unix timestamp for which all older messages shall be deleted, or + /// nullopt if there is no delete-before timestamp set. + std::optional get_delete_before() const; /// API: groups/Info::set_delete_attach_before /// @@ -272,9 +277,12 @@ class Info : public ConfigBase { /// /// Inputs: /// - `timestamp` -- the new unix timestamp before which clients should delete attachments. Pass - /// 0 - /// (or negative) to disable the delete-attachment-before timestamp. - void set_delete_attach_before(int64_t timestamp); + /// 0 (or negative) to disable the delete-attachment-before timestamp. + void set_delete_attach_before(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether you meant seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_delete_attach_before(int64_t timestamp); /// API: groups/Info::get_delete_attach_before /// @@ -286,8 +294,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `int64_t` -- the unix timestamp for which all older message attachments shall be deleted - std::optional get_delete_attach_before() const; + /// - `sys_seconds` -- the unix timestamp for which all older message attachments shall be + /// deleted, or nullopt if delete-attach-before is not enabled. + std::optional get_delete_attach_before() const; /// API: groups/Info::destroy_group /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index ef279c28..48e470a6 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -6,7 +6,6 @@ extern "C" { #include "base.h" #include "notify.h" -#include "util.h" // Maximum length of a group name, in bytes LIBSESSION_EXPORT extern const size_t GROUP_NAME_MAX_LENGTH; diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 55febbc9..057c6994 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -82,11 +82,13 @@ namespace session::config { struct base_group_info { static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded - int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means - // pinned higher (i.e. higher priority conversations come first). - int64_t joined_at = 0; // unix timestamp (seconds) when the group was joined (or re-joined) + int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means + // pinned higher (i.e. higher priority conversations come first). + std::chrono::sys_seconds joined_at{}; // unix timestamp (seconds) when the group + // was joined (or re-joined) notify_mode notifications = notify_mode::defaulted; // When the user wants notifications - int64_t mute_until = 0; // unix timestamp (seconds) until which notifications are disabled + std::chrono::sys_seconds mute_until{}; // unix timestamp (seconds) until which + // notifications are disabled std::string name; // human-readable; always set for a legacy closed group, only used before // joining a new closed group (after joining the group info provide the name) diff --git a/include/session/util.hpp b/include/session/util.hpp index fc57908f..9cd6d452 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -249,13 +249,7 @@ inline std::string utf8_truncate(std::string val, size_t n) { return val; } -// Helper function to transform a timestamp provided in seconds, milliseconds or microseconds to -// seconds -inline int64_t to_epoch_seconds(int64_t timestamp) { - return timestamp > 9'000'000'000'000 ? timestamp / 1'000'000 - : timestamp > 9'000'000'000 ? timestamp / 1'000 - : timestamp; -} +using sys_milliseconds = std::chrono::sys_time; // Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it. inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) { @@ -276,4 +270,10 @@ static_assert(std::is_same_v< std::chrono::seconds, decltype(std::declval().time_since_epoch())>); +// Takes a timestamp as unix epoch milliseconds (not seconds, or microseconds) and wraps it in a +// sys_ms containing it. +inline sys_milliseconds as_sys_ms(int64_t timestamp) { + return sys_milliseconds{std::chrono::milliseconds{timestamp}}; +} + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 5c1be6b4..2f52f972 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -92,7 +93,9 @@ void contact_info::load(const dict& info_dict) { } else { notifications = notify_mode::defaulted; } - mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); + // Older client versions might have accidentally stored this as ms, so run it through + // to_sys_seconds: + mute_until = to_sys_seconds(int_or_0(info_dict, "!")); int exp_mode_ = int_or_0(info_dict, "e"); if (exp_mode_ >= static_cast(expiration_mode::none) && @@ -113,7 +116,9 @@ void contact_info::load(const dict& info_dict) { } } - created = to_epoch_seconds(int_or_0(info_dict, "j")); + // Older client versions might have accidentally stored this as ms, so run it through + // to_sys_seconds: + created = to_sys_seconds(int_or_0(info_dict, "j")); } void contact_info::into(contacts_contact& c) const { @@ -132,12 +137,12 @@ void contact_info::into(contacts_contact& c) const { c.blocked = blocked; c.priority = priority; c.notifications = static_cast(notifications); - c.mute_until = to_epoch_seconds(mute_until); + c.mute_until = mute_until.time_since_epoch().count(); c.exp_mode = static_cast(exp_mode); c.exp_seconds = exp_timer.count(); if (c.exp_seconds <= 0 && c.exp_mode != CONVO_EXPIRATION_NONE) c.exp_mode = CONVO_EXPIRATION_NONE; - c.created = to_epoch_seconds(created); + c.created = created.time_since_epoch().count(); } contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, 66} { @@ -156,12 +161,12 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, blocked = c.blocked; priority = c.priority; notifications = static_cast(c.notifications); - mute_until = to_epoch_seconds(c.mute_until); + mute_until = to_sys_seconds(c.mute_until); exp_mode = static_cast(c.exp_mode); exp_timer = exp_mode == expiration_mode::none ? 0s : std::chrono::seconds{c.exp_seconds}; if (exp_timer <= 0s && exp_mode != expiration_mode::none) exp_mode = expiration_mode::none; - created = to_epoch_seconds(c.created); + created = to_sys_seconds(c.created); } std::optional Contacts::get(std::string_view pubkey_hex) const { @@ -211,7 +216,7 @@ void Contacts::set(const contact_info& contact) { if (notify == notify_mode::mentions_only) notify = notify_mode::all; set_positive_int(info["@"], static_cast(notify)); - set_positive_int(info["!"], to_epoch_seconds(contact.mute_until)); + set_ts(info["!"], contact.mute_until); set_pair_if( contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, @@ -220,7 +225,7 @@ void Contacts::set(const contact_info& contact) { info["E"], contact.exp_timer.count()); - set_positive_int(info["j"], to_epoch_seconds(contact.created)); + set_ts(info["j"], contact.created); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -285,9 +290,9 @@ void Contacts::set_expiry( set(c); } -void Contacts::set_created(std::string_view session_id, int64_t timestamp) { +void Contacts::set_created(std::string_view session_id, std::chrono::sys_seconds timestamp) { auto c = get_or_construct(session_id); - c.created = to_epoch_seconds(timestamp); + c.created = timestamp; set(c); } diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 54f73b62..cfd6e018 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -5,14 +5,10 @@ #include #include -#include -#include -#include #include #include "internal.hpp" #include "session/config/convo_info_volatile.h" -#include "session/config/error.h" #include "session/export.h" #include "session/types.hpp" #include "session/util.hpp" @@ -30,17 +26,17 @@ namespace convo { check_session_id(session_id); } one_to_one::one_to_one(const convo_info_volatile_1to1& c) : - base{c.last_read, c.unread}, session_id{c.session_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, session_id{c.session_id, 66} {} void one_to_one::into(convo_info_volatile_1to1& c) const { std::memcpy(c.session_id, session_id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } community::community(const convo_info_volatile_community& c) : config::community{c.base_url, c.room, std::span{c.pubkey, 32}}, - base{c.last_read, c.unread} {} + base{as_sys_ms(c.last_read), c.unread} {} void community::into(convo_info_volatile_community& c) const { static_assert(sizeof(c.base_url) == BASE_URL_MAX_LENGTH + 1); @@ -48,7 +44,7 @@ namespace convo { copy_c_str(c.base_url, base_url()); copy_c_str(c.room, room_norm()); std::memcpy(c.pubkey, pubkey().data(), 32); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -59,11 +55,11 @@ namespace convo { check_session_id(id, "03"); } group::group(const convo_info_volatile_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, id{c.group_id, 66} {} void group::into(convo_info_volatile_group& c) const { std::memcpy(c.group_id, id.c_str(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -74,11 +70,11 @@ namespace convo { check_session_id(id); } legacy_group::legacy_group(const convo_info_volatile_legacy_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, id{c.group_id, 66} {} void legacy_group::into(convo_info_volatile_legacy_group& c) const { std::memcpy(c.group_id, id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -91,19 +87,19 @@ namespace convo { check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); } blinded_one_to_one::blinded_one_to_one(const convo_info_volatile_blinded_1to1& c) : - base{c.last_read, c.unread}, + base{as_sys_ms(c.last_read), c.unread}, blinded_session_id{c.blinded_session_id, 66}, legacy_blinding{c.legacy_blinding} {} void blinded_one_to_one::into(convo_info_volatile_blinded_1to1& c) const { std::memcpy(c.blinded_session_id, blinded_session_id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; c.legacy_blinding = legacy_blinding; } void base::load(const dict& info_dict) { - last_read = int_or_0(info_dict, "r"); + last_read = as_sys_ms(int_or_0(info_dict, "r")); unread = (bool)int_or_0(info_dict, "u"); } @@ -263,23 +259,21 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { auto r = info["r"]; - // If we're making the last_read value *older* for some reason then ignore the prune cutoff - // (because we might be intentionally resetting the value after a deletion, for instance). - if (auto* val = r.integer(); val && c.last_read < *val) - r = c.last_read; - else { - std::chrono::system_clock::time_point last_read{std::chrono::milliseconds{c.last_read}}; - if (last_read > std::chrono::system_clock::now() - PRUNE_LOW) - info["r"] = c.last_read; - } + if (auto* val = r.integer(); + // If we're making the last_read value *older* for some reason then ignore the prune cutoff + // (because we might be intentionally resetting the value after a deletion, for instance). + (val && c.last_read < sys_milliseconds{std::chrono::milliseconds{*val}}) // + || + // Otherwise set it if it's more recent than the prune cutoff + c.last_read > std::chrono::system_clock::now() - PRUNE_LOW) + + r = c.last_read.time_since_epoch().count(); set_flag(info["u"], c.unread); } void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { - const int64_t cutoff = std::chrono::duration_cast( - (std::chrono::system_clock::now() - prune).time_since_epoch()) - .count(); + const auto cutoff = std::chrono::system_clock::now() - prune; std::vector stale; for (auto it = begin_1to1(); it != end(); ++it) diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 2025ca0a..92cdc41c 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -3,13 +3,12 @@ #include #include -#include +#include #include "../internal.hpp" #include "session/config/error.h" #include "session/config/groups/info.h" #include "session/export.h" -#include "session/types.hpp" #include "session/util.hpp" using namespace std::literals; @@ -84,33 +83,47 @@ void Info::set_expiry_timer(std::chrono::seconds expiration_timer) { set_positive_int(data["E"], expiration_timer.count()); } +void Info::set_created(std::chrono::sys_seconds timestamp) { + set_ts(data["c"], timestamp); +} void Info::set_created(int64_t timestamp) { - set_positive_int(data["c"], to_epoch_seconds(timestamp)); + set_created(to_sys_seconds(timestamp)); } -std::optional Info::get_created() const { +std::optional Info::get_created() const { if (auto* ts = data["c"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } +void Info::set_delete_before(std::chrono::sys_seconds timestamp) { + set_ts(data["d"], timestamp); +} + void Info::set_delete_before(int64_t timestamp) { - set_positive_int(data["d"], to_epoch_seconds(timestamp)); + set_delete_before(to_sys_seconds(timestamp)); } -std::optional Info::get_delete_before() const { +std::optional Info::get_delete_before() const { if (auto* ts = data["d"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } +void Info::set_delete_attach_before(std::chrono::sys_seconds timestamp) { + set_ts(data["D"], timestamp); +} + void Info::set_delete_attach_before(int64_t timestamp) { - set_positive_int(data["D"], to_epoch_seconds(timestamp)); + set_delete_attach_before(to_sys_seconds(timestamp)); } -std::optional Info::get_delete_attach_before() const { +std::optional Info::get_delete_attach_before() const { if (auto* ts = data["D"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } @@ -306,7 +319,11 @@ LIBSESSION_C_API void groups_info_set_expiry_timer(config_object* conf, int expi /// Outputs: /// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { - return unbox(conf)->get_created().value_or(0); + return unbox(conf) + ->get_created() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_created @@ -318,7 +335,7 @@ LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { - unbox(conf)->set_created(std::max(0, ts)); + unbox(conf)->set_created(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_get_delete_before @@ -332,7 +349,11 @@ LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { /// Outputs: /// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_before().value_or(0); + return unbox(conf) + ->get_delete_before() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_delete_before @@ -344,7 +365,7 @@ LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_before(std::max(0, ts)); + unbox(conf)->set_delete_before(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_get_attach_delete_before @@ -358,7 +379,11 @@ LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t /// Outputs: /// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_attach_before().value_or(0); + return unbox(conf) + ->get_delete_attach_before() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_attach_delete_before @@ -370,7 +395,7 @@ LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_objec /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_attach_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_attach_before(std::max(0, ts)); + unbox(conf)->set_delete_attach_before(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_is_destroyed(const config_object* conf); diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 7c8bf724..3fa7f357 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -6,16 +6,12 @@ #include #include -#include #include -#include #include #include "internal.hpp" -#include "session/config/error.h" #include "session/config/user_groups.h" #include "session/export.h" -#include "session/types.hpp" #include "session/util.hpp" using namespace std::literals; @@ -34,18 +30,18 @@ namespace session::config { template static void base_into(const base_group_info& self, T& c) { c.priority = self.priority; - c.joined_at = to_epoch_seconds(self.joined_at); + c.joined_at = self.joined_at.time_since_epoch().count(); c.notifications = static_cast(self.notifications); - c.mute_until = to_epoch_seconds(self.mute_until); + c.mute_until = self.mute_until.time_since_epoch().count(); c.invited = self.invited; } template static void base_from(base_group_info& self, const T& c) { self.priority = c.priority; - self.joined_at = to_epoch_seconds(c.joined_at); + self.joined_at = to_sys_seconds(c.joined_at); self.notifications = static_cast(c.notifications); - self.mute_until = to_epoch_seconds(c.mute_until); + self.mute_until = to_sys_seconds(c.mute_until); self.invited = c.invited; } @@ -127,7 +123,9 @@ void legacy_group_info::into(ugroups_legacy_group_info& c) && { void base_group_info::load(const dict& info_dict) { priority = int_or_0(info_dict, "+"); - joined_at = to_epoch_seconds(std::max(0, int_or_0(info_dict, "j"))); + // This value could have been accidentally stored in ms by a previous version, so pass it + // through to_sys_seconds: + joined_at = to_sys_seconds(std::max(0, int_or_0(info_dict, "j"))); int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) @@ -135,7 +133,9 @@ void base_group_info::load(const dict& info_dict) { else notifications = notify_mode::defaulted; - mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); + // This value could have been accidentally stored in ms by a previous version, so pass it + // through to_sys_seconds: + mute_until = to_sys_seconds(int_or_0(info_dict, "!")); invited = int_or_0(info_dict, "i"); } @@ -406,9 +406,9 @@ void UserGroups::set(const community_info& c) { void UserGroups::set_base(const base_group_info& bg, DictFieldProxy& info) const { set_nonzero_int(info["+"], bg.priority); - set_positive_int(info["j"], to_epoch_seconds(bg.joined_at)); + set_ts(info["j"], bg.joined_at); set_positive_int(info["@"], static_cast(bg.notifications)); - set_positive_int(info["!"], to_epoch_seconds(bg.mute_until)); + set_ts(info["!"], bg.mute_until); set_flag(info["i"], bg.invited); // We don't set n here because it's subtly different in the three group types } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index e30ac306..835ec486 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -55,9 +55,9 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); CHECK_FALSE(c.profile_picture); - CHECK(c.created == 0); + CHECK(c.created.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); CHECK_FALSE(contacts.needs_push()); CHECK_FALSE(contacts.needs_dump()); @@ -68,9 +68,9 @@ TEST_CASE("Contacts", "[config][contacts]") { c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; - c.created = created_ts * 1'000; + c.created = session::to_sys_seconds(created_ts * 1'000); // test setting ms c.notifications = session::config::notify_mode::all; - c.mute_until = (now + 1800) * 1'000'000; + c.mute_until = session::to_sys_seconds((now + 1800) * 1'000'000); // test setting us contacts.set(c); @@ -116,9 +116,9 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); CHECK_FALSE(x->blocked); - CHECK(x->created == created_ts); + CHECK(x->created.time_since_epoch() == created_ts * 1s); CHECK(x->notifications == session::config::notify_mode::all); - CHECK(x->mute_until == now + 1800); + CHECK(x->mute_until.time_since_epoch() == (now + 1800) * 1s); auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"sv; auto c2 = contacts2.get_or_construct(another_id); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 502ea132..a1e26273 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/util.hpp" #include "utils.hpp" TEST_CASE("Conversations", "[config][conversations]") { @@ -48,15 +49,15 @@ TEST_CASE("Conversations", "[config][conversations]") { auto c = convos.get_or_construct_1to1(definitely_real_id); CHECK(c.session_id == definitely_real_id); - CHECK(c.last_read == 0); + CHECK(c.last_read.time_since_epoch() == 0s); CHECK_FALSE(convos.needs_push()); CHECK_FALSE(convos.needs_dump()); CHECK(std::get(convos.push()) == 0); - auto now_ms = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); + auto now_ms = std::chrono::time_point_cast( + std::chrono::system_clock::now()); + static_assert(std::same_as); c.last_read = now_ms; @@ -88,7 +89,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto g = convos.get_or_construct_group(benders_nightmare_group); CHECK(g.id == benders_nightmare_group); - CHECK(g.last_read == 0); + CHECK(g.last_read.time_since_epoch() == 0s); CHECK_FALSE(g.unread); g.last_read = now_ms; @@ -100,7 +101,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto lb = convos.get_or_construct_blinded_1to1(legacy_blinded_id, true); CHECK(lb.blinded_session_id == legacy_blinded_id); - CHECK(lb.last_read == 0); + CHECK(lb.last_read.time_since_epoch() == 0s); CHECK_FALSE(lb.unread); lb.last_read = now_ms; @@ -109,7 +110,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto b = convos.get_or_construct_blinded_1to1(blinded_id, false); CHECK(b.blinded_session_id == blinded_id); - CHECK(b.last_read == 0); + CHECK(b.last_read.time_since_epoch() == 0s); CHECK_FALSE(b.unread); b.last_read = now_ms; @@ -174,7 +175,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto c3 = convos2.get_or_construct_legacy_group( "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); - c3.last_read = now_ms - 50; + c3.last_read = now_ms - 50ms; convos2.set(c3); auto c4 = convos2.get_or_construct_blinded_1to1( @@ -605,29 +606,26 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { auto pk = some_pubkey(x); return "05" + oxenc::to_hex(pk.begin(), pk.end()); }; - const auto now = std::chrono::system_clock::now() - 1ms; - auto unix_timestamp = [&now](int days_ago) -> int64_t { - return std::chrono::duration_cast( - (now - days_ago * 24h).time_since_epoch()) - .count(); - }; + const auto now = std::chrono::time_point_cast( + std::chrono::system_clock::now()) - + 1ms; for (int i = 0; i <= 65; i++) { if (i % 3 == 0) { auto c = convos.get_or_construct_1to1(some_session_id(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); } else if (i % 3 == 1) { auto c = convos.get_or_construct_legacy_group(some_session_id(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); } else { auto c = convos.get_or_construct_community( "https://example.org", "room{}"_format(i), some_pubkey(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); @@ -652,13 +650,19 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { // internals like this!) // These ones wouldn't be stored by the normal `set()` interface, but won't get pruned either: - convos.data["1"][oxenc::from_hex(some_session_id(80))]["r"] = unix_timestamp(33); - convos.data["1"][oxenc::from_hex(some_session_id(81))]["r"] = unix_timestamp(40); - convos.data["1"][oxenc::from_hex(some_session_id(82))]["r"] = unix_timestamp(44); + convos.data["1"][oxenc::from_hex(some_session_id(80))]["r"] = + (now - 33 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(81))]["r"] = + (now - 40 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(82))]["r"] = + (now - 44 * 24h).time_since_epoch().count(); // These ones should get pruned as soon as we push: - convos.data["1"][oxenc::from_hex(some_session_id(83))]["r"] = unix_timestamp(45); - convos.data["1"][oxenc::from_hex(some_session_id(84))]["r"] = unix_timestamp(46); - convos.data["1"][oxenc::from_hex(some_session_id(85))]["r"] = unix_timestamp(1000); + convos.data["1"][oxenc::from_hex(some_session_id(83))]["r"] = + (now - 45 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(84))]["r"] = + (now - 46 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(85))]["r"] = + (now - 1000 * 24h).time_since_epoch().count(); CHECK(convos.size_1to1() == 19); int count = 0; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index db94916b..be738a1f 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -4,12 +4,12 @@ #include #include -#include #include #include #include #include "session/config/notify.hpp" +#include "session/util.hpp" #include "utils.hpp" static constexpr int64_t created_ts = 1680064059; @@ -117,9 +117,9 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(c.priority == 0); CHECK(c.name == ""); CHECK(c.members().empty()); - CHECK(c.joined_at == 0); + CHECK(c.joined_at.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); CHECK_FALSE(groups.needs_push()); CHECK_FALSE(groups.needs_dump()); @@ -136,9 +136,9 @@ TEST_CASE("User Groups", "[config][groups]") { c.name = "Englishmen"; c.disappearing_timer = 60min; - c.joined_at = created_ts * 1000; // milliseconds + c.joined_at = session::to_sys_seconds(created_ts * 1000); // milliseconds c.notifications = session::config::notify_mode::mentions_only; - c.mute_until = now + 3600; + c.mute_until = session::to_sys_seconds(now + 3600); CHECK(c.insert(users[0], false)); CHECK(c.insert(users[1], true)); CHECK(c.insert(users[2], false)); @@ -243,9 +243,9 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(c1.priority == 3); CHECK(c1.members() == expected_members); CHECK(c1.name == "Englishmen"); - CHECK(c1.joined_at == created_ts); + CHECK(c1.joined_at.time_since_epoch() == created_ts * 1s); CHECK(c1.notifications == session::config::notify_mode::mentions_only); - CHECK(c1.mute_until == now + 3600); + CHECK(c1.mute_until.time_since_epoch() == (now + 3600) * 1s); CHECK_FALSE(g2.needs_push()); CHECK_FALSE(g2.needs_dump()); @@ -457,9 +457,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c.secretkey.empty()); CHECK(c.id == definitely_real_id); CHECK(c.priority == 0); - CHECK(c.joined_at == 0); + CHECK(c.joined_at.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); c.secretkey = session::to_vector(ed_sk); // This *isn't* the right secret key for the group, so // won't propagate, and so auth data will: @@ -485,16 +485,16 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c2->id == definitely_real_id); CHECK(c2->priority == 0); - CHECK(c2->joined_at == 0); + CHECK(c2->joined_at.time_since_epoch() == 0s); CHECK(c2->notifications == session::config::notify_mode::defaulted); - CHECK(c2->mute_until == 0); + CHECK(c2->mute_until.time_since_epoch() == 0s); CHECK_FALSE(c2->invited); CHECK(c2->name == ""); c2->priority = 123; - c2->joined_at = (int64_t)1'234'567'890 * 1'000; + c2->joined_at = session::to_sys_seconds((int64_t)1'234'567'890 * 1'000); // ms c2->notifications = session::config::notify_mode::mentions_only; - c2->mute_until = (int64_t)456'789'012 * 1'000'000; + c2->mute_until = session::to_sys_seconds((int64_t)456'789'012 * 1'000'000); // µs c2->invited = true; c2->name = "Magic Special Room"; @@ -526,9 +526,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { "0000000000000000000000000000"); CHECK(c3->id == definitely_real_id); CHECK(c3->priority == 123); - CHECK(c3->joined_at == 1234567890); + CHECK(c3->joined_at.time_since_epoch() == 1234567890s); CHECK(c3->notifications == session::config::notify_mode::mentions_only); - CHECK(c3->mute_until == 456789012); + CHECK(c3->mute_until.time_since_epoch() == 456789012s); CHECK(c3->invited); CHECK(c3->name == "Magic Special Room"); @@ -731,7 +731,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { auto grp = c2.get_legacy_group(definitely_real_id); REQUIRE(grp); CHECK(grp->members() == expected_members); - CHECK(grp->joined_at == created_ts); + CHECK(grp->joined_at.time_since_epoch() == created_ts * 1s); } TEST_CASE("User groups empty member bug", "[config][groups][bug]") { @@ -843,30 +843,35 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro { auto lg = c.get_or_construct_legacy_group( "051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - int64_t joined_at = get_timestamp_us(); - int64_t mute_until = get_timestamp_s(); - lg.joined_at = joined_at; - lg.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_us(); + int64_t mute_until_raw = get_timestamp_s(); + auto joined_at = joined_at_raw * 1us; + auto mute_until = mute_until_raw * 1s; + lg.joined_at = session::to_sys_seconds(joined_at_raw); // µs + lg.mute_until = session::to_sys_seconds(mute_until_raw); // s c.set(lg); auto lg2 = c.get_or_construct_legacy_group( "051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(lg2.joined_at == joined_at / 1'000'000); // joined_at was given in microseconds - CHECK(lg2.mute_until == mute_until); // mute_until was given in seconds + CHECK(lg2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); + CHECK(lg2.mute_until.time_since_epoch() == mute_until); c.erase_legacy_group("051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); } { auto gr = c.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - int64_t joined_at = get_timestamp_ms(); - int64_t mute_until = get_timestamp_us(); - gr.joined_at = joined_at; - gr.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_ms(); + int64_t mute_until_raw = get_timestamp_us(); + auto joined_at = joined_at_raw * 1ms; + auto mute_until = mute_until_raw * 1us; + gr.joined_at = session::to_sys_seconds(joined_at_raw); // ms + gr.mute_until = session::to_sys_seconds(mute_until_raw); // µs c.set(gr); auto gr2 = c.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(gr2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds - CHECK(gr2.mute_until == mute_until / 1'000'000); // mute_until was given in microseconds + // Non-whole second timestamp components should have been truncate: + CHECK(gr2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); + CHECK(gr2.mute_until.time_since_epoch() == mute_until - mute_until % 1s); c.erase_group("031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); } @@ -876,14 +881,16 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro const auto url = "http://example.org:5678"; const auto room = "sudoku_room"; auto comm = c.get_or_construct_community(url, room, open_group_pubkey); - int64_t joined_at = get_timestamp_ms(); - int64_t mute_until = get_timestamp_ms(); - comm.joined_at = joined_at; - comm.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_ms(); + int64_t mute_until_raw = get_timestamp_ms(); + auto joined_at = joined_at_raw * 1ms; + auto mute_until = mute_until_raw * 1ms; + comm.joined_at = session::to_sys_seconds(joined_at_raw); + comm.mute_until = session::to_sys_seconds(mute_until_raw); c.set(comm); auto comm2 = c.get_or_construct_community(url, room, open_group_pubkey); - CHECK(comm2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds - CHECK(comm2.mute_until == mute_until / 1'000); // mute_until was given in milliseconds + CHECK(comm2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); // ms + CHECK(comm2.mute_until.time_since_epoch() == mute_until - mute_until % 1s); // ms c.erase_community(url, room); } { @@ -891,24 +898,19 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro // - an invalid joined_at (1'733'979'503'520) and // - an invalid mute_until (1'733'979'503'520'780) values const auto dump_with_not_seconds = - "64313a21693165313a243231303a64313a23693165313a2664313a676433333a031234567890abcdef" - "1234" - "567890abcdef1234567890abcdef1234567890abcdef64313a21693137333339373935303335323037" - "3830" - "65313a4b303a313a6a693137333339373935303335323065656565313a3c6c6c69306533323aea173b" - "57be" - "ca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d64313a67643333" - "3a03" - "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef64313a21303a313a4b" - "303a" - "313a6a303a65656565313a28303a313a296c6565"_hexbytes; + "64313a21693165313a243231303a64313a23693165313a2664313a676433333a031234567890abcd" + "ef1234567890abcdef1234567890abcdef1234567890abcdef64313a216931373333393739353033" + "35323037383065313a4b303a313a6a693137333339373935303335323065656565313a3c6c6c6930" + "6533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565" + "313a3d64313a676433333a031234567890abcdef1234567890abcdef1234567890abcdef12345678" + "90abcdef64313a21303a313a4b303a313a6a303a65656565313a28303a313a296c6565"_hexbytes; session::config::UserGroups c2{std::span{seed}, dump_with_not_seconds}; auto gr = c2.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(gr.joined_at == 1'733'979'503'520 / 1'000); - CHECK(gr.mute_until == 1'733'979'503'520'780 / 1'000'000); + CHECK(gr.joined_at.time_since_epoch() == 1'733'979'503'520ms - 520ms); + CHECK(gr.mute_until.time_since_epoch() == 1'733'979'503'520'780us - 520'780us); } } diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 4dfea5c1..638786c7 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -76,9 +76,11 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); ginfo2.set_expiry_timer(1h); constexpr int64_t create_time{1682529839}; - ginfo2.set_created(create_time); - ginfo2.set_delete_before((create_time + 50 * 86400) * 1'000'000); // as microseconds - ginfo2.set_delete_attach_before((create_time + 70 * 86400) * 1'000); // as milliseconds + ginfo2.set_created(session::to_sys_seconds(create_time)); + // µs: + ginfo2.set_delete_before(session::to_sys_seconds((create_time + 50 * 86400) * 1'000'000)); + // ms: + ginfo2.set_delete_attach_before(session::to_sys_seconds((create_time + 70 * 86400) * 1'000)); ginfo2.destroy_group(); auto [s2, p2, o2] = ginfo2.push(); @@ -106,14 +108,18 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.needs_push()); auto [s3, p3, o3] = ginfo1.push(); + constexpr std::chrono::sys_seconds expected_created{create_time * 1s}; + constexpr std::chrono::sys_seconds expected_del_before{create_time * 1s + 50 * 24h}; + constexpr std::chrono::sys_seconds expected_del_attach{create_time * 1s + 70 * 24h}; + CHECK(ginfo1.get_name() == "Better name!"); CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); CHECK(ginfo1.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo1.get_expiry_timer() == 1h); - CHECK(ginfo1.get_created() == create_time); - CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400); - CHECK(ginfo1.get_delete_attach_before() == create_time + 70 * 86400); + CHECK(ginfo1.get_created() == expected_created); + CHECK(ginfo1.get_delete_before() == expected_del_before); + CHECK(ginfo1.get_delete_attach_before() == expected_del_attach); CHECK(ginfo1.is_destroyed()); ginfo1.confirm_pushed(s3, {"fakehash3"}); @@ -126,9 +132,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo2.get_expiry_timer() == 1h); - CHECK(ginfo2.get_created() == create_time); - CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); - CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); + CHECK(ginfo2.get_created() == expected_created); + CHECK(ginfo2.get_delete_before() == expected_del_before); + CHECK(ginfo2.get_delete_attach_before() == expected_del_attach); CHECK(ginfo2.is_destroyed()); CHECK_THROWS( @@ -245,7 +251,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // Now let's get more complicated: we will have *two* valid signers who submit competing updates ginfo_rw2.set_name("Super Group 2"); - ginfo_rw2.set_created(12345); + ginfo_rw2.set_created(session::to_sys_seconds(12345)); ginfo_rw.set_name("Super Group 3"); ginfo_rw.set_expiry_timer(365 * 24h); @@ -299,7 +305,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(*n == "Super Group 2"); auto c = g.get_created(); REQUIRE(c); - CHECK(*c == 12345); + CHECK(c->time_since_epoch() == 12345s); auto et = g.get_expiry_timer(); REQUIRE(et); CHECK(*et == 365 * 24h);