From fda6db3128c7fd4d002e32fe06c5f9e3782cb204 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 11 Jul 2025 11:42:46 +1000 Subject: [PATCH 1/7] Added 'profile_seqno' to contact_info & member to version profile info --- include/session/config/contacts.h | 1 + include/session/config/contacts.hpp | 12 +++++++ include/session/config/groups/members.h | 1 + include/session/config/groups/members.hpp | 8 +++++ src/config/contacts.cpp | 9 ++++++ src/config/groups/members.cpp | 4 +++ tests/test_config_contacts.cpp | 22 +++++++++++++ tests/test_group_members.cpp | 39 +++++++++++++++++++++++ 8 files changed, 96 insertions(+) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index e2752153..d14a1e13 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_seqno; bool approved; bool approved_me; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 757e6cd0..4b654f37 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. +/// # - The `profile_seqno` (version number) for this contacts profile information. /// Struct containing contact info. struct contact_info { @@ -53,6 +54,7 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; + int64_t profile_seqno = 0; bool approved = false; bool approved_me = false; bool blocked = false; @@ -230,6 +232,16 @@ 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_seqno + /// + /// 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_seqno` -- profile seqno of the contact + void set_profile_seqno(std::string_view session_id, int64_t profile_seqno); + /// 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..c8ffd40d 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_seqno; 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..9718e96c 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). +/// # - The `profile_seqno` (version number) for this members 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_seqno + /// + /// Member variable + /// + /// The version number for this members profile information. + int64_t profile_seqno = 0; + /// API: groups/member::admin /// /// Member variable diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 093c0a9c..3b8756a0 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -83,6 +83,7 @@ void contact_info::load(const dict& info_dict) { profile_picture.clear(); } + profile_seqno = maybe_int(info_dict, "#").value_or(0); 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); @@ -131,6 +132,7 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } + c.profile_seqno = profile_seqno; 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_seqno = c.profile_seqno; approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; @@ -227,6 +230,7 @@ void Contacts::set(const contact_info& contact) { info["q"], contact.profile_picture.key); + set_positive_int(info["#"], contact.profile_seqno); set_flag(info["a"], contact.approved); set_flag(info["A"], contact.approved_me); set_flag(info["b"], contact.blocked); @@ -279,6 +283,11 @@ 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_seqno(std::string_view session_id, int64_t profile_seqno) { + auto c = get_or_construct(session_id); + c.profile_seqno = profile_seqno; + 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/groups/members.cpp b/src/config/groups/members.cpp index ca515e66..e3a30b18 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_positive_int(info["#"], mem.profile_seqno); 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); @@ -95,6 +96,7 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } + profile_seqno = maybe_int(info_dict, "#").value_or(0); 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); @@ -187,6 +189,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_seqno = m.profile_seqno; admin = m.admin; invite_status = (m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT) @@ -211,6 +214,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } + m.profile_seqno = profile_seqno; m.admin = admin; static_assert(groups::STATUS_SENT == ::STATUS_SENT); static_assert(groups::STATUS_FAILED == ::STATUS_FAILED); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 06a55166..d8625b8e 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -48,6 +48,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); + CHECK(c.profile_seqno == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -62,6 +63,7 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); + c.profile_seqno = 1; c.approved = true; c.approved_me = true; c.created = created_ts * 1'000; @@ -74,6 +76,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_seqno == 1); 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 +109,7 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); + CHECK(x->profile_seqno == 1); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); @@ -137,11 +141,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_seqnos; 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_seqnos.emplace_back(cc.profile_seqno); } REQUIRE(session_ids.size() == 2); @@ -150,6 +156,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_seqnos[0] == 1); + CHECK(profile_seqnos[1] == 0); // Conflict! Oh no! @@ -159,6 +167,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_seqno(third_id, 2); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -216,15 +225,19 @@ TEST_CASE("Contacts", "[config][contacts]") { session_ids.clear(); nicknames.clear(); + profile_seqnos.clear(); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_seqnos.emplace_back(cc.profile_seqno); } 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_seqnos[0] == 0); + CHECK(profile_seqnos[1] == 2); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" @@ -279,6 +292,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_seqno == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -287,6 +301,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { strcpy(c.name, "Joe"); strcpy(c.nickname, "Joey"); + c.profile_seqno = 1; c.approved = true; c.approved_me = true; c.created = created_ts; @@ -298,6 +313,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); + CHECK(c2.profile_seqno == 1); CHECK(c2.approved); CHECK(c2.approved_me); CHECK_FALSE(c2.blocked); @@ -333,6 +349,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_seqno == 1); CHECK(c3.approved); CHECK(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -343,6 +360,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_seqno == 0); CHECK_FALSE(c3.approved); CHECK_FALSE(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -372,6 +390,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_seqnos; CHECK(contacts_size(conf) == 2); contacts_iterator* it = contacts_iterator_new(conf); @@ -379,6 +398,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_seqnos.emplace_back(ci.profile_seqno); } contacts_iterator_free(it); @@ -387,6 +407,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_seqnos[0] == 1); + CHECK(profile_seqnos[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..80dd77b1 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -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_seqno = 1; 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_seqno = 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_seqno == 1); 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_seqno == 2); CHECK_FALSE(m.profile_picture.empty()); } else { CHECK(m.name.empty()); CHECK(m.profile_picture.empty()); + CHECK(m.profile_seqno == 0); } } 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_seqno += 1; + 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_seqno = 1; 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_seqno == 1); + if (i >= 5 && i < 10) + CHECK(m.profile_seqno == 2); + if (i >= 10 && i < 15) + CHECK(m.profile_seqno == 3); + if (i >= 15 && i < 20) + CHECK(m.profile_seqno == 2); + if (i >= 20 && i < 22) + CHECK(m.profile_seqno == 0); + if (i >= 22 && i < 50) + CHECK(m.profile_seqno == 1); + if (i >= 50) + CHECK(m.profile_seqno == 0); 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_seqno == 1); + if (i >= 5 && i < 10) + CHECK(m.profile_seqno == 2); + if (i >= 10 && i < 15) + CHECK(m.profile_seqno == 3); + if (i >= 15 && i < 20) + CHECK(m.profile_seqno == 2); + if (i >= 20 && i < 22) + CHECK(m.profile_seqno == 0); + if (i >= 22 && i < 50) + CHECK(m.profile_seqno == 1); + if (i >= 50) + CHECK(m.profile_seqno == 0); if (is_prime100(i) || (i >= 25 && i < 50)) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_not_sent); From 7d3c68cb6e0d73d654caf3d9540b82f9f48c00ba Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 11 Jul 2025 12:11:54 +1000 Subject: [PATCH 2/7] Changed 'profile_seqno' to 'profile_updated' --- include/session/config/contacts.h | 2 +- include/session/config/contacts.hpp | 10 +++--- include/session/config/groups/members.h | 2 +- include/session/config/groups/members.hpp | 8 ++--- src/config/contacts.cpp | 12 +++---- src/config/groups/members.cpp | 8 ++--- tests/test_config_contacts.cpp | 44 +++++++++++------------ tests/test_group_members.cpp | 42 +++++++++++----------- 8 files changed, 64 insertions(+), 64 deletions(-) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index d14a1e13..99c1d818 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -20,7 +20,7 @@ typedef struct contacts_contact { char name[101]; char nickname[101]; user_profile_pic profile_pic; - int64_t profile_seqno; + 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 4b654f37..dff3d671 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -44,7 +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. -/// # - The `profile_seqno` (version number) for this contacts profile information. +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. /// Struct containing contact info. struct contact_info { @@ -54,7 +54,7 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; - int64_t profile_seqno = 0; + int64_t profile_updated = 0; /// The unix timestamp (seconds) that this profile information was last updated. bool approved = false; bool approved_me = false; bool blocked = false; @@ -232,15 +232,15 @@ 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_seqno + /// 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_seqno` -- profile seqno of the contact - void set_profile_seqno(std::string_view session_id, int64_t profile_seqno); + /// - `profile_updated` -- profile updated unix timestamp (seconds) of the contact + void set_profile_updated(std::string_view session_id, int64_t profile_updated); /// API: contacts/contacts::set_approved /// diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index c8ffd40d..da07bbfb 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -38,7 +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_seqno; + 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 9718e96c..270b94a4 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -40,7 +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). -/// # - The `profile_seqno` (version number) for this members profile information. +/// 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; @@ -101,12 +101,12 @@ struct member { /// member. profile_pic profile_picture; - /// API: groups/member::profile_seqno + /// API: groups/member::profile_updated /// /// Member variable /// - /// The version number for this members profile information. - int64_t profile_seqno = 0; + /// The unix timestamp (seconds) that this profile information was last updated. + int64_t profile_updated = 0; /// API: groups/member::admin /// diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 3b8756a0..d828dd51 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -83,7 +83,7 @@ void contact_info::load(const dict& info_dict) { profile_picture.clear(); } - profile_seqno = maybe_int(info_dict, "#").value_or(0); + profile_updated = to_epoch_seconds(maybe_int(info_dict, "t").value_or(0)); 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); @@ -132,7 +132,7 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } - c.profile_seqno = profile_seqno; + c.profile_updated = profile_updated; c.approved = approved; c.approved_me = approved_me; c.blocked = blocked; @@ -156,7 +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_seqno = c.profile_seqno; + profile_updated = c.profile_updated; approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; @@ -230,7 +230,7 @@ void Contacts::set(const contact_info& contact) { info["q"], contact.profile_picture.key); - set_positive_int(info["#"], contact.profile_seqno); + set_positive_int(info["t"], to_epoch_seconds(contact.profile_updated)); set_flag(info["a"], contact.approved); set_flag(info["A"], contact.approved_me); set_flag(info["b"], contact.blocked); @@ -283,9 +283,9 @@ 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_seqno(std::string_view session_id, int64_t profile_seqno) { +void Contacts::set_profile_updated(std::string_view session_id, int64_t profile_updated) { auto c = get_or_construct(session_id); - c.profile_seqno = profile_seqno; + c.profile_updated = profile_updated; set(c); } void Contacts::set_approved(std::string_view session_id, bool approved) { diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index e3a30b18..d18de500 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -66,7 +66,7 @@ void Members::set(const member& mem) { info["q"], mem.profile_picture.key); - set_positive_int(info["#"], mem.profile_seqno); + set_positive_int(info["t"], to_epoch_seconds(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); @@ -96,7 +96,7 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } - profile_seqno = maybe_int(info_dict, "#").value_or(0); + profile_updated = to_epoch_seconds(maybe_int(info_dict, "t").value_or(0)); 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); @@ -189,7 +189,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_seqno = m.profile_seqno; + profile_updated = m.profile_updated; admin = m.admin; invite_status = (m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT) @@ -214,7 +214,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } - m.profile_seqno = profile_seqno; + m.profile_updated = profile_updated; m.admin = admin; static_assert(groups::STATUS_SENT == ::STATUS_SENT); static_assert(groups::STATUS_FAILED == ::STATUS_FAILED); diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index d8625b8e..6c3131a1 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -48,7 +48,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); - CHECK(c.profile_seqno == 0); + CHECK(c.profile_updated == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -63,7 +63,7 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); - c.profile_seqno = 1; + c.profile_updated = 1; c.approved = true; c.approved_me = true; c.created = created_ts * 1'000; @@ -76,7 +76,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_seqno == 1); + CHECK(contacts.get(definitely_real_id)->profile_updated == 1); CHECK(contacts.get(definitely_real_id)->approved); CHECK(contacts.get(definitely_real_id)->approved_me); CHECK_FALSE(contacts.get(definitely_real_id)->profile_picture); @@ -109,7 +109,7 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); - CHECK(x->profile_seqno == 1); + CHECK(x->profile_updated == 1); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); @@ -141,13 +141,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_seqnos; + 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_seqnos.emplace_back(cc.profile_seqno); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); @@ -156,8 +156,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); - CHECK(profile_seqnos[0] == 1); - CHECK(profile_seqnos[1] == 0); + CHECK(profile_updateds[0] == 1); + CHECK(profile_updateds[1] == 0); // Conflict! Oh no! @@ -167,7 +167,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_seqno(third_id, 2); + contacts2.set_profile_updated(third_id, 2); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -225,19 +225,19 @@ TEST_CASE("Contacts", "[config][contacts]") { session_ids.clear(); nicknames.clear(); - profile_seqnos.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_seqnos.emplace_back(cc.profile_seqno); + 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_seqnos[0] == 0); - CHECK(profile_seqnos[1] == 2); + CHECK(profile_updateds[0] == 0); + CHECK(profile_updateds[1] == 2); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" @@ -292,7 +292,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_seqno == 0); + CHECK(c.profile_updated == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -301,7 +301,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { strcpy(c.name, "Joe"); strcpy(c.nickname, "Joey"); - c.profile_seqno = 1; + c.profile_updated = 1; c.approved = true; c.approved_me = true; c.created = created_ts; @@ -313,7 +313,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); - CHECK(c2.profile_seqno == 1); + CHECK(c2.profile_updated == 1); CHECK(c2.approved); CHECK(c2.approved_me); CHECK_FALSE(c2.blocked); @@ -349,7 +349,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_seqno == 1); + CHECK(c3.profile_updated == 1); CHECK(c3.approved); CHECK(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -360,7 +360,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_seqno == 0); + CHECK(c3.profile_updated == 0); CHECK_FALSE(c3.approved); CHECK_FALSE(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -390,7 +390,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_seqnos; + std::vector profile_updateds; CHECK(contacts_size(conf) == 2); contacts_iterator* it = contacts_iterator_new(conf); @@ -398,7 +398,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_seqnos.emplace_back(ci.profile_seqno); + profile_updateds.emplace_back(ci.profile_updated); } contacts_iterator_free(it); @@ -407,8 +407,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_seqnos[0] == 1); - CHECK(profile_seqnos[1] == 0); + 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 80dd77b1..04c61710 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -72,7 +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_seqno = 1; + m.profile_updated = 1; gmem1.set(m); } // 10 members: @@ -82,7 +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_seqno = 2; + m.profile_updated = 2; gmem1.set(m); } // 5 members with no attributes (not even a name): @@ -133,7 +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_seqno == 1); + CHECK(m.profile_updated == 1); CHECK_FALSE(m.profile_picture.empty()); CHECK(gmem2.get_status(m) == session::config::groups::member::Status::promotion_accepted); @@ -147,12 +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_seqno == 2); + CHECK(m.profile_updated == 2); CHECK_FALSE(m.profile_picture.empty()); } else { CHECK(m.name.empty()); CHECK(m.profile_picture.empty()); - CHECK(m.profile_seqno == 0); + CHECK(m.profile_updated == 0); } } i++; @@ -162,13 +162,13 @@ TEST_CASE("Group Members", "[config][groups][members]") { for (int i = 5; i < 15; i++) { auto m = gmem2.get_or_construct(sids[i]); - m.profile_seqno += 1; + m.profile_updated += 1; 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_seqno = 1; + m.profile_updated = 1; gmem2.set(m); } for (int i = 50; i < 55; i++) { @@ -223,19 +223,19 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) - CHECK(m.profile_seqno == 1); + CHECK(m.profile_updated == 1); if (i >= 5 && i < 10) - CHECK(m.profile_seqno == 2); + CHECK(m.profile_updated == 2); if (i >= 10 && i < 15) - CHECK(m.profile_seqno == 3); + CHECK(m.profile_updated == 3); if (i >= 15 && i < 20) - CHECK(m.profile_seqno == 2); + CHECK(m.profile_updated == 2); if (i >= 20 && i < 22) - CHECK(m.profile_seqno == 0); + CHECK(m.profile_updated == 0); if (i >= 22 && i < 50) - CHECK(m.profile_seqno == 1); + CHECK(m.profile_updated == 1); if (i >= 50) - CHECK(m.profile_seqno == 0); + CHECK(m.profile_updated == 0); if (i >= 10 && i < 25) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_sending); @@ -307,19 +307,19 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) - CHECK(m.profile_seqno == 1); + CHECK(m.profile_updated == 1); if (i >= 5 && i < 10) - CHECK(m.profile_seqno == 2); + CHECK(m.profile_updated == 2); if (i >= 10 && i < 15) - CHECK(m.profile_seqno == 3); + CHECK(m.profile_updated == 3); if (i >= 15 && i < 20) - CHECK(m.profile_seqno == 2); + CHECK(m.profile_updated == 2); if (i >= 20 && i < 22) - CHECK(m.profile_seqno == 0); + CHECK(m.profile_updated == 0); if (i >= 22 && i < 50) - CHECK(m.profile_seqno == 1); + CHECK(m.profile_updated == 1); if (i >= 50) - CHECK(m.profile_seqno == 0); + CHECK(m.profile_updated == 0); if (is_prime100(i) || (i >= 25 && i < 50)) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_not_sent); From b30f58a6b1862888da440b615eec4dd69aae2797 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Jul 2025 11:27:53 +1000 Subject: [PATCH 3/7] Ran the formatter, fixed a network test that was still talking to testnet --- include/session/config/contacts.hpp | 3 ++- tests/test_session_network.cpp | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index dff3d671..85eff2c5 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -54,7 +54,8 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; - int64_t profile_updated = 0; /// The unix timestamp (seconds) that this profile information was last updated. + int64_t profile_updated = 0; /// The unix timestamp (seconds) that this profile information + /// was last updated. bool approved = false; bool approved_me = false; bool blocked = false; diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index 32d83941..102da784 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -59,13 +59,14 @@ std::optional node_for_destination(network_destination destination std::shared_ptr create_test_server(uint16_t port) { oxen::quic::opt::inbound_alpns server_alpns{"oxenstorage"}; - auto server_key_pair = session::ed25519::ed25519_key_pair(to_span(fmt::format("{:032}", port))); + auto server_key_pair = + session::ed25519::ed25519_key_pair(to_span(fmt::format("{:032}", port))); auto server_x25519_pubkey = session::curve25519::to_curve25519_pubkey( {server_key_pair.first.data(), server_key_pair.first.size()}); auto server_x25519_seckey = session::curve25519::to_curve25519_seckey( {server_key_pair.second.data(), server_key_pair.second.size()}); - auto creds = - oxen::quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(server_key_pair.second)); + auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( + to_string_view(server_key_pair.second)); oxen::quic::Address server_local{port}; session::onionreq::HopEncryption decryptor{ x25519_seckey::from_bytes(to_span(server_x25519_seckey)), From 0e3f86ec126aa7e545ff24198a001a627a930f37 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 14 Jul 2025 11:41:57 +1000 Subject: [PATCH 4/7] Ran the formatter again --- tests/test_session_network.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_session_network.cpp b/tests/test_session_network.cpp index 102da784..32d83941 100644 --- a/tests/test_session_network.cpp +++ b/tests/test_session_network.cpp @@ -59,14 +59,13 @@ std::optional node_for_destination(network_destination destination std::shared_ptr create_test_server(uint16_t port) { oxen::quic::opt::inbound_alpns server_alpns{"oxenstorage"}; - auto server_key_pair = - session::ed25519::ed25519_key_pair(to_span(fmt::format("{:032}", port))); + auto server_key_pair = session::ed25519::ed25519_key_pair(to_span(fmt::format("{:032}", port))); auto server_x25519_pubkey = session::curve25519::to_curve25519_pubkey( {server_key_pair.first.data(), server_key_pair.first.size()}); auto server_x25519_seckey = session::curve25519::to_curve25519_seckey( {server_key_pair.second.data(), server_key_pair.second.size()}); - auto creds = oxen::quic::GNUTLSCreds::make_from_ed_seckey( - to_string_view(server_key_pair.second)); + auto creds = + oxen::quic::GNUTLSCreds::make_from_ed_seckey(to_string_view(server_key_pair.second)); oxen::quic::Address server_local{port}; session::onionreq::HopEncryption decryptor{ x25519_seckey::from_bytes(to_span(server_x25519_seckey)), From 302cba5886ef362b6117ebd6d92011b9ed894bee Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Jul 2025 15:21:41 -0300 Subject: [PATCH 5/7] Make profile timestamp type safe std::chrono::sys_seconds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses std::chrono::sys_seconds for the new profile_updated timestamp field, so that we can avoid the unfortunate bug that happened in earlier releases where some clients violated the documented restrictions and passed milliseconds or microseconds: sys_seconds is absolutely unambiguous as to what it holds and makes such an error virtually impossible. - Adds `session::to_sys_seconds(int)` that can get a proper sys_seconds from a maybe-s, ms, or µs value by guessing based on the magnitude. - Add methods for getting/setting sys_seconds timestamps from/into configs. --- include/session/config/contacts.hpp | 9 ++--- include/session/config/groups/members.hpp | 2 +- include/session/util.hpp | 19 ++++++++++ src/config/contacts.cpp | 12 ++++--- src/config/groups/members.cpp | 8 ++--- src/config/internal.cpp | 13 +++++++ src/config/internal.hpp | 14 ++++++++ src/util.cpp | 4 +++ tests/test_config_contacts.cpp | 22 ++++++------ tests/test_group_members.cpp | 44 +++++++++++------------ 10 files changed, 101 insertions(+), 46 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 85eff2c5..9c265c18 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -54,8 +54,8 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; - int64_t profile_updated = 0; /// The unix timestamp (seconds) that this profile information - /// was last updated. + 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; @@ -240,8 +240,9 @@ class Contacts : public ConfigBase { /// /// Inputs: /// - `session_id` -- hex string of the session id - /// - `profile_updated` -- profile updated unix timestamp (seconds) of the contact - void set_profile_updated(std::string_view session_id, int64_t profile_updated); + /// - `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 /// diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 270b94a4..0ea32b00 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -106,7 +106,7 @@ struct member { /// Member variable /// /// The unix timestamp (seconds) that this profile information was last updated. - int64_t profile_updated = 0; + std::chrono::sys_seconds profile_updated{}; /// API: groups/member::admin /// 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 d828dd51..f0e43e4a 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -83,7 +83,7 @@ void contact_info::load(const dict& info_dict) { profile_picture.clear(); } - profile_updated = to_epoch_seconds(maybe_int(info_dict, "t").value_or(0)); + profile_updated = ts_or_epoch(info_dict, "t"); 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); @@ -132,7 +132,7 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } - c.profile_updated = profile_updated; + c.profile_updated = profile_updated.time_since_epoch().count(); c.approved = approved; c.approved_me = approved_me; c.blocked = blocked; @@ -156,7 +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 = c.profile_updated; + profile_updated = to_sys_seconds(c.profile_updated); approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; @@ -230,7 +230,8 @@ void Contacts::set(const contact_info& contact) { info["q"], contact.profile_picture.key); - set_positive_int(info["t"], to_epoch_seconds(contact.profile_updated)); + 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); @@ -283,7 +284,8 @@ 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, int64_t profile_updated) { +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); diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index d18de500..c4eb29ff 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -66,7 +66,7 @@ void Members::set(const member& mem) { info["q"], mem.profile_picture.key); - set_positive_int(info["t"], to_epoch_seconds(mem.profile_updated)); + 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); @@ -96,7 +96,7 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } - profile_updated = to_epoch_seconds(maybe_int(info_dict, "t").value_or(0)); + profile_updated = ts_or_epoch(info_dict, "t"); 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); @@ -189,7 +189,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 = m.profile_updated; + 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) @@ -214,7 +214,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } - m.profile_updated = profile_updated; + 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..8d9432fb 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -81,6 +81,19 @@ std::optional maybe_int(const session::config::dict& d, const char* key return std::nullopt; } +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; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 74cc31fd..deec11d0 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -147,6 +147,15 @@ 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 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); @@ -172,6 +181,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/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 6c3131a1..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,7 +50,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); - CHECK(c.profile_updated == 0); + CHECK(c.profile_updated == std::chrono::sys_seconds{}); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -63,7 +65,7 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); - c.profile_updated = 1; + c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; c.created = created_ts * 1'000; @@ -76,7 +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 == 1); + 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); @@ -109,7 +111,7 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); - CHECK(x->profile_updated == 1); + CHECK(x->profile_updated.time_since_epoch() == 1s); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); @@ -141,7 +143,7 @@ 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; + std::vector profile_updateds; CHECK(contacts.size() == 2); CHECK_FALSE(contacts.empty()); for (const auto& cc : contacts) { @@ -156,8 +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] == 1); - CHECK(profile_updateds[1] == 0); + CHECK(profile_updateds[0].time_since_epoch() == 1s); + CHECK(profile_updateds[1].time_since_epoch() == 0s); // Conflict! Oh no! @@ -167,7 +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, 2); + contacts2.set_profile_updated(third_id, session::to_sys_seconds(2)); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -236,8 +238,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); - CHECK(profile_updateds[0] == 0); - CHECK(profile_updateds[1] == 2); + CHECK(profile_updateds[0].time_since_epoch() == 0s); + CHECK(profile_updateds[1].time_since_epoch() == 2s); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 04c61710..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,7 +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 = 1; + m.profile_updated = std::chrono::sys_seconds{1s}; gmem1.set(m); } // 10 members: @@ -82,7 +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 = 2; + m.profile_updated = session::to_sys_seconds(2); gmem1.set(m); } // 5 members with no attributes (not even a name): @@ -133,7 +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 == 1); + 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); @@ -147,12 +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 == 2); + 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 == 0); + CHECK(m.profile_updated.time_since_epoch() == 0s); } } i++; @@ -162,13 +162,13 @@ TEST_CASE("Group Members", "[config][groups][members]") { for (int i = 5; i < 15; i++) { auto m = gmem2.get_or_construct(sids[i]); - m.profile_updated += 1; + 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 = 1; + m.profile_updated = std::chrono::sys_seconds{1s}; gmem2.set(m); } for (int i = 50; i < 55; i++) { @@ -223,19 +223,19 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) - CHECK(m.profile_updated == 1); + CHECK(m.profile_updated.time_since_epoch() == 1s); if (i >= 5 && i < 10) - CHECK(m.profile_updated == 2); + CHECK(m.profile_updated.time_since_epoch() == 2s); if (i >= 10 && i < 15) - CHECK(m.profile_updated == 3); + CHECK(m.profile_updated.time_since_epoch() == 3s); if (i >= 15 && i < 20) - CHECK(m.profile_updated == 2); + CHECK(m.profile_updated.time_since_epoch() == 2s); if (i >= 20 && i < 22) - CHECK(m.profile_updated == 0); + CHECK(m.profile_updated.time_since_epoch() == 0s); if (i >= 22 && i < 50) - CHECK(m.profile_updated == 1); + CHECK(m.profile_updated.time_since_epoch() == 1s); if (i >= 50) - CHECK(m.profile_updated == 0); + 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); @@ -307,19 +307,19 @@ TEST_CASE("Group Members", "[config][groups][members]") { : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); if (i < 5) - CHECK(m.profile_updated == 1); + CHECK(m.profile_updated.time_since_epoch() == 1s); if (i >= 5 && i < 10) - CHECK(m.profile_updated == 2); + CHECK(m.profile_updated.time_since_epoch() == 2s); if (i >= 10 && i < 15) - CHECK(m.profile_updated == 3); + CHECK(m.profile_updated.time_since_epoch() == 3s); if (i >= 15 && i < 20) - CHECK(m.profile_updated == 2); + CHECK(m.profile_updated.time_since_epoch() == 2s); if (i >= 20 && i < 22) - CHECK(m.profile_updated == 0); + CHECK(m.profile_updated.time_since_epoch() == 0s); if (i >= 22 && i < 50) - CHECK(m.profile_updated == 1); + CHECK(m.profile_updated.time_since_epoch() == 1s); if (i >= 50) - CHECK(m.profile_updated == 0); + 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); From 939f2c47b5f381aefa06fd5c03925f4f4b18eea4 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Jul 2025 15:26:45 -0300 Subject: [PATCH 6/7] Add `int_or_0`/`string_or_empty` to match ts_or_epoch There are a lot of internal calls such as `maybe_int(c, "key").value_or(0)` and `maybe_string(c, "key").value_or("")`. This makes it them slightly less cumbersome by adding `int_or_0` and `string_or_empty` functions that handle the fallback-to-0/empty automatically. --- src/config/contacts.cpp | 22 +++++++++++----------- src/config/convo_info_volatile.cpp | 4 ++-- src/config/groups/members.cpp | 15 +++++++-------- src/config/internal.cpp | 18 ++++++++++++++++++ src/config/internal.hpp | 19 +++++++++++++++---- src/config/user_groups.cpp | 17 +++++++---------- 6 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index f0e43e4a..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"); @@ -84,13 +84,13 @@ void contact_info::load(const dict& info_dict) { } profile_updated = ts_or_epoch(info_dict, "t"); - 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); + 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) @@ -98,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_); @@ -110,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; @@ -119,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 { 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 c4eb29ff..86c3b085 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -85,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"); @@ -97,13 +97,12 @@ void member::load(const dict& info_dict) { } profile_updated = ts_or_epoch(info_dict, "t"); - 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; + 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 diff --git a/src/config/internal.cpp b/src/config/internal.cpp index 8d9432fb..0b2051dc 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -81,6 +81,12 @@ 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)) @@ -100,12 +106,24 @@ std::optional maybe_string(const session::config::dict& d, const ch 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 deec11d0..337523c5 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -147,6 +147,10 @@ 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); @@ -159,15 +163,22 @@ std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* // 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); 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() { From c20e01271640c3d14c9dda08c9d0b01cbe82fe46 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Jul 2025 16:51:43 -0300 Subject: [PATCH 7/7] Drone: drop unneeded backports from Debian 12 build --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'),