diff --git a/.drone.jsonnet b/.drone.jsonnet index e8701988..9477df61 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -357,7 +357,7 @@ local static_build(name, clang(17), full_llvm(17), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), - debian_build('Debian 12', docker_base + 'debian-bookworm', extra_setup=debian_backports('bookworm', ['cmake'])), + debian_build('Debian 12', docker_base + 'debian-bookworm'), debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index e2752153..99c1d818 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -20,6 +20,7 @@ typedef struct contacts_contact { char name[101]; char nickname[101]; user_profile_pic profile_pic; + int64_t profile_updated; // unix timestamp (seconds) bool approved; bool approved_me; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 757e6cd0..9c265c18 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -44,6 +44,7 @@ namespace session::config { /// E - Disappearing message timer, in seconds. Omitted when `e` is omitted. /// j - Unix timestamp (seconds) when the contact was created ("j" to match user_groups /// equivalent "j"oined field). Omitted if 0. +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. /// Struct containing contact info. struct contact_info { @@ -53,6 +54,8 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; + std::chrono::sys_seconds profile_updated{}; /// The unix timestamp (seconds) that this + /// profile information was last updated. bool approved = false; bool approved_me = false; bool blocked = false; @@ -230,6 +233,17 @@ class Contacts : public ConfigBase { /// - `profile_pic` -- profile pic of the contact void set_profile_pic(std::string_view session_id, profile_pic pic); + /// API: contacts/contacts::set_profile_updated + /// + /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you + /// should use `set()` instead). + /// + /// Inputs: + /// - `session_id` -- hex string of the session id + /// - `profile_updated` -- profile updated unix timestamp (seconds) of the contact. (To convert + /// a raw s/ms/µs integer value, use session::to_sys_seconds). + void set_profile_updated(std::string_view session_id, std::chrono::sys_seconds profile_updated); + /// API: contacts/contacts::set_approved /// /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index d502fbe2..da07bbfb 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -38,6 +38,7 @@ typedef struct config_group_member { // These two will be 0-length strings when unset: char name[101]; user_profile_pic profile_pic; + int64_t profile_updated; // unix timestamp (seconds) bool admin; int invited; // 0 == unset, STATUS_SENT = invited, STATUS_FAILED = invite failed to send, diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index d35fa52c..0ea32b00 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -40,6 +40,7 @@ using namespace std::literals; /// resent) /// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet. /// - omitted once the promotion is accepted (i.e. once `A` gets set). +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. constexpr int STATUS_SENT = 1, STATUS_FAILED = 2, STATUS_NOT_SENT = 3; constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; @@ -100,6 +101,13 @@ struct member { /// member. profile_pic profile_picture; + /// API: groups/member::profile_updated + /// + /// Member variable + /// + /// The unix timestamp (seconds) that this profile information was last updated. + std::chrono::sys_seconds profile_updated{}; + /// API: groups/member::admin /// /// Member variable diff --git a/include/session/util.hpp b/include/session/util.hpp index 2cd85840..fc57908f 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -257,4 +257,23 @@ inline int64_t to_epoch_seconds(int64_t timestamp) { : timestamp; } +// 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) { + return std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; +} + +// Helper function to transform a timestamp integer that might be seconds, milliseconds or +// microseconds to typesafe system clock seconds unix timestamp. +inline std::chrono::sys_seconds to_sys_seconds(int64_t timestamp) { + if (timestamp > 9'000'000'000'000) + timestamp /= 1'000'000; + else if (timestamp > 9'000'000'000) + timestamp /= 1'000; + return as_sys_seconds(timestamp); +} + +static_assert(std::is_same_v< + std::chrono::seconds, + decltype(std::declval().time_since_epoch())>); + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 093c0a9c..20ef0818 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -71,8 +71,8 @@ LIBSESSION_C_API int contacts_init( } void contact_info::load(const dict& info_dict) { - name = maybe_string(info_dict, "n").value_or(""); - nickname = maybe_string(info_dict, "N").value_or(""); + name = string_or_empty(info_dict, "n"); + nickname = string_or_empty(info_dict, "N"); auto url = maybe_string(info_dict, "p"); auto key = maybe_vector(info_dict, "q"); @@ -83,13 +83,14 @@ void contact_info::load(const dict& info_dict) { profile_picture.clear(); } - approved = maybe_int(info_dict, "a").value_or(0); - approved_me = maybe_int(info_dict, "A").value_or(0); - blocked = maybe_int(info_dict, "b").value_or(0); + profile_updated = ts_or_epoch(info_dict, "t"); + approved = int_or_0(info_dict, "a"); + approved_me = int_or_0(info_dict, "A"); + blocked = int_or_0(info_dict, "b"); - priority = maybe_int(info_dict, "+").value_or(0); + priority = int_or_0(info_dict, "+"); - int notify = maybe_int(info_dict, "@").value_or(0); + int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) { notifications = static_cast(notify); if (notifications == notify_mode::mentions_only) @@ -97,9 +98,9 @@ void contact_info::load(const dict& info_dict) { } else { notifications = notify_mode::defaulted; } - mute_until = to_epoch_seconds(maybe_int(info_dict, "!").value_or(0)); + mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); - int exp_mode_ = maybe_int(info_dict, "e").value_or(0); + int exp_mode_ = int_or_0(info_dict, "e"); if (exp_mode_ >= static_cast(expiration_mode::none) && exp_mode_ <= static_cast(expiration_mode::after_read)) exp_mode = static_cast(exp_mode_); @@ -109,7 +110,7 @@ void contact_info::load(const dict& info_dict) { if (exp_mode == expiration_mode::none) exp_timer = 0s; else { - int secs = maybe_int(info_dict, "E").value_or(0); + int secs = int_or_0(info_dict, "E"); if (secs <= 0) { exp_mode = expiration_mode::none; exp_timer = 0s; @@ -118,7 +119,7 @@ void contact_info::load(const dict& info_dict) { } } - created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); + created = to_epoch_seconds(int_or_0(info_dict, "j")); } void contact_info::into(contacts_contact& c) const { @@ -131,6 +132,7 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } + c.profile_updated = profile_updated.time_since_epoch().count(); c.approved = approved; c.approved_me = approved_me; c.blocked = blocked; @@ -154,6 +156,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, profile_picture.url = c.profile_pic.url; profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); } + profile_updated = to_sys_seconds(c.profile_updated); approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; @@ -227,6 +230,8 @@ void Contacts::set(const contact_info& contact) { info["q"], contact.profile_picture.key); + set_ts(info["t"], contact.profile_updated); + set_flag(info["a"], contact.approved); set_flag(info["A"], contact.approved_me); set_flag(info["b"], contact.blocked); @@ -279,6 +284,12 @@ void Contacts::set_profile_pic(std::string_view session_id, profile_pic pic) { c.profile_picture = std::move(pic); set(c); } +void Contacts::set_profile_updated( + std::string_view session_id, std::chrono::sys_seconds profile_updated) { + auto c = get_or_construct(session_id); + c.profile_updated = profile_updated; + set(c); +} void Contacts::set_approved(std::string_view session_id, bool approved) { auto c = get_or_construct(session_id); c.approved = approved; diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 8d1206c7..14e291ad 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -83,8 +83,8 @@ namespace convo { } void base::load(const dict& info_dict) { - last_read = maybe_int(info_dict, "r").value_or(0); - unread = (bool)maybe_int(info_dict, "u").value_or(0); + last_read = int_or_0(info_dict, "r"); + unread = (bool)int_or_0(info_dict, "u"); } } // namespace convo diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index ca515e66..86c3b085 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -66,6 +66,7 @@ void Members::set(const member& mem) { info["q"], mem.profile_picture.key); + set_ts(info["t"], mem.profile_updated); set_flag(info["A"], mem.admin); set_positive_int(info["P"], mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); @@ -84,7 +85,7 @@ void Members::set(const member& mem) { } void member::load(const dict& info_dict) { - name = maybe_string(info_dict, "n").value_or(""); + name = string_or_empty(info_dict, "n"); auto url = maybe_string(info_dict, "p"); auto key = maybe_vector(info_dict, "q"); @@ -95,13 +96,13 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } - admin = maybe_int(info_dict, "A").value_or(0); - invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); - promotion_status = maybe_int(info_dict, "P").value_or(0); - removed_status = maybe_int(info_dict, "R").value_or(0); - supplement = invite_status > 0 && !(admin || promotion_status > 0) - ? maybe_int(info_dict, "s").value_or(0) - : 0; + profile_updated = ts_or_epoch(info_dict, "t"); + admin = int_or_0(info_dict, "A"); + invite_status = admin ? 0 : int_or_0(info_dict, "I"); + promotion_status = int_or_0(info_dict, "P"); + removed_status = int_or_0(info_dict, "R"); + supplement = + invite_status > 0 && !(admin || promotion_status > 0) ? int_or_0(info_dict, "s") : 0; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -187,6 +188,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { profile_picture.url = m.profile_pic.url; profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32); } + profile_updated = to_sys_seconds(m.profile_updated); admin = m.admin; invite_status = (m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT) @@ -211,6 +213,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } + m.profile_updated = profile_updated.time_since_epoch().count(); m.admin = admin; static_assert(groups::STATUS_SENT == ::STATUS_SENT); static_assert(groups::STATUS_FAILED == ::STATUS_FAILED); diff --git a/src/config/internal.cpp b/src/config/internal.cpp index a81446ef..0b2051dc 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -81,18 +81,49 @@ std::optional maybe_int(const session::config::dict& d, const char* key return std::nullopt; } +int64_t int_or_0(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return *i; + return 0; +} + +std::optional maybe_ts(const session::config::dict& d, const char* key) { + std::optional result; + if (auto* i = maybe_scalar(d, key)) + result.emplace(std::chrono::seconds{*i}); + return result; +} + +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return std::chrono::sys_seconds{std::chrono::seconds{*i}}; + return std::chrono::sys_seconds{}; +} + std::optional maybe_string(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string string_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""s; +} + std::optional maybe_sv(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string_view sv_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""sv; +} + std::optional> maybe_vector( const session::config::dict& d, const char* key) { std::optional> result; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 74cc31fd..337523c5 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -147,18 +147,38 @@ const config::set* maybe_set(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out an int64_t; nullopt if not there (or not int) std::optional maybe_int(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out an int64_t; returns 0 if the value is not there or not an +// int. Equivalent to `maybe_int(d, key).value_or(0)`. +int64_t int_or_0(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out an int64_t containing unix timestamp seconds, returns it +// wrapped in a std::chrono::sys_seconds. Returns nullopt if not there (or not int). +std::optional maybe_ts(const session::config::dict& d, const char* key); + +// Works like maybe_ts, except that if the value isn't present it returns a default-constructed +// sys_seconds (i.e. unix timestamp 0). Equivalent to `maybe_ts(d, +// key).value_or(std::chrono::sys_seconds{})`. +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key); + // Digs into a config `dict` to get out a string; nullopt if not there (or not string) std::optional maybe_string(const session::config::dict& d, const char* key); -// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not -// string) -std::optional> maybe_vector( - const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string; ""s if not there (or not string) +std::string string_or_empty(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out a string view; nullopt if not there (or not string). The // string view is only valid as long as the dict stays unchanged. std::optional maybe_sv(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string view; ""sv if not there (or not string). The +// string view is only valid as long as the dict stays unchanged. +std::string_view sv_or_empty(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not +// string) +std::optional> maybe_vector( + const session::config::dict& d, const char* key); + /// Sets a value to 1 if true, removes it if false. void set_flag(ConfigBase::DictFieldProxy&& field, bool val); @@ -172,6 +192,11 @@ void set_nonzero_int(ConfigBase::DictFieldProxy&& field, int64_t val); /// Sets an integer value, if positive; removes it if <= 0. void set_positive_int(ConfigBase::DictFieldProxy&& field, int64_t val); +/// Sets a unix timestamp as an integer, if positive; removes it if <= 0. +inline void set_ts(ConfigBase::DictFieldProxy&& field, std::chrono::sys_seconds val) { + set_positive_int(std::move(field), val.time_since_epoch().count()); +} + /// Sets a pair of values if the given condition is satisfied, clears both values otherwise. template void set_pair_if( diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index a93da2a5..7c8bf724 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -126,18 +126,18 @@ void legacy_group_info::into(ugroups_legacy_group_info& c) && { } void base_group_info::load(const dict& info_dict) { - priority = maybe_int(info_dict, "+").value_or(0); - joined_at = to_epoch_seconds(std::max(0, maybe_int(info_dict, "j").value_or(0))); + priority = int_or_0(info_dict, "+"); + joined_at = to_epoch_seconds(std::max(0, int_or_0(info_dict, "j"))); - int notify = maybe_int(info_dict, "@").value_or(0); + int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) notifications = static_cast(notify); else notifications = notify_mode::defaulted; - mute_until = to_epoch_seconds(maybe_int(info_dict, "!").value_or(0)); + mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); - invited = maybe_int(info_dict, "i").value_or(0); + invited = int_or_0(info_dict, "i"); } void legacy_group_info::load(const dict& info_dict) { @@ -157,10 +157,7 @@ void legacy_group_info::load(const dict& info_dict) { enc_pubkey.clear(); enc_seckey.clear(); } - if (auto secs = maybe_int(info_dict, "E").value_or(0); secs > 0) - disappearing_timer = std::chrono::seconds{secs}; - else - disappearing_timer = 0s; + disappearing_timer = std::max(0s, std::chrono::seconds{int_or_0(info_dict, "E")}); members_.clear(); if (auto* members = maybe_set(info_dict, "m")) @@ -244,7 +241,7 @@ void group_info::load(const dict& info_dict) { if (auto sig = maybe_vector(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); - removed_status = maybe_int(info_dict, "r").value_or(0); + removed_status = int_or_0(info_dict, "r"); } void group_info::mark_kicked() { diff --git a/src/util.cpp b/src/util.cpp index 7669d0e1..60409c58 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -87,4 +87,8 @@ std::tuple, std::optional().time_since_epoch())>); + } // namespace session diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 06a55166..1b2286d2 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include #include @@ -48,6 +50,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); + CHECK(c.profile_updated == std::chrono::sys_seconds{}); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -62,6 +65,7 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); + c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; c.created = created_ts * 1'000; @@ -74,6 +78,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(contacts.get(definitely_real_id)->name == "Joe"); CHECK(contacts.get(definitely_real_id)->nickname == "Joey"); + CHECK(contacts.get(definitely_real_id)->profile_updated.time_since_epoch() == 1s); CHECK(contacts.get(definitely_real_id)->approved); CHECK(contacts.get(definitely_real_id)->approved_me); CHECK_FALSE(contacts.get(definitely_real_id)->profile_picture); @@ -106,6 +111,7 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); + CHECK(x->profile_updated.time_since_epoch() == 1s); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); @@ -137,11 +143,13 @@ TEST_CASE("Contacts", "[config][contacts]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts.size() == 2); CHECK_FALSE(contacts.empty()); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); @@ -150,6 +158,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0].time_since_epoch() == 1s); + CHECK(profile_updateds[1].time_since_epoch() == 0s); // Conflict! Oh no! @@ -159,6 +169,7 @@ TEST_CASE("Contacts", "[config][contacts]") { // Client 2 adds a new friend: auto third_id = "052222222222222222222222222222222222222222222222222222222222222222"sv; contacts2.set_nickname(third_id, "Nickname 3"); + contacts2.set_profile_updated(third_id, session::to_sys_seconds(2)); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -216,15 +227,19 @@ TEST_CASE("Contacts", "[config][contacts]") { session_ids.clear(); nicknames.clear(); + profile_updateds.clear(); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); CHECK(session_ids[0] == another_id); CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); + CHECK(profile_updateds[0].time_since_epoch() == 0s); + CHECK(profile_updateds[1].time_since_epoch() == 2s); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" @@ -279,6 +294,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(strlen(c.name) == 0); CHECK(strlen(c.nickname) == 0); + CHECK(c.profile_updated == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -287,6 +303,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { strcpy(c.name, "Joe"); strcpy(c.nickname, "Joey"); + c.profile_updated = 1; c.approved = true; c.approved_me = true; c.created = created_ts; @@ -298,6 +315,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); + CHECK(c2.profile_updated == 1); CHECK(c2.approved); CHECK(c2.approved_me); CHECK_FALSE(c2.blocked); @@ -333,6 +351,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get(conf2, &c3, definitely_real_id)); CHECK(c3.name == "Joe"sv); CHECK(c3.nickname == "Joey"sv); + CHECK(c3.profile_updated == 1); CHECK(c3.approved); CHECK(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -343,6 +362,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get_or_construct(conf, &c3, another_id)); CHECK(strlen(c3.name) == 0); CHECK(strlen(c3.nickname) == 0); + CHECK(c3.profile_updated == 0); CHECK_FALSE(c3.approved); CHECK_FALSE(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -372,6 +392,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts_size(conf) == 2); contacts_iterator* it = contacts_iterator_new(conf); @@ -379,6 +400,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { session_ids.push_back(ci.session_id); nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); + profile_updateds.emplace_back(ci.profile_updated); } contacts_iterator_free(it); @@ -387,6 +409,8 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0] == 1); + CHECK(profile_updateds[1] == 0); // Changing things while iterating: it = contacts_iterator_new(conf); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 072d105c..017abe75 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include @@ -72,6 +72,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + m.profile_updated = std::chrono::sys_seconds{1s}; gmem1.set(m); } // 10 members: @@ -81,6 +82,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + m.profile_updated = session::to_sys_seconds(2); gmem1.set(m); } // 5 members with no attributes (not even a name): @@ -131,6 +133,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { session::config::groups::member::Status::invite_not_sent); CHECK(m.admin); CHECK(m.name == "Admin {}"_format(i)); + CHECK(m.profile_updated.time_since_epoch() == 1s); CHECK_FALSE(m.profile_picture.empty()); CHECK(gmem2.get_status(m) == session::config::groups::member::Status::promotion_accepted); @@ -144,10 +147,12 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.admin); if (i < 20) { CHECK(m.name == "Member {}"_format(i)); + CHECK(m.profile_updated.time_since_epoch() == 2s); CHECK_FALSE(m.profile_picture.empty()); } else { CHECK(m.name.empty()); CHECK(m.profile_picture.empty()); + CHECK(m.profile_updated.time_since_epoch() == 0s); } } i++; @@ -155,9 +160,15 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(i == 25); } + for (int i = 5; i < 15; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.profile_updated += 1s; + gmem2.set(m); + } for (int i = 22; i < 50; i++) { auto m = gmem2.get_or_construct(sids[i]); m.name = "Member {}"_format(i); + m.profile_updated = std::chrono::sys_seconds{1s}; gmem2.set(m); } for (int i = 50; i < 55; i++) { @@ -211,6 +222,20 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); + if (i < 5) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 5 && i < 10) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 10 && i < 15) + CHECK(m.profile_updated.time_since_epoch() == 3s); + if (i >= 15 && i < 20) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 20 && i < 22) + CHECK(m.profile_updated.time_since_epoch() == 0s); + if (i >= 22 && i < 50) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 50) + CHECK(m.profile_updated.time_since_epoch() == 0s); if (i >= 10 && i < 25) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_sending); @@ -281,6 +306,20 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); + if (i < 5) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 5 && i < 10) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 10 && i < 15) + CHECK(m.profile_updated.time_since_epoch() == 3s); + if (i >= 15 && i < 20) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 20 && i < 22) + CHECK(m.profile_updated.time_since_epoch() == 0s); + if (i >= 22 && i < 50) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 50) + CHECK(m.profile_updated.time_since_epoch() == 0s); if (is_prime100(i) || (i >= 25 && i < 50)) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_not_sent);