From fda6db3128c7fd4d002e32fe06c5f9e3782cb204 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 11 Jul 2025 11:42:46 +1000 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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'), From c5d1abc99fb9d5b6bcb9da52342fc6254d349a91 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 13 Jun 2025 16:24:12 +1000 Subject: [PATCH 08/14] Added blinded contact records to Contacts and ConvoInfoVolatile --- include/session/config/contacts.h | 123 ++++++ include/session/config/contacts.hpp | 126 ++++++- include/session/config/convo_info_volatile.h | 190 ++++++++++ .../session/config/convo_info_volatile.hpp | 111 +++++- src/config/contacts.cpp | 353 +++++++++++++++--- src/config/convo_info_volatile.cpp | 176 ++++++++- 6 files changed, 1005 insertions(+), 74 deletions(-) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 99c1d818..d45318b7 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -37,6 +37,31 @@ typedef struct contacts_contact { } contacts_contact; +typedef struct contacts_blinded_contact { + char session_id[67]; // in hex; 66 hex chars + null terminator. + char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case, + // only has port if non-default, has trailing / removed) + unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls) + + char name[101]; // This will be a 0-length strings when unset + user_profile_pic profile_pic; + + bool legacy_blinding; + int64_t created; // unix timestamp (seconds) + +} contacts_blinded_contact; + +/// Struct containing a list of contacts_blinded_contact structs. Typically where this is returned +/// by this API it must be freed (via `free()`) when done with it. +/// +/// When returned as a pointer by a libsession-util function this is allocated in such a way that +/// just the outer contacts_blinded_contact_list can be free()d to free both the list *and* the +/// inner `value` and pointed-at values. +typedef struct contacts_blinded_contact_list { + contacts_blinded_contact** value; // array of blinded contacts + size_t len; // length of `value` +} contacts_blinded_contact_list; + /// API: contacts/contacts_init /// /// Constructs a contacts config object and sets a pointer to it in `conf`. @@ -209,6 +234,104 @@ LIBSESSION_EXPORT bool contacts_erase(config_object* conf, const char* session_i /// - `size_t` -- number of contacts LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); +/// API: contacts/contacts_blinded_contacts +/// +/// Retrieves a list of blinded contact records. +/// +/// Declaration: +/// ```cpp +/// contacts_blinded_contact_list* contacts_blinded_contacts( +/// [in] config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in, out] Pointer to config_object object +/// +/// Outputs: +/// - `contacts_blinded_contact_list*` -- pointer to the list of blinded contact structs; the +/// pointer belongs to the caller and must be freed when done with it. +LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded_contacts( + const config_object* conf); + +/// API: contacts/contacts_get_blinded_contact +/// +/// Fills `blinded_contact` with the blinded contact info given a blinded session ID (specified as a +/// null-terminated hex string), if the blinded contact exists, and returns true. If the contact +/// does not exist then `blinded_contact` is left unchanged and false is returned. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_get_blinded_contact( +/// [in] config_object* conf, +/// [in] const char* blinded_session_id, +/// [in] bool legacy_blinding, +/// [out] contacts_blinded_contact* blinded_contact +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `blinded_session_id` -- [in] null terminated hex string +/// - `legacy_blinding` -- [in] null terminated hex string +/// - `blinded_contact` -- [out] the blinded contact info data +/// +/// Output: +/// - `bool` -- Returns true if blinded contact exists +LIBSESSION_EXPORT bool contacts_get_blinded_contact( + config_object* conf, + const char* blinded_session_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) LIBSESSION_WARN_UNUSED; + +/// API: contacts/contacts_set_blinded_contact +/// +/// Adds or updates a blinded contact from the given contact info struct. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_set_blinded_contact( +/// [in] config_object* conf, +/// [in] contacts_blinded_contact* bc +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `blinded_contact` -- [in] the blinded contact info data +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool contacts_set_blinded_contact( + config_object* conf, const contacts_blinded_contact* bc); + +/// API: contacts/contacts_erase_blinded_contact +/// +/// Erases a blinded contact from the blinded contact list. blinded_id is in hex. Returns true if +/// the blinded contact was found and removed, false if the blinded contact was not present. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_erase_blinded_contact( +/// [in, out] config_object* conf, +/// [in] const char* base_url, +/// [in] const char* blinded_id, +/// [in] bool legacy_blinding +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in, out] Pointer to the config object +/// - `base_url` -- [in] Text containing null terminated base url for the community this blinded +/// contact originated from +/// - `blinded_id` -- [in] Text containing null terminated hex string +/// - `legacy_blinding` -- [in] Flag indicating whether this blinded contact used legacy blinding +/// +/// Outputs: +/// - `bool` -- True if erasing was successful +LIBSESSION_EXPORT bool contacts_erase_blinded_contact( + config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding); + typedef struct contacts_iterator { void* _internals; } contacts_iterator; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 9c265c18..c4153c8c 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -7,12 +7,14 @@ #include #include "base.hpp" +#include "community.hpp" #include "expiring.hpp" #include "namespaces.hpp" #include "notify.hpp" #include "profile_pic.hpp" extern "C" struct contacts_contact; +extern "C" struct contacts_blinded_contact; using namespace std::literals; @@ -45,8 +47,25 @@ namespace session::config { /// 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. +/// +/// b - dict of blinded contacts. This is a nested dict where the outkey keys are the BASE_URL of +/// the community the blinded contact originated from and the outer value is a dict containing: +/// +/// `#` - the 32-byte server pubkey +/// `R` - dict of blinded contacts from the server; each key is the blinded session pubkey +/// without the prefix ("R" to match user_groups equivalent "R"oom field, and to make use of +/// existing community iterators, binary, 32 bytes), value is a dict containing keys: +/// containing keys: +/// +/// n - contact name (string). This is always serialized, even if empty (but empty indicates +/// no name) so that we always have at least one key set (required to keep the dict value +/// alive as empty dicts get pruned). +/// p - profile url (string) +/// q - profile decryption key (binary) +/// j - Unix timestamp (seconds) when the contact was created ("j" to match user_groups +/// equivalent "j"oined field). Omitted if 0. +/// y - flag indicating whether the blinded message request is using legac"y" blinding. -/// Struct containing contact info. struct contact_info { static constexpr size_t MAX_NAME_LENGTH = 100; @@ -100,6 +119,47 @@ struct contact_info { void load(const dict& info_dict); }; +struct blinded_contact_info : community { + using community::community; + + const std::string session_id() const; // in hex + std::string name; + profile_pic profile_picture; + bool legacy_blinding; + int64_t created = 0; // Unix timestamp (seconds) when this contact was added + + explicit blinded_contact_info( + std::string_view base_url, + std::string_view blinded_id, + std::span pubkey, + bool legacy_blinding); + + // Internal ctor/method for C API implementations: + blinded_contact_info(const struct contacts_blinded_contact& c); // From c struct + + /// API: contacts/blinded_contact_info::into + /// + /// converts the contact info into a c struct + /// + /// Inputs: + /// - `c` -- Return Parameter that will be filled with data in blinded_contact_info + void into(contacts_blinded_contact& c) const; + + /// API: contacts/contact_info::set_name + /// + /// Sets a name; this is exactly the same as assigning to .name directly, + /// except that we throw an exception if the given name is longer than MAX_NAME_LENGTH. + /// + /// Inputs: + /// - `name` -- Name to assign to the contact + void set_name(std::string name); + + private: + friend class Contacts; + friend struct session::config::comm_iterator_helper; + void load(const dict& info_dict); +}; + class Contacts : public ConfigBase { public: @@ -353,6 +413,70 @@ class Contacts : public ConfigBase { bool accepts_protobuf() const override { return true; } + protected: + // Drills into the nested dicts to access open group details + DictFieldProxy blinded_contact_field( + const blinded_contact_info& bc, + std::span* get_pubkey = nullptr) const; + + public: + /// API: contacts/Contacts::blinded_contacts + /// + /// Retrieves a list of all known blinded contacts. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::vector` - Returns a list of blinded_contact_info + std::vector blinded_contacts() const; + + /// API: contacts/Contacts::get_blinded + /// + /// Looks up and returns a blinded contact by blinded session ID (hex). Returns nullopt if the + /// blinded session ID was not found, otherwise returns a filled out `blinded_contact_info`. + /// + /// Inputs: + /// - `pubkey_hex` -- hex string of the session id + /// - `legacy_blinding` -- flag indicating whether the pubkey is using legacy blinding + /// + /// Outputs: + /// - `std::optional` - Returns nullopt if blinded session ID was not + /// found, otherwise a filled out blinded_contact_info + std::optional get_blinded( + std::string_view pubkey_hex, bool legacy_blinding) const; + + /// API: contacts/contacts::set_blinded_contact + /// + /// Sets or updates multiple blinded contact info values at once with the given info. The usual + /// use is to access the current info, change anything desired, then pass it back into + /// set_blinded_contact, e.g.: + /// + ///```cpp + /// auto c = contacts.get_blinded(pubkey, legacy_blinding); + /// c.name = "Session User 42"; + /// contacts.set_blinded_contact(c); + ///``` + /// + /// Inputs: + /// - `bc` -- set_blinded_contact value to set + bool set_blinded_contact(const blinded_contact_info& bc); + + /// API: contacts/contacts::erase_blinded_contact + /// + /// Removes a blinded contact, if present. Returns true if it was found and removed, false + /// otherwise. Note that this removes all fields related to a blinded contact, even fields we do + /// not know about. + /// + /// Inputs: + /// - `base_url` -- the base url for the community this blinded contact originated from + /// - `blinded_id` -- hex string of the blinded id + /// - `legacy_blinding` -- flag indicating whether `blinded_id` is using legacy blinding + /// + /// Outputs: + /// - `bool` - Returns true if contact was found and removed, false otherwise + bool erase_blinded_contact( + std::string_view base_url, std::string_view blinded_id, bool legacy_blinding); + struct iterator; /// API: contacts/contacts::begin /// diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 952b6ff7..94de509d 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -38,6 +38,14 @@ typedef struct convo_info_volatile_legacy_group { bool unread; // true if marked unread } convo_info_volatile_legacy_group; +typedef struct convo_info_volatile_blinded_1to1 { + char blinded_session_id[67]; // in hex; 66 hex chars + null terminator. + bool legacy_blinding; + + int64_t last_read; // ms since unix epoch + bool unread; // true if the conversation is explicitly marked unread +} convo_info_volatile_blinded_1to1; + /// API: convo_info_volatile/convo_info_volatile_init /// /// Constructs a conversations config object and sets a pointer to it in `conf`. @@ -345,6 +353,76 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_legacy_group( convo_info_volatile_legacy_group* convo, const char* id) LIBSESSION_WARN_UNUSED; +/// API: convo_info_volatile/convo_info_volatile_get_blinded_1to1 +/// +/// Fills `convo` with the conversation info given a blinded session ID (specified as a +/// null-terminated hex string), if the conversation exists, and returns true. If the conversation +/// does not exist then `convo` is left unchanged and false is returned. If an error occurs, false +/// is returned and `conf->last_error` will be set to non-NULL containing the error string (if no +/// error occurs, such as in the case where the conversation merely doesn't exist, `last_error` will +/// be set to NULL). +/// +/// Declaration: +/// ```cpp +/// BOOL convo_info_volatile_get_blinded_1to1( +/// [in] config_object* conf, +/// [out] convo_info_volatile_blinded_1to1* convo, +/// [in] const char* blinded_session_id +/// [in] bool legacy_blinding +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `convo` -- [out] Pointer to conversation info +/// - `blinded_session_id` -- [in] Null terminated hex string of the session_id +/// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy blinding +/// +/// Outputs: +/// - `bool` - Returns true if the conversation exists +LIBSESSION_EXPORT bool convo_info_volatile_get_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) LIBSESSION_WARN_UNUSED; + +/// API: convo_info_volatile/convo_info_volatile_get_or_construct_blinded_1to1 +/// +/// Same as the above convo_info_volatile_get_blinded_1to1 except that when the conversation does +/// not exist, this sets all the convo fields to defaults and loads it with the given +/// blinded_session_id. +/// +/// Returns true as long as it is given a valid blinded_session_id. A false return is considered an +/// error, and means the blinded_session_id was not a valid blinded_session_id. In such a case +/// `conf->last_error` will be set to an error string. +/// +/// This is the method that should usually be used to create or update a conversation, followed by +/// setting fields in the convo, and then giving it to convo_info_volatile_set(). +/// +/// Declaration: +/// ```cpp +/// BOOL convo_info_volatile_get_or_construct_1to1( +/// [in] config_object* conf, +/// [out] convo_info_volatile_blinded_1to1* convo, +/// [in] const char* blinded_session_id +/// [in] bool legacy_blinding +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `convo` -- [out] Pointer to conversation info +/// - `blinded_session_id` -- [in] Null terminated hex string of the blinded session id +/// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy blinding +/// +/// Outputs: +/// - `bool` - Returns true if the conversation exists +LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) LIBSESSION_WARN_UNUSED; + /// API: convo_info_volatile/convo_info_volatile_set_1to1 /// /// Adds or updates a conversation from the given convo info @@ -429,6 +507,27 @@ LIBSESSION_EXPORT bool convo_info_volatile_set_group( LIBSESSION_EXPORT bool convo_info_volatile_set_legacy_group( config_object* conf, const convo_info_volatile_legacy_group* convo); +/// API: convo_info_volatile/convo_info_volatile_set_blinded_1to1 +/// +/// Adds or updates a conversation from the given convo info +/// +/// Declaration: +/// ```cpp +/// VOID convo_info_volatile_set_blinded_1to1( +/// [in] config_object* conf, +/// [in] const convo_info_volatile_blidned_1to1* convo +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `convo` -- [in] Pointer to conversation info structure +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool convo_info_volatile_set_blinded_1to1( + config_object* conf, const convo_info_volatile_blinded_1to1* convo); + /// API: convo_info_volatile/convo_info_volatile_erase_1to1 /// /// Erases a conversation from the conversation list. Returns true if the conversation was found @@ -520,6 +619,31 @@ LIBSESSION_EXPORT bool convo_info_volatile_erase_group(config_object* conf, cons LIBSESSION_EXPORT bool convo_info_volatile_erase_legacy_group( config_object* conf, const char* group_id); +/// API: convo_info_volatile/convo_info_volatile_erase_blinded_1to1 +/// +/// Erases a conversation from the conversation list. Returns true if the conversation was found +/// and removed, false if the conversation was not present. You must not call this during +/// iteration; see details below. +/// +/// Declaration: +/// ```cpp +/// BOOL convo_info_volatile_erase_blinded_1to1( +/// [in] config_object* conf, +/// [in] const char* blinded_session_id +/// [in] bool legacy_blinding +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `blinded_session_id` -- [in] Null terminated hex string +/// - `legacy_blinding` -- flag indicating whether the blinded contact used legacy blinding +/// +/// Outputs: +/// - `bool` - Returns true if conversation was found and removed +LIBSESSION_EXPORT bool convo_info_volatile_erase_blinded_1to1( + config_object* conf, const char* blinded_session_id, bool legacy_blinding); + /// API: convo_info_volatile/convo_info_volatile_size /// /// Returns the number of conversations. @@ -610,6 +734,24 @@ LIBSESSION_EXPORT size_t convo_info_volatile_size_groups(const config_object* co /// - `size_t` -- number of legacy groups LIBSESSION_EXPORT size_t convo_info_volatile_size_legacy_groups(const config_object* conf); +/// API: convo_info_volatile/convo_info_volatile_size_blinded_1to1 +/// +/// Returns the number of conversations. +/// +/// Declaration: +/// ```cpp +/// SIZE_T convo_info_volatile_size_blinded_1to1( +/// [in] const config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `size_t` -- number of conversations +LIBSESSION_EXPORT size_t convo_info_volatile_size_blinded_1to1(const config_object* conf); + typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// API: convo_info_volatile/convo_info_volatile_iterator_new @@ -622,6 +764,7 @@ typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// convo_info_volatile_community c2; /// convo_info_volatile_group c3; /// convo_info_volatile_legacy_group c4; +/// convo_info_volatile_blinded_1to1 c5; /// convo_info_volatile_iterator *it = convo_info_volatile_iterator_new(my_convos); /// for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { /// if (convo_info_volatile_it_is_1to1(it, &c1)) { @@ -632,6 +775,8 @@ typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// // use c3.whatever /// } else if (convo_info_volatile_it_is_legacy_group(it, &c4)) { /// // use c4.whatever +/// } else if (convo_info_volatile_it_is_blinded_1to1(it, &c5)) { +/// // use c5.whatever /// } /// } /// convo_info_volatile_iterator_free(it); @@ -747,6 +892,29 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( const config_object* conf); +/// API: convo_info_volatile/convo_info_volatile_iterator_new_blinded_1to1 +/// +/// The same as `convo_info_volatile_iterator_new` except that this iterates *only* over one type of +/// conversation. You still need to use `convo_info_volatile_it_is_blinded_1to1` (or the +/// alternatives) to load the data in each pass of the loop. (You can, however, safely ignore the +/// bool return value of the `it_is_whatever` function: it will always be true for the particular +/// type being iterated over). +/// +/// Declaration: +/// ```cpp +/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new_blinded_1to1( +/// [in] const config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `convo_info_volatile_iterator*` -- Iterator +LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_blinded_1to1( + const config_object* conf); + /// API: convo_info_volatile/convo_info_volatile_iterator_free /// /// Frees an iterator once no longer needed. @@ -883,6 +1051,28 @@ LIBSESSION_EXPORT bool convo_info_volatile_it_is_group( LIBSESSION_EXPORT bool convo_info_volatile_it_is_legacy_group( convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c); +/// API: convo_info_volatile/convo_info_volatile_it_is_blinded_1to1 +/// +/// If the current iterator record is a blinded 1-to-1 conversation this sets the details into `c` +/// and returns true. Otherwise it returns false. +/// +/// Declaration: +/// ```cpp +/// BOOL convo_info_volatile_it_is_blinded_1to1( +/// [in] convo_info_volatile_iterator* it, +/// [out] convo_info_volatile_blinded_1to1* c +/// ); +/// ``` +/// +/// Inputs: +/// - `it` -- [in] The convo_info_volatile_iterator +/// - `c` -- [out] Pointer to the convo_info_volatile, will be populated if true +/// +/// Outputs: +/// - `bool` -- True if the record is a blinded 1-to-1 conversation +LIBSESSION_EXPORT bool convo_info_volatile_it_is_blinded_1to1( + convo_info_volatile_iterator* it, convo_info_volatile_blinded_1to1* c); + #ifdef __cplusplus } // extern "C" #endif diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index 3871a694..ce954ddb 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -16,6 +16,7 @@ struct convo_info_volatile_1to1; struct convo_info_volatile_community; struct convo_info_volatile_group; struct convo_info_volatile_legacy_group; +struct convo_info_volatile_blinded_1to1; } namespace session::config { @@ -55,6 +56,13 @@ class val_loader; /// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, /// but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. +/// +/// b - outgoing blinded message request conversations. The key is the blinded Session ID without +/// the prefix. Values are dicts with keys: +/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, +/// but will be 0 if no messages are read. +/// u - will be present and set to 1 if this conversation is specifically marked unread. +/// y - flag indicating whether the blinded message request is using legac"y" blinding. namespace convo { @@ -149,7 +157,34 @@ namespace convo { void into(convo_info_volatile_legacy_group& c) const; // Into c struct }; - using any = std::variant; + struct blinded_one_to_one : base { + std::string blinded_session_id; // in hex + bool legacy_blinding; + + /// API: convo_info_volatile/blinded_one_to_one::blinded_one_to_one + /// + /// Constructs an empty blinded_one_to_one from a blinded_session_id. Session ID can be + /// either bytes (33) or hex (66). + /// + /// Declaration: + /// ```cpp + /// explicit blinded_one_to_one(std::string&& blinded_session_id); + /// explicit blinded_one_to_one(std::string_view blinded_session_id); + /// ``` + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string of the blinded session id + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + explicit blinded_one_to_one(std::string&& blinded_session_id, bool legacy_blinding); + explicit blinded_one_to_one(std::string_view blinded_session_id, bool legacy_blinding); + + // Internal ctor/method for C API implementations: + blinded_one_to_one(const struct convo_info_volatile_blinded_1to1& c); // From c struct + void into(convo_info_volatile_blinded_1to1& c) const; // Into c struct + }; + + using any = std::variant; } // namespace convo class ConvoInfoVolatile : public ConfigBase { @@ -298,6 +333,22 @@ class ConvoInfoVolatile : public ConfigBase { /// - `std::optional` - Returns a group std::optional get_legacy_group(std::string_view pubkey_hex) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_blinded_1to1 + /// + /// Looks up and returns a blinded contact by blinded session ID (hex). Returns nullopt if the + /// blinded session ID was not found, otherwise returns a filled out + /// `convo::blinded_one_to_one`. + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string of the blinded Session ID + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + /// + /// Outputs: + /// - `std::optional` - Returns a contact + std::optional get_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_1to1 /// /// These are the same as the above `get` methods (without "_or_construct" in the name), except @@ -385,6 +436,22 @@ class ConvoInfoVolatile : public ConfigBase { /// - `convo::community` - Returns a group convo::community get_or_construct_community(std::string_view full_url) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_blinded_1to1 + /// + /// These are the same as the above `get` methods (without "_or_construct" in the name), except + /// that when the conversation doesn't exist a new one is created, prefilled with the + /// pubkey/url/etc. + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string blinded Session ID + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + /// + /// Outputs: + /// - `convo::blinded_one_to_one` - Returns a blinded contact + convo::blinded_one_to_one get_or_construct_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) const; + /// API: convo_info_volatile/ConvoInfoVolatile::set /// /// Inserts or replaces existing conversation info. For example, to update a 1-to-1 @@ -402,6 +469,7 @@ class ConvoInfoVolatile : public ConfigBase { /// void set(const convo::group& c); /// void set(const convo::legacy_group& c); /// void set(const convo::community& c); + /// void set(const convo::blinded_one_to_one& c); /// void set(const convo::any& c); // Variant which can be any of the above /// ``` /// @@ -411,6 +479,7 @@ class ConvoInfoVolatile : public ConfigBase { void set(const convo::legacy_group& c); void set(const convo::group& c); void set(const convo::community& c); + void set(const convo::blinded_one_to_one& c); void set(const convo::any& c); // Variant which can be any of the above protected: @@ -469,6 +538,19 @@ class ConvoInfoVolatile : public ConfigBase { /// - `bool` - Returns true if found and removed, otherwise false bool erase_legacy_group(std::string_view pubkey_hex); + /// API: convo_info_volatile/ConvoInfoVolatile::erase_blinded_1to1 + /// + /// Removes a blinded one-to-one conversation. Returns true if found and removed, false if not + /// present. + /// + /// Inputs: + /// - `pubkey` -- hex blinded session id + /// - `legacy_blinding` -- flag indicating whether this blinded contact is using legacy blinding + /// + /// Outputs: + /// - `bool` - Returns true if found and removed, otherwise false + bool erase_blinded_1to1(std::string_view pubkey, bool legacy_blinding); + /// API: convo_info_volatile/ConvoInfoVolatile::erase /// /// Removes a conversation taking the convo::whatever record (rather than the pubkey/url). @@ -478,6 +560,7 @@ class ConvoInfoVolatile : public ConfigBase { /// bool erase(const convo::one_to_one& c); /// bool erase(const convo::community& c); /// bool erase(const convo::legacy_group& c); + /// bool erase(const convo::blinded_one_to_one& c); /// bool erase(const convo::any& c); // Variant of any of them /// ``` /// @@ -490,6 +573,7 @@ class ConvoInfoVolatile : public ConfigBase { bool erase(const convo::community& c); bool erase(const convo::group& c); bool erase(const convo::legacy_group& c); + bool erase(const convo::blinded_one_to_one& c); bool erase(const convo::any& c); // Variant of any of them @@ -506,6 +590,7 @@ class ConvoInfoVolatile : public ConfigBase { /// size_t size_communities() const; /// size_t size_groups() const; /// size_t size_legacy_groups() const; + /// size_t size_blinded_1to1() const; /// ``` /// /// Inputs: None @@ -520,6 +605,7 @@ class ConvoInfoVolatile : public ConfigBase { size_t size_communities() const; size_t size_groups() const; size_t size_legacy_groups() const; + size_t size_blinded_1to1() const; /// API: convo_info_volatile/ConvoInfoVolatile::empty /// @@ -549,6 +635,8 @@ class ConvoInfoVolatile : public ConfigBase { /// // use cg->id, cg->last_read /// } else if (const auto* lcg = std::get_if(&convo)) { /// // use lcg->id, lcg->last_read + /// } else if (const auto* bc = std::get_if(&convo)) { + /// // use bc->id, bc->last_read /// } /// } /// ``` @@ -570,6 +658,7 @@ class ConvoInfoVolatile : public ConfigBase { /// subtype_iterator begin_communities() const; /// subtype_iterator begin_groups() const; /// subtype_iterator begin_legacy_groups() const; + /// subtype_iterator begin_blinded_one_to_one() const; /// ``` /// /// Inputs: None @@ -597,10 +686,15 @@ class ConvoInfoVolatile : public ConfigBase { subtype_iterator begin_communities() const { return {data}; } subtype_iterator begin_groups() const { return {data}; } subtype_iterator begin_legacy_groups() const { return {data}; } + subtype_iterator begin_blinded_1to1() const { return {data}; } using iterator_category = std::input_iterator_tag; - using value_type = - std::variant; + using value_type = std::variant< + convo::one_to_one, + convo::community, + convo::group, + convo::legacy_group, + convo::blinded_one_to_one>; using reference = value_type&; using pointer = value_type*; using difference_type = std::ptrdiff_t; @@ -609,7 +703,7 @@ class ConvoInfoVolatile : public ConfigBase { protected: std::shared_ptr _val; std::optional _it_11, _end_11, _it_group, _end_group, _it_lgroup, - _end_lgroup; + _end_lgroup, _it_b11, _end_b11; std::optional _it_comm; void _load_val(); iterator() = default; // Constructs an end tombstone @@ -618,8 +712,10 @@ class ConvoInfoVolatile : public ConfigBase { bool oneto1, bool communities, bool groups, - bool legacy_groups); - explicit iterator(const DictFieldRoot& data) : iterator(data, true, true, true, true) {} + bool legacy_groups, + bool blinded_1to1); + explicit iterator(const DictFieldRoot& data) : + iterator(data, true, true, true, true, true) {} friend class ConvoInfoVolatile; public: @@ -645,7 +741,8 @@ class ConvoInfoVolatile : public ConfigBase { std::is_same_v, std::is_same_v, std::is_same_v, - std::is_same_v) {} + std::is_same_v, + std::is_same_v) {} friend class ConvoInfoVolatile; public: diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 20ef0818..f020e562 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,8 +1,12 @@ #include "session/config/contacts.hpp" +#include +#include #include #include +#include +#include #include #include "internal.hpp" @@ -14,8 +18,7 @@ using namespace std::literals; using namespace session::config; - -LIBSESSION_C_API const size_t CONTACT_MAX_NAME_LENGTH = contact_info::MAX_NAME_LENGTH; +using namespace oxen::log::literals; // Check for agreement between various C/C++ types static_assert(sizeof(contacts_contact::name) == contact_info::MAX_NAME_LENGTH + 1); @@ -61,15 +64,6 @@ Contacts::Contacts( load_key(ed25519_secretkey); } -LIBSESSION_C_API int contacts_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - void contact_info::load(const dict& info_dict) { name = string_or_empty(info_dict, "n"); nickname = string_or_empty(info_dict, "N"); @@ -170,6 +164,73 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, created = to_epoch_seconds(c.created); } +blinded_contact_info::blinded_contact_info( + std::string_view base_url, + std::string_view blinded_id, + std::span pubkey, + bool legacy_blinding) : + legacy_blinding{legacy_blinding}, + community(std::move(base_url), blinded_id.substr(2), std::move(pubkey)) { + check_session_id(blinded_id, legacy_blinding ? "15" : "25"); +} + +const std::string blinded_contact_info::session_id() const { + return "{}{}"_format(legacy_blinding ? "15" : "25", room()); +} + +void blinded_contact_info::set_name(std::string n) { + if (n.size() > contact_info::MAX_NAME_LENGTH) + name = utf8_truncate(std::move(n), contact_info::MAX_NAME_LENGTH); + else + name = std::move(n); +} + +void blinded_contact_info::load(const dict& info_dict) { + name = maybe_string(info_dict, "n").value_or(""); + + auto url = maybe_string(info_dict, "p"); + auto key = maybe_vector(info_dict, "q"); + if (url && key && !url->empty() && key->size() == 32) { + profile_picture.url = std::move(*url); + profile_picture.key = std::move(*key); + } else { + profile_picture.clear(); + } + legacy_blinding = maybe_int(info_dict, "y").value_or(0); + created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); +} + +void blinded_contact_info::into(contacts_blinded_contact& c) const { + copy_c_str(c.base_url, base_url()); + c.session_id[0] = (legacy_blinding ? '1' : '2'); + c.session_id[1] = '5'; + std::memcpy(c.session_id + 2, session_id().data(), 64); + c.session_id[66] = '\0'; + std::memcpy(c.pubkey, pubkey().data(), 32); + copy_c_str(c.name, name); + if (profile_picture) { + copy_c_str(c.profile_pic.url, profile_picture.url); + std::memcpy(c.profile_pic.key, profile_picture.key.data(), 32); + } else { + copy_c_str(c.profile_pic.url, ""); + } + c.legacy_blinding = legacy_blinding; + c.created = to_epoch_seconds(created); +} + +blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) : + community(c.base_url, {c.session_id + 2, 64}, c.pubkey) { + assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); + name = c.name; + assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); + if (std::strlen(c.profile_pic.url)) { + profile_picture.url = c.profile_pic.url; + profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); + } + legacy_blinding = c.legacy_blinding; + created = to_epoch_seconds(c.created); +} + std::optional Contacts::get(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); @@ -182,20 +243,6 @@ std::optional Contacts::get(std::string_view pubkey_hex) const { return result; } -LIBSESSION_C_API bool contacts_get( - config_object* conf, contacts_contact* contact, const char* session_id) { - return wrap_exceptions( - conf, - [&] { - if (auto c = unbox(conf)->get(session_id)) { - c->into(*contact); - return true; - } - return false; - }, - false); -} - contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { if (auto maybe = get(pubkey_hex)) return *std::move(maybe); @@ -203,17 +250,6 @@ contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { return contact_info{std::string{pubkey_hex}}; } -LIBSESSION_C_API bool contacts_get_or_construct( - config_object* conf, contacts_contact* contact, const char* session_id) { - return wrap_exceptions( - conf, - [&] { - unbox(conf)->get_or_construct(session_id).into(*contact); - return true; - }, - false); -} - void Contacts::set(const contact_info& contact) { std::string pk = session_id_to_bytes(contact.session_id); auto info = data["c"][pk]; @@ -254,16 +290,6 @@ void Contacts::set(const contact_info& contact) { set_positive_int(info["j"], to_epoch_seconds(contact.created)); } -LIBSESSION_C_API bool contacts_set(config_object* conf, const contacts_contact* contact) { - return wrap_exceptions( - conf, - [&] { - unbox(conf)->set(contact_info{*contact}); - return true; - }, - false); -} - void Contacts::set_name(std::string_view session_id, std::string name) { auto c = get_or_construct(session_id); c.set_name(std::move(name)); @@ -340,22 +366,92 @@ bool Contacts::erase(std::string_view session_id) { return ret; } -LIBSESSION_C_API bool contacts_erase(config_object* conf, const char* session_id) { - try { - return unbox(conf)->erase(session_id); - } catch (...) { - return false; - } -} - size_t Contacts::size() const { if (auto* c = data["c"].dict()) return c->size(); return 0; } -LIBSESSION_C_API size_t contacts_size(const config_object* conf) { - return unbox(conf)->size(); +ConfigBase::DictFieldProxy Contacts::blinded_contact_field( + const blinded_contact_info& bc, std::span* get_pubkey) const { + auto record = data["b"][bc.base_url()]; + if (get_pubkey) { + auto pkrec = record["#"]; + if (auto pk = pkrec.string_view_or(""); pk.size() == 32) + *get_pubkey = std::span{ + reinterpret_cast(pk.data()), pk.size()}; + } + return record["R"][bc.room()]; // The `room` value is the blinded id without the prefix +} + +using any_blinded_contact = std::variant; + +std::optional Contacts::get_blinded( + std::string_view pubkey_hex, bool legacy_blinding) const { + check_session_id(pubkey_hex, legacy_blinding ? "15" : "25"); + + if (auto* b = data["b"].dict()) { + auto comm = comm_iterator_helper{b->begin(), b->end()}; + std::shared_ptr val; + + while (!comm.done()) { + if (comm.load(val)) // TODO: This is untested + if (auto* ptr = std::get_if(val.get()); + ptr && ptr->session_id() == pubkey_hex) + return *ptr; + comm.advance(); + } + } + + return std::nullopt; +} + +std::vector Contacts::blinded_contacts() const { + std::vector ret; + + if (auto* b = data["b"].dict()) { + auto comm = comm_iterator_helper{b->begin(), b->end()}; + std::shared_ptr val; + + while (!comm.done()) { + if (comm.load(val)) + if (auto* ptr = std::get_if(val.get())) + ret.emplace_back(*ptr); + comm.advance(); + } + } + + return ret; +} + +bool Contacts::set_blinded_contact(const blinded_contact_info& bc) { + data["b"][bc.base_url()]["#"] = bc.pubkey(); + auto info = blinded_contact_field(bc); // data["b"][base]["R"][bc_session_id_without_prefix] + + // Always set the name, even if empty, to keep the dict from getting pruned if there are no + // other entries. + info["n"] = bc.name.substr(0, contact_info::MAX_NAME_LENGTH); + + set_pair_if( + bc.profile_picture, + info["p"], + bc.profile_picture.url, + info["q"], + bc.profile_picture.key); + + set_positive_int(info["y"], bc.legacy_blinding); + set_positive_int(info["j"], to_epoch_seconds(bc.created)); +} + +bool Contacts::erase_blinded_contact( + std::string_view base_url_, std::string_view blinded_id, bool legacy_blinding) { + std::string pk = session_id_to_bytes(blinded_id, legacy_blinding ? "15" : "25").substr(2); + + auto base_url = community::canonical_url(base_url_); + auto info = data["d"][base_url]["R"][pk]; + bool ret = info.exists(); + info.erase(); + return ret; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -398,6 +494,149 @@ Contacts::iterator& Contacts::iterator::operator++() { return *this; } +extern "C" { + +LIBSESSION_C_API const size_t CONTACT_MAX_NAME_LENGTH = contact_info::MAX_NAME_LENGTH; + +LIBSESSION_C_API int contacts_init( + config_object** conf, + const unsigned char* ed25519_secretkey_bytes, + const unsigned char* dumpstr, + size_t dumplen, + char* error) { + return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); +} + +LIBSESSION_C_API bool contacts_get( + config_object* conf, contacts_contact* contact, const char* session_id) { + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get(session_id)) { + c->into(*contact); + return true; + } + return false; + }, + false); +} + +LIBSESSION_C_API bool contacts_get_or_construct( + config_object* conf, contacts_contact* contact, const char* session_id) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->get_or_construct(session_id).into(*contact); + return true; + }, + false); +} + +LIBSESSION_C_API bool contacts_set(config_object* conf, const contacts_contact* contact) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(contact_info{*contact}); + return true; + }, + false); +} + +LIBSESSION_C_API bool contacts_erase(config_object* conf, const char* session_id) { + try { + return unbox(conf)->erase(session_id); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API size_t contacts_size(const config_object* conf) { + return unbox(conf)->size(); +} + +LIBSESSION_C_API bool contacts_get_blinded_contact( + config_object* conf, + const char* blinded_session_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) { + return wrap_exceptions( + conf, + [&] { + if (auto bc = unbox(conf)->get_blinded( + blinded_session_id, legacy_blinding)) { + bc->into(*blinded_contact); + return true; + } + return false; + }, + false); +} + +LIBSESSION_C_API contacts_blinded_contact_list* contacts_blinded_contacts( + const config_object* conf) { + try { + auto cpp_contacts = unbox(conf)->blinded_contacts(); + + if (cpp_contacts.empty()) + return nullptr; + + // We malloc space for the contacts_blinded_contact_list struct itself, plus the required + // number of contacts_blinded_contact pointers to store its records, and the space to + // actually contain a copy of the data. When we're done, the malloced memory we grab is + // going to look like this: + // + // {contacts_blinded_contact_list} + // {pointer1}{pointer2}... + // {contacts_blinded_contact data 1\0}{contacts_blinded_contact data 2\0}... + // + // where contacts_blinded_contact.value points at the beginning of {pointer1}, and each + // pointerN points at the beginning of the {contacts_blinded_contact data N\0} struct. + // + // Since we malloc it all at once, when the user frees it, they also free the entire thing. + size_t sz = sizeof(contacts_blinded_contact_list) + + (cpp_contacts.size() * sizeof(contacts_blinded_contact*)) + + (cpp_contacts.size() * sizeof(contacts_blinded_contact)); + auto* ret = static_cast(std::malloc(sz)); + ret->len = cpp_contacts.size(); + + // value points at the space immediately after the struct itself, which is the first element + // in the array of contacts_blinded_contact pointers. + ret->value = reinterpret_cast(ret + 1); + contacts_blinded_contact* next_struct = + reinterpret_cast(ret->value + ret->len); + + for (size_t i = 0; i < cpp_contacts.size(); ++i) { + ret->value[i] = next_struct; + cpp_contacts[i].into(*next_struct); + next_struct++; + } + + return ret; + } catch (...) { + return nullptr; + } +} + +LIBSESSION_C_API bool contacts_set_blinded_contact( + config_object* conf, const contacts_blinded_contact* bc) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_blinded_contact(blinded_contact_info{*bc}); + return true; + }, + false); +} + +LIBSESSION_C_API bool contacts_erase_blinded_contact( + config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding) { + try { + return unbox(conf)->erase_blinded_contact(base_url, blinded_id, legacy_blinding); + } catch (...) { + return false; + } +} + LIBSESSION_C_API contacts_iterator* contacts_iterator_new(const config_object* conf) { auto* it = new contacts_iterator{}; it->_internals = new Contacts::iterator{unbox(conf)->begin()}; @@ -420,3 +659,5 @@ LIBSESSION_C_API bool contacts_iterator_done(contacts_iterator* it, contacts_con LIBSESSION_C_API void contacts_iterator_advance(contacts_iterator* it) { ++*static_cast(it->_internals); } + +} // extern "C" \ No newline at end of file diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 14e291ad..54f73b62 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -82,6 +82,26 @@ namespace convo { c.unread = unread; } + blinded_one_to_one::blinded_one_to_one(std::string&& sid, bool legacy_blinding) : + blinded_session_id{std::move(sid)}, legacy_blinding{legacy_blinding} { + check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); + } + blinded_one_to_one::blinded_one_to_one(std::string_view sid, bool legacy_blinding) : + blinded_session_id{sid}, legacy_blinding{legacy_blinding} { + check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); + } + blinded_one_to_one::blinded_one_to_one(const convo_info_volatile_blinded_1to1& c) : + base{c.last_read, c.unread}, + blinded_session_id{c.blinded_session_id, 66}, + legacy_blinding{c.legacy_blinding} {} + + void blinded_one_to_one::into(convo_info_volatile_blinded_1to1& c) const { + std::memcpy(c.blinded_session_id, blinded_session_id.data(), 67); + c.last_read = last_read; + c.unread = unread; + c.legacy_blinding = legacy_blinding; + } + void base::load(const dict& info_dict) { last_read = int_or_0(info_dict, "r"); unread = (bool)int_or_0(info_dict, "u"); @@ -213,6 +233,28 @@ convo::legacy_group ConvoInfoVolatile::get_or_construct_legacy_group( return convo::legacy_group{std::string{pubkey_hex}}; } +std::optional ConvoInfoVolatile::get_blinded_1to1( + std::string_view pubkey_hex, bool legacy_blinding) const { + std::string pubkey = session_id_to_bytes(pubkey_hex, legacy_blinding ? "15" : "25"); + + auto* info_dict = data["b"][pubkey].dict(); + if (!info_dict) + return std::nullopt; + + auto result = + std::make_optional(std::string{pubkey_hex}, legacy_blinding); + result->load(*info_dict); + return result; +} + +convo::blinded_one_to_one ConvoInfoVolatile::get_or_construct_blinded_1to1( + std::string_view pubkey_hex, bool legacy_blinding) const { + if (auto maybe = get_blinded_1to1(pubkey_hex, legacy_blinding)) + return *std::move(maybe); + + return convo::blinded_one_to_one{std::string{pubkey_hex}, legacy_blinding}; +} + void ConvoInfoVolatile::set(const convo::one_to_one& c) { auto info = data["1"][session_id_to_bytes(c.session_id)]; set_base(c, info); @@ -286,6 +328,14 @@ void ConvoInfoVolatile::set(const convo::legacy_group& c) { set_base(c, info); } +void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { + std::string pubkey = session_id_to_bytes(c.blinded_session_id, c.legacy_blinding ? "15" : "25"); + + auto info = data["b"][pubkey]; + set_nonzero_int(info["y"], c.legacy_blinding); + set_base(c, info); +} + template static bool erase_impl(Field convo) { bool ret = convo.exists(); @@ -315,6 +365,11 @@ bool ConvoInfoVolatile::erase(const convo::group& c) { bool ConvoInfoVolatile::erase(const convo::legacy_group& c) { return erase_impl(data["C"][session_id_to_bytes(c.id)]); } +bool ConvoInfoVolatile::erase(const convo::blinded_one_to_one& c) { + std::string pubkey = session_id_to_bytes(c.blinded_session_id, c.legacy_blinding ? "15" : "25"); + + return erase_impl(data["b"][pubkey]); +} bool ConvoInfoVolatile::erase(const convo::any& c) { return std::visit([this](const auto& c) { return erase(c); }, c); @@ -331,6 +386,10 @@ bool ConvoInfoVolatile::erase_group(std::string_view id) { bool ConvoInfoVolatile::erase_legacy_group(std::string_view id) { return erase(convo::legacy_group{id}); } +bool ConvoInfoVolatile::erase_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) { + return erase(convo::blinded_one_to_one{blinded_session_id, legacy_blinding}); +} size_t ConvoInfoVolatile::size_1to1() const { if (auto* d = data["1"].dict()) @@ -366,12 +425,24 @@ size_t ConvoInfoVolatile::size_legacy_groups() const { return 0; } +size_t ConvoInfoVolatile::size_blinded_1to1() const { + if (auto* d = data["b"].dict()) + return d->size(); + return 0; +} + size_t ConvoInfoVolatile::size() const { - return size_1to1() + size_communities() + size_legacy_groups() + size_groups(); + return size_1to1() + size_communities() + size_legacy_groups() + size_groups() + + size_blinded_1to1(); } ConvoInfoVolatile::iterator::iterator( - const DictFieldRoot& data, bool oneto1, bool communities, bool groups, bool legacy_groups) { + const DictFieldRoot& data, + bool oneto1, + bool communities, + bool groups, + bool legacy_groups, + bool blinded_1to1) { if (oneto1) if (auto* d = data["1"].dict()) { _it_11 = d->begin(); @@ -390,6 +461,11 @@ ConvoInfoVolatile::iterator::iterator( _it_lgroup = d->begin(); _end_lgroup = d->end(); } + if (blinded_1to1) + if (auto* d = data["b"].dict()) { + _it_b11 = d->begin(); + _end_b11 = d->end(); + } _load_val(); } @@ -400,7 +476,8 @@ class val_loader { std::shared_ptr& val, std::optional& it, std::optional& end, - char prefix) { + char prefix, + std::optional legacy_prefix = std::nullopt) { while (it) { if (*it == *end) { it.reset(); @@ -410,9 +487,13 @@ class val_loader { auto& [k, v] = **it; - if (k.size() == 33 && k[0] == prefix) { + if (k.size() == 33 && (k[0] == prefix || (legacy_prefix && k[0] == *legacy_prefix))) { if (auto* info_dict = std::get_if(&v)) { - val = std::make_shared(ConvoType{oxenc::to_hex(k)}); + if constexpr (std::is_same_v) + val = std::make_shared(ConvoType{ + oxenc::to_hex(k), (legacy_prefix && k[0] == *legacy_prefix)}); + else + val = std::make_shared(ConvoType{oxenc::to_hex(k)}); std::get(*val).load(*info_dict); return true; } @@ -425,7 +506,7 @@ class val_loader { /// Load _val from the current iterator position; if it is invalid, skip to the next key until we /// find one that is valid (or hit the end). We also span across four different iterators: we -/// exhaust, in order: _it_11, _it_group, _it_comm, _it_lgroup. +/// exhaust, in order: _it_11, _it_group, _it_comm, _it_lgroup, _it_b11. /// /// We *always* call this after incrementing the iterator (and after iterator initialization), and /// this is responsible for making sure that _it_11, _it_group, etc. are only set to non-nullopt if @@ -448,15 +529,18 @@ void ConvoInfoVolatile::iterator::_load_val() { if (val_loader::load(_val, _it_lgroup, _end_lgroup, 0x05)) return; + + if (val_loader::load(_val, _it_b11, _end_b11, 0x25, 0x15)) + return; } bool ConvoInfoVolatile::iterator::operator==(const iterator& other) const { return _it_11 == other._it_11 && _it_group == other._it_group && _it_comm == other._it_comm && - _it_lgroup == other._it_lgroup; + _it_lgroup == other._it_lgroup && _it_b11 == other._it_b11; } bool ConvoInfoVolatile::iterator::done() const { - return !_it_11 && !_it_group && (!_it_comm || _it_comm->done()) && !_it_lgroup; + return !_it_11 && !_it_group && (!_it_comm || _it_comm->done()) && !_it_lgroup && !_it_b11; } ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { @@ -466,9 +550,11 @@ ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { ++*_it_group; else if (_it_comm && !_it_comm->done()) _it_comm->advance(); - else { - assert(_it_lgroup); + else if (_it_lgroup) ++*_it_lgroup; + else { + assert(_it_b11); + ++*_it_b11; } _load_val(); return *this; @@ -604,6 +690,40 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_legacy_group( false); } +LIBSESSION_C_API bool convo_info_volatile_get_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_blinded_1to1( + blinded_session_id, legacy_blinding)) { + c->into(*convo); + return true; + } + return false; + }, + false); +} + +LIBSESSION_C_API bool convo_info_volatile_get_or_construct_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + unbox(conf) + ->get_or_construct_blinded_1to1(blinded_session_id, legacy_blinding) + .into(*convo); + return true; + }, + false); +} + LIBSESSION_C_API bool convo_info_volatile_set_1to1( config_object* conf, const convo_info_volatile_1to1* convo) { return wrap_exceptions( @@ -645,6 +765,17 @@ LIBSESSION_C_API bool convo_info_volatile_set_legacy_group( false); } +LIBSESSION_C_API bool convo_info_volatile_set_blinded_1to1( + config_object* conf, const convo_info_volatile_blinded_1to1* convo) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::blinded_one_to_one{*convo}); + return true; + }, + false); +} + LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { return wrap_exceptions( conf, [&] { return unbox(conf)->erase_1to1(session_id); }, false); @@ -667,6 +798,16 @@ LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( [&] { return unbox(conf)->erase_legacy_group(group_id); }, false); } +LIBSESSION_C_API bool convo_info_volatile_erase_blinded_1to1( + config_object* conf, const char* blinded_session_id, bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + return unbox(conf)->erase_blinded_1to1( + blinded_session_id, legacy_blinding); + }, + false); +} LIBSESSION_C_API size_t convo_info_volatile_size(const config_object* conf) { return unbox(conf)->size(); @@ -683,6 +824,9 @@ LIBSESSION_C_API size_t convo_info_volatile_size_groups(const config_object* con LIBSESSION_C_API size_t convo_info_volatile_size_legacy_groups(const config_object* conf) { return unbox(conf)->size_legacy_groups(); } +LIBSESSION_C_API size_t convo_info_volatile_size_blinded_1to1(const config_object* conf) { + return unbox(conf)->size_blinded_1to1(); +} LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new( const config_object* conf) { @@ -718,6 +862,13 @@ LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_ new ConvoInfoVolatile::iterator{unbox(conf)->begin_legacy_groups()}; return it; } +LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_blinded_1to1( + const config_object* conf) { + auto* it = new convo_info_volatile_iterator{}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(conf)->begin_blinded_1to1()}; + return it; +} LIBSESSION_C_API void convo_info_volatile_iterator_free(convo_info_volatile_iterator* it) { delete static_cast(it->_internals); @@ -764,3 +915,8 @@ LIBSESSION_C_API bool convo_info_volatile_it_is_legacy_group( convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c) { return convo_info_volatile_it_is_impl(it, c); } + +LIBSESSION_C_API bool convo_info_volatile_it_is_blinded_1to1( + convo_info_volatile_iterator* it, convo_info_volatile_blinded_1to1* c) { + return convo_info_volatile_it_is_impl(it, c); +} From fb9546808d49457bbd9ac72d9ead709ac21bb764 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 26 Jun 2025 09:06:56 +1000 Subject: [PATCH 09/14] Comment fixes from PR feedback --- include/session/config/contacts.h | 2 +- include/session/config/contacts.hpp | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index d45318b7..e057ee73 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -43,7 +43,7 @@ typedef struct contacts_blinded_contact { // only has port if non-default, has trailing / removed) unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls) - char name[101]; // This will be a 0-length strings when unset + char name[101]; // This will be a 0-length string when unset user_profile_pic profile_pic; bool legacy_blinding; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index c4153c8c..0ebde396 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -48,14 +48,13 @@ namespace session::config { /// equivalent "j"oined field). Omitted if 0. /// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. /// -/// b - dict of blinded contacts. This is a nested dict where the outkey keys are the BASE_URL of +/// b - dict of blinded contacts. This is a nested dict where the outer keys are the BASE_URL of /// the community the blinded contact originated from and the outer value is a dict containing: /// /// `#` - the 32-byte server pubkey /// `R` - dict of blinded contacts from the server; each key is the blinded session pubkey /// without the prefix ("R" to match user_groups equivalent "R"oom field, and to make use of /// existing community iterators, binary, 32 bytes), value is a dict containing keys: -/// containing keys: /// /// n - contact name (string). This is always serialized, even if empty (but empty indicates /// no name) so that we always have at least one key set (required to keep the dict value @@ -414,7 +413,7 @@ class Contacts : public ConfigBase { bool accepts_protobuf() const override { return true; } protected: - // Drills into the nested dicts to access open group details + // Drills into the nested dicts to access community details DictFieldProxy blinded_contact_field( const blinded_contact_info& bc, std::span* get_pubkey = nullptr) const; From b42905c8e79fe661f5f3775a25d1aaecdd3cde9e Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 26 Jun 2025 09:15:16 +1000 Subject: [PATCH 10/14] Updated `blinded_contact_info` to not inherit from `community` --- include/session/config/contacts.hpp | 12 +++++++-- src/config/contacts.cpp | 38 ++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 0ebde396..eab97b24 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -118,8 +118,8 @@ struct contact_info { void load(const dict& info_dict); }; -struct blinded_contact_info : community { - using community::community; +struct blinded_contact_info { + community comm; const std::string session_id() const; // in hex std::string name; @@ -127,6 +127,7 @@ struct blinded_contact_info : community { bool legacy_blinding; int64_t created = 0; // Unix timestamp (seconds) when this contact was added + blinded_contact_info() = default; explicit blinded_contact_info( std::string_view base_url, std::string_view blinded_id, @@ -153,6 +154,13 @@ struct blinded_contact_info : community { /// - `name` -- Name to assign to the contact void set_name(std::string name); + /// These functions are here so we can use the `comm_iterator_helper` for loading data + /// into this struct + void set_base_url(std::string_view base_url); + void set_room(std::string_view room); + void set_pubkey(std::span pubkey); + void set_pubkey(std::string_view pubkey); + private: friend class Contacts; friend struct session::config::comm_iterator_helper; diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index f020e562..79a39945 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -169,13 +169,13 @@ blinded_contact_info::blinded_contact_info( std::string_view blinded_id, std::span pubkey, bool legacy_blinding) : - legacy_blinding{legacy_blinding}, - community(std::move(base_url), blinded_id.substr(2), std::move(pubkey)) { + comm{community(std::move(base_url), blinded_id.substr(2), std::move(pubkey))}, + legacy_blinding{legacy_blinding} { check_session_id(blinded_id, legacy_blinding ? "15" : "25"); } const std::string blinded_contact_info::session_id() const { - return "{}{}"_format(legacy_blinding ? "15" : "25", room()); + return "{}{}"_format(legacy_blinding ? "15" : "25", comm.room()); } void blinded_contact_info::set_name(std::string n) { @@ -185,6 +185,22 @@ void blinded_contact_info::set_name(std::string n) { name = std::move(n); } +void blinded_contact_info::set_base_url(std::string_view base_url) { + comm.set_base_url(base_url); +} + +void blinded_contact_info::set_room(std::string_view room) { + comm.set_room(room); +} + +void blinded_contact_info::set_pubkey(std::span pubkey) { + comm.set_pubkey(pubkey); +} + +void blinded_contact_info::set_pubkey(std::string_view pubkey) { + comm.set_pubkey(pubkey); +} + void blinded_contact_info::load(const dict& info_dict) { name = maybe_string(info_dict, "n").value_or(""); @@ -201,12 +217,12 @@ void blinded_contact_info::load(const dict& info_dict) { } void blinded_contact_info::into(contacts_blinded_contact& c) const { - copy_c_str(c.base_url, base_url()); + copy_c_str(c.base_url, comm.base_url()); c.session_id[0] = (legacy_blinding ? '1' : '2'); c.session_id[1] = '5'; std::memcpy(c.session_id + 2, session_id().data(), 64); c.session_id[66] = '\0'; - std::memcpy(c.pubkey, pubkey().data(), 32); + std::memcpy(c.pubkey, comm.pubkey().data(), 32); copy_c_str(c.name, name); if (profile_picture) { copy_c_str(c.profile_pic.url, profile_picture.url); @@ -218,8 +234,8 @@ void blinded_contact_info::into(contacts_blinded_contact& c) const { c.created = to_epoch_seconds(created); } -blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) : - community(c.base_url, {c.session_id + 2, 64}, c.pubkey) { +blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { + comm = community(c.base_url, {c.session_id + 2, 64}, c.pubkey); assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); name = c.name; assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); @@ -374,14 +390,14 @@ size_t Contacts::size() const { ConfigBase::DictFieldProxy Contacts::blinded_contact_field( const blinded_contact_info& bc, std::span* get_pubkey) const { - auto record = data["b"][bc.base_url()]; + auto record = data["b"][bc.comm.base_url()]; if (get_pubkey) { auto pkrec = record["#"]; if (auto pk = pkrec.string_view_or(""); pk.size() == 32) *get_pubkey = std::span{ reinterpret_cast(pk.data()), pk.size()}; } - return record["R"][bc.room()]; // The `room` value is the blinded id without the prefix + return record["R"][bc.comm.room()]; // The `room` value is the blinded id without the prefix } using any_blinded_contact = std::variant; @@ -395,7 +411,7 @@ std::optional Contacts::get_blinded( std::shared_ptr val; while (!comm.done()) { - if (comm.load(val)) // TODO: This is untested + if (comm.load(val)) if (auto* ptr = std::get_if(val.get()); ptr && ptr->session_id() == pubkey_hex) return *ptr; @@ -425,7 +441,7 @@ std::vector Contacts::blinded_contacts() const { } bool Contacts::set_blinded_contact(const blinded_contact_info& bc) { - data["b"][bc.base_url()]["#"] = bc.pubkey(); + data["b"][bc.comm.base_url()]["#"] = bc.comm.pubkey(); auto info = blinded_contact_field(bc); // data["b"][base]["R"][bc_session_id_without_prefix] // Always set the name, even if empty, to keep the dict from getting pruned if there are no From cd58ab39a011bbfde6105fac042adaa02f2e3c35 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 16 Jul 2025 08:38:02 +1000 Subject: [PATCH 11/14] Added unit tests for blinded contacts, bug fixes and cleanup --- include/session/config/contacts.h | 67 ++++-- include/session/config/contacts.hpp | 52 +++-- src/config/contacts.cpp | 247 +++++++++++++--------- tests/test_config_contacts.cpp | 210 ++++++++++++++++++ tests/test_config_convo_info_volatile.cpp | 120 ++++++++++- 5 files changed, 562 insertions(+), 134 deletions(-) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index e057ee73..08c47fbc 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -251,8 +251,7 @@ LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); /// Outputs: /// - `contacts_blinded_contact_list*` -- pointer to the list of blinded contact structs; the /// pointer belongs to the caller and must be freed when done with it. -LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded_contacts( - const config_object* conf); +LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded(const config_object* conf); /// API: contacts/contacts_get_blinded_contact /// @@ -264,7 +263,7 @@ LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded_contacts( /// ```cpp /// BOOL contacts_get_blinded_contact( /// [in] config_object* conf, -/// [in] const char* blinded_session_id, +/// [in] const char* blinded_id, /// [in] bool legacy_blinding, /// [out] contacts_blinded_contact* blinded_contact /// ); @@ -272,19 +271,60 @@ LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded_contacts( /// /// Inputs: /// - `conf` -- [in] Pointer to the config object -/// - `blinded_session_id` -- [in] null terminated hex string +/// - `blinded_id` -- [in] null terminated hex string /// - `legacy_blinding` -- [in] null terminated hex string /// - `blinded_contact` -- [out] the blinded contact info data /// /// Output: /// - `bool` -- Returns true if blinded contact exists -LIBSESSION_EXPORT bool contacts_get_blinded_contact( +LIBSESSION_EXPORT bool contacts_get_blinded( config_object* conf, - const char* blinded_session_id, + const char* blinded_id, bool legacy_blinding, contacts_blinded_contact* blinded_contact) LIBSESSION_WARN_UNUSED; -/// API: contacts/contacts_set_blinded_contact +/// API: contacts/contacts_get_or_construct_blinded +/// +/// Same as the above `contacts_get_blinded()` except that when the blinded contact does not exist, +/// this sets all the contact fields to defaults and loads it with the given blinded_id. +/// +/// Returns true as long as it is given a valid blinded_id. A false return is considered an error, +/// and means the blinded_id was not a valid blinded_id. +/// +/// This is the method that should usually be used to create or update a blinded contact, followed +/// by setting fields in the blinded contact, and then giving it to contacts_set_blinded(). +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_get_or_construct_blinded( +/// [in] config_object* conf, +/// [in] const char* community_base_url, +/// [in] const char* community_pubkey_hex, +/// [in] const char* blinded_id, +/// [in] bool legacy_blinding, +/// [out] contacts_blinded_contact* blinded_contact +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `community_base_url` -- [in] null terminated string +/// - `community_pubkey_hex` -- [in] null terminated hex string +/// - `blinded_id` -- [in] null terminated hex string +/// - `legacy_blinding` -- [in] null terminated hex string +/// - `blinded_contact` -- [out] the blinded contact info data +/// +/// Output: +/// - `bool` -- Returns true if contact exsts +LIBSESSION_EXPORT bool contacts_get_or_construct_blinded( + config_object* conf, + const char* community_base_url, + const char* community_pubkey_hex, + const char* blinded_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) LIBSESSION_WARN_UNUSED; + +/// API: contacts/contacts_set_blinded /// /// Adds or updates a blinded contact from the given contact info struct. /// @@ -302,19 +342,19 @@ LIBSESSION_EXPORT bool contacts_get_blinded_contact( /// /// Output: /// - `bool` -- Returns true if the call succeeds, false if an error occurs. -LIBSESSION_EXPORT bool contacts_set_blinded_contact( +LIBSESSION_EXPORT bool contacts_set_blinded( config_object* conf, const contacts_blinded_contact* bc); -/// API: contacts/contacts_erase_blinded_contact +/// API: contacts/contacts_erase_blinded /// /// Erases a blinded contact from the blinded contact list. blinded_id is in hex. Returns true if /// the blinded contact was found and removed, false if the blinded contact was not present. /// /// Declaration: /// ```cpp -/// BOOL contacts_erase_blinded_contact( +/// BOOL contacts_erase_blinded( /// [in, out] config_object* conf, -/// [in] const char* base_url, +/// [in] const char* community_base_url, /// [in] const char* blinded_id, /// [in] bool legacy_blinding /// ); @@ -330,7 +370,10 @@ LIBSESSION_EXPORT bool contacts_set_blinded_contact( /// Outputs: /// - `bool` -- True if erasing was successful LIBSESSION_EXPORT bool contacts_erase_blinded_contact( - config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding); + config_object* conf, + const char* community_base_url, + const char* blinded_id, + bool legacy_blinding); typedef struct contacts_iterator { void* _internals; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index eab97b24..576a0094 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -129,9 +129,9 @@ struct blinded_contact_info { blinded_contact_info() = default; explicit blinded_contact_info( - std::string_view base_url, + std::string_view community_base_url, + std::span community_pubkey, std::string_view blinded_id, - std::span pubkey, bool legacy_blinding); // Internal ctor/method for C API implementations: @@ -427,7 +427,7 @@ class Contacts : public ConfigBase { std::span* get_pubkey = nullptr) const; public: - /// API: contacts/Contacts::blinded_contacts + /// API: contacts/Contacts::blinded /// /// Retrieves a list of all known blinded contacts. /// @@ -435,7 +435,7 @@ class Contacts : public ConfigBase { /// /// Outputs: /// - `std::vector` - Returns a list of blinded_contact_info - std::vector blinded_contacts() const; + std::vector blinded() const; /// API: contacts/Contacts::get_blinded /// @@ -443,32 +443,58 @@ class Contacts : public ConfigBase { /// blinded session ID was not found, otherwise returns a filled out `blinded_contact_info`. /// /// Inputs: - /// - `pubkey_hex` -- hex string of the session id + /// - `blinded_id_hex` -- hex string of the session id /// - `legacy_blinding` -- flag indicating whether the pubkey is using legacy blinding /// /// Outputs: /// - `std::optional` - Returns nullopt if blinded session ID was not /// found, otherwise a filled out blinded_contact_info std::optional get_blinded( - std::string_view pubkey_hex, bool legacy_blinding) const; + std::string_view blinded_id_hex, bool legacy_blinding) const; + + /// API: contacts/Contacts::get_or_construct_blinded + /// + /// Similar to get_blinded(), but if the blinded ID does not exist this returns a filled-out + /// blinded_contact_info containing the blinded_id, community info and legacy_blinded flag (all + /// other fields will be empty/defaulted). This is intended to be combined with `set_blinded` + /// to set-or-create a record. + /// + /// NB: calling this does *not* add the blinded id to the blinded list when called: that + /// requires also calling `set_blinded` with this value. + /// + /// Inputs: + /// - `community_base_url` -- String of the base URL for the community this blinded id + /// originates from + /// - `community_pubkey_hex` -- Hex string of the public key for the community this blinded id + /// originates from + /// - `blinded_id_hex` -- hex string of the blinded id + /// - `legacy_blinding` -- flag indicating whether the pubkey is using legacy blinding + /// + /// Outputs: + /// - `blinded_contact_info` - Returns a filled out blinded_contact_info + blinded_contact_info get_or_construct_blinded( + std::string_view community_base_url, + std::string_view community_pubkey_hex, + std::string_view blinded_id_hex, + bool legacy_blinding); - /// API: contacts/contacts::set_blinded_contact + /// API: contacts/contacts::set_blinded /// /// Sets or updates multiple blinded contact info values at once with the given info. The usual /// use is to access the current info, change anything desired, then pass it back into - /// set_blinded_contact, e.g.: + /// set_blinded, e.g.: /// ///```cpp /// auto c = contacts.get_blinded(pubkey, legacy_blinding); /// c.name = "Session User 42"; - /// contacts.set_blinded_contact(c); + /// contacts.set_blinded(c); ///``` /// /// Inputs: - /// - `bc` -- set_blinded_contact value to set - bool set_blinded_contact(const blinded_contact_info& bc); + /// - `bc` -- set_blinded value to set + void set_blinded(const blinded_contact_info& bc); - /// API: contacts/contacts::erase_blinded_contact + /// API: contacts/contacts::erase_blinded /// /// Removes a blinded contact, if present. Returns true if it was found and removed, false /// otherwise. Note that this removes all fields related to a blinded contact, even fields we do @@ -481,7 +507,7 @@ class Contacts : public ConfigBase { /// /// Outputs: /// - `bool` - Returns true if contact was found and removed, false otherwise - bool erase_blinded_contact( + bool erase_blinded( std::string_view base_url, std::string_view blinded_id, bool legacy_blinding); struct iterator; diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 79a39945..78af882b 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -164,89 +164,6 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, created = to_epoch_seconds(c.created); } -blinded_contact_info::blinded_contact_info( - std::string_view base_url, - std::string_view blinded_id, - std::span pubkey, - bool legacy_blinding) : - comm{community(std::move(base_url), blinded_id.substr(2), std::move(pubkey))}, - legacy_blinding{legacy_blinding} { - check_session_id(blinded_id, legacy_blinding ? "15" : "25"); -} - -const std::string blinded_contact_info::session_id() const { - return "{}{}"_format(legacy_blinding ? "15" : "25", comm.room()); -} - -void blinded_contact_info::set_name(std::string n) { - if (n.size() > contact_info::MAX_NAME_LENGTH) - name = utf8_truncate(std::move(n), contact_info::MAX_NAME_LENGTH); - else - name = std::move(n); -} - -void blinded_contact_info::set_base_url(std::string_view base_url) { - comm.set_base_url(base_url); -} - -void blinded_contact_info::set_room(std::string_view room) { - comm.set_room(room); -} - -void blinded_contact_info::set_pubkey(std::span pubkey) { - comm.set_pubkey(pubkey); -} - -void blinded_contact_info::set_pubkey(std::string_view pubkey) { - comm.set_pubkey(pubkey); -} - -void blinded_contact_info::load(const dict& info_dict) { - name = maybe_string(info_dict, "n").value_or(""); - - auto url = maybe_string(info_dict, "p"); - auto key = maybe_vector(info_dict, "q"); - if (url && key && !url->empty() && key->size() == 32) { - profile_picture.url = std::move(*url); - profile_picture.key = std::move(*key); - } else { - profile_picture.clear(); - } - legacy_blinding = maybe_int(info_dict, "y").value_or(0); - created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); -} - -void blinded_contact_info::into(contacts_blinded_contact& c) const { - copy_c_str(c.base_url, comm.base_url()); - c.session_id[0] = (legacy_blinding ? '1' : '2'); - c.session_id[1] = '5'; - std::memcpy(c.session_id + 2, session_id().data(), 64); - c.session_id[66] = '\0'; - std::memcpy(c.pubkey, comm.pubkey().data(), 32); - copy_c_str(c.name, name); - if (profile_picture) { - copy_c_str(c.profile_pic.url, profile_picture.url); - std::memcpy(c.profile_pic.key, profile_picture.key.data(), 32); - } else { - copy_c_str(c.profile_pic.url, ""); - } - c.legacy_blinding = legacy_blinding; - c.created = to_epoch_seconds(created); -} - -blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { - comm = community(c.base_url, {c.session_id + 2, 64}, c.pubkey); - assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); - name = c.name; - assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); - if (std::strlen(c.profile_pic.url)) { - profile_picture.url = c.profile_pic.url; - profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); - } - legacy_blinding = c.legacy_blinding; - created = to_epoch_seconds(c.created); -} - std::optional Contacts::get(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); @@ -388,6 +305,90 @@ size_t Contacts::size() const { return 0; } +blinded_contact_info::blinded_contact_info( + std::string_view community_base_url, + std::span community_pubkey, + std::string_view blinded_id, + bool legacy_blinding) : + comm{community( + std::move(community_base_url), blinded_id.substr(2), std::move(community_pubkey))}, + legacy_blinding{legacy_blinding} { + check_session_id(blinded_id, legacy_blinding ? "15" : "25"); +} + +void blinded_contact_info::load(const dict& info_dict) { + name = maybe_string(info_dict, "n").value_or(""); + + auto url = maybe_string(info_dict, "p"); + auto key = maybe_vector(info_dict, "q"); + if (url && key && !url->empty() && key->size() == 32) { + profile_picture.url = std::move(*url); + profile_picture.key = std::move(*key); + } else { + profile_picture.clear(); + } + legacy_blinding = maybe_int(info_dict, "y").value_or(0); + created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); +} + +void blinded_contact_info::into(contacts_blinded_contact& c) const { + copy_c_str(c.base_url, comm.base_url()); + c.session_id[0] = (legacy_blinding ? '1' : '2'); + c.session_id[1] = '5'; + std::memcpy(c.session_id + 2, session_id().data(), 64); + c.session_id[66] = '\0'; + std::memcpy(c.pubkey, comm.pubkey().data(), 32); + copy_c_str(c.name, name); + if (profile_picture) { + copy_c_str(c.profile_pic.url, profile_picture.url); + std::memcpy(c.profile_pic.key, profile_picture.key.data(), 32); + } else { + copy_c_str(c.profile_pic.url, ""); + } + c.legacy_blinding = legacy_blinding; + c.created = to_epoch_seconds(created); +} + +blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { + comm = community(c.base_url, {c.session_id + 2, 64}, c.pubkey); + assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); + name = c.name; + assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); + if (std::strlen(c.profile_pic.url)) { + profile_picture.url = c.profile_pic.url; + profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); + } + legacy_blinding = c.legacy_blinding; + created = to_epoch_seconds(c.created); +} + +const std::string blinded_contact_info::session_id() const { + return "{}{}"_format(legacy_blinding ? "15" : "25", comm.room()); +} + +void blinded_contact_info::set_name(std::string n) { + if (n.size() > contact_info::MAX_NAME_LENGTH) + name = utf8_truncate(std::move(n), contact_info::MAX_NAME_LENGTH); + else + name = std::move(n); +} + +void blinded_contact_info::set_base_url(std::string_view base_url) { + comm.set_base_url(base_url); +} + +void blinded_contact_info::set_room(std::string_view room) { + comm.set_room(room); +} + +void blinded_contact_info::set_pubkey(std::span pubkey) { + comm.set_pubkey(pubkey); +} + +void blinded_contact_info::set_pubkey(std::string_view pubkey) { + comm.set_pubkey(pubkey); +} + ConfigBase::DictFieldProxy Contacts::blinded_contact_field( const blinded_contact_info& bc, std::span* get_pubkey) const { auto record = data["b"][bc.comm.base_url()]; @@ -403,8 +404,8 @@ ConfigBase::DictFieldProxy Contacts::blinded_contact_field( using any_blinded_contact = std::variant; std::optional Contacts::get_blinded( - std::string_view pubkey_hex, bool legacy_blinding) const { - check_session_id(pubkey_hex, legacy_blinding ? "15" : "25"); + std::string_view blinded_id_hex, bool legacy_blinding) const { + check_session_id(blinded_id_hex, legacy_blinding ? "15" : "25"); if (auto* b = data["b"].dict()) { auto comm = comm_iterator_helper{b->begin(), b->end()}; @@ -413,7 +414,7 @@ std::optional Contacts::get_blinded( while (!comm.done()) { if (comm.load(val)) if (auto* ptr = std::get_if(val.get()); - ptr && ptr->session_id() == pubkey_hex) + ptr && ptr->session_id() == blinded_id_hex) return *ptr; comm.advance(); } @@ -422,7 +423,22 @@ std::optional Contacts::get_blinded( return std::nullopt; } -std::vector Contacts::blinded_contacts() const { +blinded_contact_info Contacts::get_or_construct_blinded( + std::string_view community_base_url, + std::string_view community_pubkey_hex, + std::string_view blinded_id_hex, + bool legacy_blinding) { + if (auto maybe = get_blinded(blinded_id_hex, legacy_blinding)) + return *std::move(maybe); + + return blinded_contact_info{ + community_base_url, + to_span(oxenc::from_hex(community_pubkey_hex)), + blinded_id_hex, + legacy_blinding}; +} + +std::vector Contacts::blinded() const { std::vector ret; if (auto* b = data["b"].dict()) { @@ -440,7 +456,7 @@ std::vector Contacts::blinded_contacts() const { return ret; } -bool Contacts::set_blinded_contact(const blinded_contact_info& bc) { +void Contacts::set_blinded(const blinded_contact_info& bc) { data["b"][bc.comm.base_url()]["#"] = bc.comm.pubkey(); auto info = blinded_contact_field(bc); // data["b"][base]["R"][bc_session_id_without_prefix] @@ -459,12 +475,13 @@ bool Contacts::set_blinded_contact(const blinded_contact_info& bc) { set_positive_int(info["j"], to_epoch_seconds(bc.created)); } -bool Contacts::erase_blinded_contact( +bool Contacts::erase_blinded( std::string_view base_url_, std::string_view blinded_id, bool legacy_blinding) { - std::string pk = session_id_to_bytes(blinded_id, legacy_blinding ? "15" : "25").substr(2); + check_session_id(blinded_id, legacy_blinding ? "15" : "25"); auto base_url = community::canonical_url(base_url_); - auto info = data["d"][base_url]["R"][pk]; + auto pk = std::string(blinded_id.substr(2)); + auto info = data["b"][base_url]["R"][pk]; bool ret = info.exists(); info.erase(); return ret; @@ -570,16 +587,15 @@ LIBSESSION_C_API size_t contacts_size(const config_object* conf) { return unbox(conf)->size(); } -LIBSESSION_C_API bool contacts_get_blinded_contact( +LIBSESSION_C_API bool contacts_get_blinded( config_object* conf, - const char* blinded_session_id, + const char* blinded_id, bool legacy_blinding, contacts_blinded_contact* blinded_contact) { return wrap_exceptions( conf, [&] { - if (auto bc = unbox(conf)->get_blinded( - blinded_session_id, legacy_blinding)) { + if (auto bc = unbox(conf)->get_blinded(blinded_id, legacy_blinding)) { bc->into(*blinded_contact); return true; } @@ -588,10 +604,31 @@ LIBSESSION_C_API bool contacts_get_blinded_contact( false); } -LIBSESSION_C_API contacts_blinded_contact_list* contacts_blinded_contacts( - const config_object* conf) { +LIBSESSION_C_API bool contacts_get_or_construct_blinded( + config_object* conf, + const char* community_base_url, + const char* community_pubkey_hex, + const char* blinded_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) { + return wrap_exceptions( + conf, + [&] { + unbox(conf) + ->get_or_construct_blinded( + community_base_url, + community_pubkey_hex, + blinded_id, + legacy_blinding) + .into(*blinded_contact); + return true; + }, + false); +} + +LIBSESSION_C_API contacts_blinded_contact_list* contacts_blinded(const config_object* conf) { try { - auto cpp_contacts = unbox(conf)->blinded_contacts(); + auto cpp_contacts = unbox(conf)->blinded(); if (cpp_contacts.empty()) return nullptr; @@ -633,21 +670,25 @@ LIBSESSION_C_API contacts_blinded_contact_list* contacts_blinded_contacts( } } -LIBSESSION_C_API bool contacts_set_blinded_contact( +LIBSESSION_C_API bool contacts_set_blinded( config_object* conf, const contacts_blinded_contact* bc) { return wrap_exceptions( conf, [&] { - unbox(conf)->set_blinded_contact(blinded_contact_info{*bc}); + unbox(conf)->set_blinded(blinded_contact_info{*bc}); return true; }, false); } -LIBSESSION_C_API bool contacts_erase_blinded_contact( - config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding) { +LIBSESSION_C_API bool contacts_erase_blinded( + config_object* conf, + const char* community_base_url, + const char* blinded_id, + bool legacy_blinding) { try { - return unbox(conf)->erase_blinded_contact(base_url, blinded_id, legacy_blinding); + return unbox(conf)->erase_blinded( + community_base_url, blinded_id, legacy_blinding); } catch (...) { return false; } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 1b2286d2..2aab588a 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -886,3 +887,212 @@ TEST_CASE("needs_dump bug", "[config][needs_dump]") { contacts.set(c); CHECK(contacts.needs_dump()); } + +TEST_CASE("Contacts", "[config][blinded_contacts]") { + + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + std::array ed_pk, curve_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair( + ed_pk.data(), ed_sk.data(), reinterpret_cast(seed.data())); + int rc = crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data()); + REQUIRE(rc == 0); + + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(oxenc::to_hex(seed.begin(), seed.end()) == + oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); + + session::config::Contacts contacts{std::span{seed}, std::nullopt}; + + constexpr auto definitely_real_id = + "150000000000000000000000000000000000000000000000000000000000000000"sv; + constexpr auto comm_base_url = "https://example.com/"sv; + constexpr auto comm_pubkey_hex = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"sv; + + int64_t now = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + CHECK_FALSE(contacts.get_blinded(definitely_real_id, true)); + + CHECK(contacts.empty()); + CHECK(contacts.size() == 0); + + auto c = contacts.get_or_construct_blinded( + comm_base_url, comm_pubkey_hex, definitely_real_id, true); + + CHECK(c.session_id() == "150000000000000000000000000000000000000000000000000000000000000000"); + CHECK(c.name.empty()); + CHECK_FALSE(c.profile_picture); + CHECK(c.legacy_blinding); + CHECK(c.created == 0); + + CHECK_FALSE(contacts.needs_push()); + CHECK_FALSE(contacts.needs_dump()); + CHECK(std::get(contacts.push()) == 0); + + c.set_name("Joe"); + c.created = created_ts * 1'000; + contacts.set_blinded(c); + + REQUIRE(contacts.get_blinded(definitely_real_id, true).has_value()); + + CHECK(contacts.get_blinded(definitely_real_id, true)->name == "Joe"); + CHECK_FALSE(contacts.get_blinded(definitely_real_id, true)->profile_picture); + CHECK(contacts.get_blinded(definitely_real_id, true)->legacy_blinding); + CHECK(contacts.get_blinded(definitely_real_id, true)->session_id() == definitely_real_id); + + CHECK(contacts.needs_push()); + CHECK(contacts.needs_dump()); + + auto [seqno, to_push, obs] = contacts.push(); + + CHECK(seqno == 1); + + // Pretend we uploaded it + contacts.confirm_pushed(seqno, {"fakehash1"}); + CHECK(contacts.needs_dump()); + CHECK_FALSE(contacts.needs_push()); + + // NB: Not going to check encrypted data and decryption here because that's general (not + // specific to contacts) and is covered already in the user profile tests. + session::config::Contacts contacts2{seed, contacts.dump()}; + CHECK_FALSE(contacts2.needs_push()); + CHECK_FALSE(contacts2.needs_dump()); + CHECK(std::get(contacts2.push()) == 1); + CHECK_FALSE(contacts.needs_dump()); // Because we just called dump() above, to load up + // contacts2. + + auto x = contacts2.get_blinded(definitely_real_id, true); + REQUIRE(x); + CHECK(x->name == "Joe"); + CHECK_FALSE(x->profile_picture); + CHECK(x->created == created_ts); + CHECK(x->legacy_blinding == true); + + auto another_id = "251111111111111111111111111111111111111111111111111111111111111111"sv; + auto c2 = contacts2.get_or_construct_blinded(comm_base_url, comm_pubkey_hex, another_id, false); + // We're not setting any fields, but we should still keep a record of the session id + contacts2.set_blinded(c2); + + CHECK(contacts2.needs_push()); + + std::tie(seqno, to_push, obs) = contacts2.push(); + REQUIRE(to_push.size() == 1); + + CHECK(seqno == 2); + + std::vector>> merge_configs; + merge_configs.emplace_back("fakehash2", to_push[0]); + contacts.merge(merge_configs); + contacts2.confirm_pushed(seqno, {"fakehash2"}); + + CHECK_FALSE(contacts.needs_push()); + CHECK(std::get(contacts.push()) == seqno); + + // Iterate through and make sure we got everything we expected + auto blinded = contacts.blinded(); + std::vector session_ids; + std::vector names; + std::vector legacy_blindings; + CHECK(blinded.size() == 2); + for (const auto& cc : blinded) { + session_ids.push_back(cc.session_id()); + names.emplace_back(cc.name.empty() ? "(N/A)" : cc.name); + legacy_blindings.emplace_back(cc.legacy_blinding); + } + + REQUIRE(session_ids.size() == 2); + REQUIRE(session_ids.size() == blinded.size()); + CHECK(session_ids[0] == definitely_real_id); + CHECK(session_ids[1] == another_id); + CHECK(names[0] == "Joe"); + CHECK(names[1] == "(N/A)"); + CHECK(legacy_blindings[0]); + CHECK_FALSE(legacy_blindings[1]); + + // Conflict! Oh no! + + // On client 1 delete a contact: + CHECK(contacts.erase_blinded(comm_base_url, definitely_real_id, true)); + + // Client 2 adds a new friend: + auto third_id = "152222222222222222222222222222222222222222222222222222222222222222"sv; + auto c3 = contacts2.get_or_construct_blinded(comm_base_url, comm_pubkey_hex, third_id, true); + c3.set_name("Name 3"); + + session::config::profile_pic p; + { + // These don't stay alive, so we use set_key/set_url to make a local copy: + std::vector key = "qwerty78901234567890123456789012"_bytes; + std::string url = "http://example.com/huge.bmp"; + p.set_key(std::move(key)); + p.url = std::move(url); + } + c3.profile_picture = std::move(p); + contacts2.set_blinded(c3); + + CHECK(contacts.needs_push()); + CHECK(contacts2.needs_push()); + + std::tie(seqno, to_push, obs) = contacts.push(); + auto [seqno2, to_push2, obs2] = contacts2.push(); + REQUIRE(to_push.size() == 1); + REQUIRE(to_push2.size() == 1); + + CHECK(seqno == seqno2); + CHECK(to_push != to_push2); + CHECK(as_set(obs) == make_set("fakehash2"s)); + CHECK(as_set(obs2) == make_set("fakehash2"s)); + + contacts.confirm_pushed(seqno, {"fakehash3a"}); + contacts2.confirm_pushed(seqno2, {"fakehash3b"}); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash3b", to_push2[0]); + contacts.merge(merge_configs); + CHECK(contacts.needs_push()); + + merge_configs.clear(); + merge_configs.emplace_back("fakehash3a", to_push[0]); + contacts2.merge(merge_configs); + CHECK(contacts2.needs_push()); + + std::tie(seqno, to_push, obs) = contacts.push(); + CHECK(seqno == seqno2 + 1); + std::tie(seqno2, to_push2, obs2) = contacts2.push(); + CHECK(seqno == seqno2); + // Disabled check for now: doesn't work with protobuf (because of the non-deterministic + // encryption in the middle of the protobuf wrapping). + // TODO: reenable once protobuf isn't always-on. + // CHECK(printable(to_push) == printable(to_push2)); + CHECK(as_set(obs) == make_set("fakehash3a"s, "fakehash3b")); + CHECK(as_set(obs2) == make_set("fakehash3a"s, "fakehash3b")); + + contacts.confirm_pushed(seqno, {"fakehash4"}); + contacts2.confirm_pushed(seqno2, {"fakehash4"}); + + CHECK_FALSE(contacts.needs_push()); + CHECK_FALSE(contacts2.needs_push()); + + auto blinded2 = contacts.blinded(); + session_ids.clear(); + names.clear(); + legacy_blindings.clear(); + for (const auto& cc : blinded2) { + session_ids.push_back(cc.session_id()); + names.emplace_back(cc.name.empty() ? "(N/A)" : cc.name); + legacy_blindings.emplace_back(cc.legacy_blinding); + } + REQUIRE(session_ids.size() == 2); + CHECK(session_ids[0] == another_id); + CHECK(session_ids[1] == third_id); + CHECK(names[0] == "(N/A)"); + CHECK(names[1] == "Name 3"); + CHECK_FALSE(legacy_blindings[0]); + CHECK(legacy_blindings[1]); +} \ No newline at end of file diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index daf1ed3a..25608564 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -35,6 +35,11 @@ TEST_CASE("Conversations", "[config][conversations]") { constexpr auto benders_nightmare_group = "030111101001001000101010011011010010101010111010000110100001210000"sv; + constexpr auto legacy_blinded_id = + "150000000000000000000000000000000000101010111010000110100001210000"sv; + constexpr auto blinded_id = + "255000000000000000000000000000000000101010111010000110100001210000"sv; + CHECK_FALSE(convos.get_1to1(definitely_real_id)); CHECK(convos.empty()); @@ -90,6 +95,27 @@ TEST_CASE("Conversations", "[config][conversations]") { g.unread = true; convos.set(g); + CHECK_FALSE(convos.get_blinded_1to1(legacy_blinded_id, true)); + CHECK_FALSE(convos.get_blinded_1to1(blinded_id, false)); + + auto lb = convos.get_or_construct_blinded_1to1(legacy_blinded_id, true); + CHECK(lb.blinded_session_id == legacy_blinded_id); + CHECK(lb.last_read == 0); + CHECK_FALSE(lb.unread); + + lb.last_read = now_ms; + lb.unread = true; + convos.set(lb); + + auto b = convos.get_or_construct_blinded_1to1(blinded_id, false); + CHECK(b.blinded_session_id == blinded_id); + CHECK(b.last_read == 0); + CHECK_FALSE(b.unread); + + b.last_read = now_ms; + b.unread = true; + convos.set(b); + auto [seqno, to_push, obs] = convos.push(); CHECK(seqno == 1); @@ -127,6 +153,18 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(x3->last_read == now_ms); CHECK(x3->unread); + auto x4 = convos2.get_blinded_1to1(legacy_blinded_id, true); + REQUIRE(x4); + CHECK(x4->blinded_session_id == "150000000000000000000000000000000000101010111010000110100001210000"); + CHECK(x4->last_read == now_ms); + CHECK(x4->unread); + + auto x5 = convos2.get_blinded_1to1(blinded_id, false); + REQUIRE(x5); + CHECK(x5->blinded_session_id == "255000000000000000000000000000000000101010111010000110100001210000"); + CHECK(x5->last_read == now_ms); + CHECK(x5->unread); + auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"sv; auto c2 = convos.get_or_construct_1to1(another_id); c2.unread = true; @@ -137,6 +175,11 @@ TEST_CASE("Conversations", "[config][conversations]") { c3.last_read = now_ms - 50; convos2.set(c3); + auto c4 = convos2.get_or_construct_blinded_1to1( + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false); + c4.unread = true; + convos2.set(c4); + CHECK(convos2.needs_push()); std::tie(seqno, to_push, obs) = convos2.push(); @@ -152,6 +195,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK_FALSE(convos.needs_push()); CHECK(std::get(convos.push()) == seqno); + using session::config::convo::blinded_one_to_one; using session::config::convo::community; using session::config::convo::group; using session::config::convo::legacy_group; @@ -163,17 +207,21 @@ TEST_CASE("Conversations", "[config][conversations]") { "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", "gr: 030111101001001000101010011011010010101010111010000110100001210000", "comm: http://example.org:5678/r/sudokuroom", - "lgr: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}) + "lgr: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "lb: 150000000000000000000000000000000000101010111010000110100001210000", + "b: 2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "b: 255000000000000000000000000000000000101010111010000110100001210000"}) expected.emplace_back(e); for (auto* conv : {&convos, &convos2}) { // Iterate through and make sure we got everything we expected seen.clear(); - CHECK(conv->size() == 5); + CHECK(conv->size() == 8); CHECK(conv->size_1to1() == 2); CHECK(conv->size_communities() == 1); CHECK(conv->size_legacy_groups() == 1); CHECK(conv->size_groups() == 1); + CHECK(conv->size_blinded_1to1() == 3); CHECK_FALSE(conv->empty()); for (const auto& convo : *conv) { if (auto* c = std::get_if(&convo)) @@ -185,6 +233,10 @@ TEST_CASE("Conversations", "[config][conversations]") { "comm: " + std::string{c->base_url()} + "/r/" + std::string{c->room()}); else if (auto* c = std::get_if(&convo)) seen.push_back("lgr: " + c->id); + else if (auto* c = std::get_if(&convo); c->legacy_blinding) + seen.push_back("lb: " + c->blinded_session_id); + else if (auto* c = std::get_if(&convo); !c->legacy_blinding) + seen.push_back("b: " + c->blinded_session_id); else seen.push_back("unknown convo type!"); } @@ -196,10 +248,13 @@ TEST_CASE("Conversations", "[config][conversations]") { convos.erase_1to1("052000000000000000000000000000000000000000000000000000000000000000"); CHECK_FALSE(convos.needs_push()); convos.erase_1to1("055000000000000000000000000000000000000000000000000000000000000000"); + convos.erase_blinded_1to1( + "255000000000000000000000000000000000101010111010000110100001210000", false); CHECK(convos.needs_push()); - CHECK(convos.size() == 4); + CHECK(convos.size() == 6); CHECK(convos.size_1to1() == 1); CHECK(convos.size_groups() == 1); + CHECK(convos.size_blinded_1to1() == 2); // Check the single-type iterators: seen.clear(); @@ -222,6 +277,14 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(seen == std::vector{{ "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", }}); + + seen.clear(); + for (auto it = convos.begin_blinded_1to1(); it != convos.end(); ++it) + seen.emplace_back(it->blinded_session_id); + CHECK(seen == std::vector{{ + "150000000000000000000000000000000000101010111010000110100001210000", + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }}); } TEST_CASE("Conversations (C API)", "[config][conversations][c]") { @@ -314,6 +377,17 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { // The new data doesn't get stored until we call this: convo_info_volatile_set_community(conf, &og); + const char* const blinded_id = + "150000000000000000000000000000000000101010111010000110100001210000"; + convo_info_volatile_blinded_1to1 b1; + REQUIRE_FALSE(convo_info_volatile_get_blinded_1to1(conf, &b1, blinded_id, true)); + REQUIRE(convo_info_volatile_get_or_construct_blinded_1to1(conf, &b1, blinded_id, true)); + b1.last_read = now_ms; + convo_info_volatile_set_blinded_1to1(conf, &b1); + + CHECK(config_needs_push(conf)); + CHECK(config_needs_dump(conf)); + config_push_data* to_push = config_push(conf); auto seqno = to_push->seqno; CHECK(seqno == 1); @@ -360,6 +434,16 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { convo_info_volatile_set_legacy_group(conf2, &cg); CHECK(config_needs_push(conf2)); + convo_info_volatile_blinded_1to1 b2; + REQUIRE(convo_info_volatile_get_or_construct_blinded_1to1( + conf2, + &b2, + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + false)); + b2.unread = true; + convo_info_volatile_set_blinded_1to1(conf2, &b2); + CHECK(config_needs_push(conf2)); + to_push = config_push(conf2); CHECK(to_push->seqno == 2); REQUIRE(to_push->n_configs == 1); @@ -383,14 +467,16 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { for (auto* conf : {conf, conf2}) { // Iterate through and make sure we got everything we expected seen.clear(); - CHECK(convo_info_volatile_size(conf) == 4); + CHECK(convo_info_volatile_size(conf) == 6); CHECK(convo_info_volatile_size_1to1(conf) == 2); CHECK(convo_info_volatile_size_communities(conf) == 1); CHECK(convo_info_volatile_size_legacy_groups(conf) == 1); + CHECK(convo_info_volatile_size_blinded_1to1(conf) == 2); convo_info_volatile_1to1 c1; convo_info_volatile_community c2; convo_info_volatile_legacy_group c3; + convo_info_volatile_blinded_1to1 c4; convo_info_volatile_iterator* it = convo_info_volatile_iterator_new(conf); for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { if (convo_info_volatile_it_is_1to1(it, &c1)) { @@ -399,6 +485,8 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.push_back("comm: "s + c2.base_url + "/r/" + c2.room); } else if (convo_info_volatile_it_is_legacy_group(it, &c3)) { seen.push_back("lgr: "s + c3.group_id); + } else if (convo_info_volatile_it_is_blinded_1to1(it, &c4)) { + seen.push_back("b: "s + c4.blinded_session_id); } } convo_info_volatile_iterator_free(it); @@ -410,7 +498,11 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "055000000000000000000000000000000000000000000000000000000000000000", "comm: http://example.org:5678/r/sudokuroom", "lgr: " - "05ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "b: " + "150000000000000000000000000000000000101010111010000110100001210000", + "b: " + "2512345cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" "c"}}); } @@ -420,9 +512,12 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK_FALSE(config_needs_push(conf)); convo_info_volatile_erase_1to1( conf, "055000000000000000000000000000000000000000000000000000000000000000"); + convo_info_volatile_erase_blinded_1to1( + conf, "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false); CHECK(config_needs_push(conf)); - CHECK(convo_info_volatile_size(conf) == 3); + CHECK(convo_info_volatile_size(conf) == 4); CHECK(convo_info_volatile_size_1to1(conf) == 1); + CHECK(convo_info_volatile_size_blinded_1to1(conf) == 1); // Check the single-type iterators: seen.clear(); @@ -464,6 +559,19 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(seen == std::vector{{ "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", }}); + + seen.clear(); + convo_info_volatile_blinded_1to1 bi; + for (it = convo_info_volatile_iterator_new_blinded_1to1(conf); + !convo_info_volatile_iterator_done(it); + convo_info_volatile_iterator_advance(it)) { + REQUIRE(convo_info_volatile_it_is_blinded_1to1(it, &bi)); + seen.emplace_back(bi.blinded_session_id); + } + convo_info_volatile_iterator_free(it); + CHECK(seen == std::vector{{ + "150000000000000000000000000000000000101010111010000110100001210000", + }}); } TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { From 5c47b528b2a9772cd4109226019967cf9ed23b76 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 16 Jul 2025 10:26:57 +1000 Subject: [PATCH 12/14] Fixed a test failure caused by odd syntax --- tests/test_config_contacts.cpp | 1 - tests/test_config_convo_info_volatile.cpp | 58 ++++++++++++----------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 2aab588a..a29127ed 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 25608564..502ea132 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -155,13 +155,15 @@ TEST_CASE("Conversations", "[config][conversations]") { auto x4 = convos2.get_blinded_1to1(legacy_blinded_id, true); REQUIRE(x4); - CHECK(x4->blinded_session_id == "150000000000000000000000000000000000101010111010000110100001210000"); + CHECK(x4->blinded_session_id == + "150000000000000000000000000000000000101010111010000110100001210000"); CHECK(x4->last_read == now_ms); CHECK(x4->unread); auto x5 = convos2.get_blinded_1to1(blinded_id, false); REQUIRE(x5); - CHECK(x5->blinded_session_id == "255000000000000000000000000000000000101010111010000110100001210000"); + CHECK(x5->blinded_session_id == + "255000000000000000000000000000000000101010111010000110100001210000"); CHECK(x5->last_read == now_ms); CHECK(x5->unread); @@ -260,31 +262,31 @@ TEST_CASE("Conversations", "[config][conversations]") { seen.clear(); for (auto it = convos.begin_1to1(); it != convos.end(); ++it) seen.push_back(it->session_id); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "051111111111111111111111111111111111111111111111111111111111111111", - }}); + }); seen.clear(); for (auto it = convos.begin_communities(); it != convos.end(); ++it) seen.emplace_back(it->base_url()); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "http://example.org:5678", - }}); + }); seen.clear(); for (auto it = convos.begin_legacy_groups(); it != convos.end(); ++it) seen.emplace_back(it->id); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }}); + }); seen.clear(); for (auto it = convos.begin_blinded_1to1(); it != convos.end(); ++it) seen.emplace_back(it->blinded_session_id); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "150000000000000000000000000000000000101010111010000110100001210000", "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }}); + }); } TEST_CASE("Conversations (C API)", "[config][conversations][c]") { @@ -492,18 +494,18 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { convo_info_volatile_iterator_free(it); CHECK(seen == std::vector{ - {"1-to-1: " - "051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: " - "055000000000000000000000000000000000000000000000000000000000000000", - "comm: http://example.org:5678/r/sudokuroom", - "lgr: " - "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "b: " - "150000000000000000000000000000000000101010111010000110100001210000", - "b: " - "2512345cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - "c"}}); + "1-to-1: " + "051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: " + "055000000000000000000000000000000000000000000000000000000000000000", + "comm: http://example.org:5678/r/sudokuroom", + "lgr: " + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "b: " + "150000000000000000000000000000000000101010111010000110100001210000", + "b: " + "2512345cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + "c"}); } CHECK_FALSE(config_needs_push(conf)); @@ -543,9 +545,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.emplace_back(ogi.base_url); } convo_info_volatile_iterator_free(it); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "http://example.org:5678", - }}); + }); seen.clear(); convo_info_volatile_legacy_group cgi; @@ -556,9 +558,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.emplace_back(cgi.group_id); } convo_info_volatile_iterator_free(it); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }}); + }); seen.clear(); convo_info_volatile_blinded_1to1 bi; @@ -569,9 +571,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.emplace_back(bi.blinded_session_id); } convo_info_volatile_iterator_free(it); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "150000000000000000000000000000000000101010111010000110100001210000", - }}); + }); } TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { From 0bcc98632c9103bd120675338d81b1e45fbdbfa8 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Jul 2025 17:10:34 -0300 Subject: [PATCH 13/14] blinded_contact_info: type-safe timestamp --- include/session/config/contacts.hpp | 2 +- src/config/contacts.cpp | 14 +++++++------- tests/test_config_contacts.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 576a0094..8f38ef06 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -125,7 +125,7 @@ struct blinded_contact_info { std::string name; profile_pic profile_picture; bool legacy_blinding; - int64_t created = 0; // Unix timestamp (seconds) when this contact was added + std::chrono::sys_seconds created{}; // Unix timestamp (seconds) when this contact was added blinded_contact_info() = default; explicit blinded_contact_info( diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 78af882b..5c1be6b4 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -317,7 +317,7 @@ blinded_contact_info::blinded_contact_info( } void blinded_contact_info::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"); @@ -327,8 +327,8 @@ void blinded_contact_info::load(const dict& info_dict) { } else { profile_picture.clear(); } - legacy_blinding = maybe_int(info_dict, "y").value_or(0); - created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); + legacy_blinding = int_or_0(info_dict, "y"); + created = ts_or_epoch(info_dict, "j"); } void blinded_contact_info::into(contacts_blinded_contact& c) const { @@ -346,7 +346,7 @@ void blinded_contact_info::into(contacts_blinded_contact& c) const { copy_c_str(c.profile_pic.url, ""); } c.legacy_blinding = legacy_blinding; - c.created = to_epoch_seconds(created); + c.created = created.time_since_epoch().count(); } blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { @@ -359,7 +359,7 @@ blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) { profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); } legacy_blinding = c.legacy_blinding; - created = to_epoch_seconds(c.created); + created = to_sys_seconds(c.created); } const std::string blinded_contact_info::session_id() const { @@ -472,7 +472,7 @@ void Contacts::set_blinded(const blinded_contact_info& bc) { bc.profile_picture.key); set_positive_int(info["y"], bc.legacy_blinding); - set_positive_int(info["j"], to_epoch_seconds(bc.created)); + set_ts(info["j"], bc.created); } bool Contacts::erase_blinded( @@ -717,4 +717,4 @@ LIBSESSION_C_API void contacts_iterator_advance(contacts_iterator* it) { ++*static_cast(it->_internals); } -} // extern "C" \ No newline at end of file +} // extern "C" diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index a29127ed..e30ac306 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -928,14 +928,14 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { CHECK(c.name.empty()); CHECK_FALSE(c.profile_picture); CHECK(c.legacy_blinding); - CHECK(c.created == 0); + CHECK(c.created.time_since_epoch() == 0s); CHECK_FALSE(contacts.needs_push()); CHECK_FALSE(contacts.needs_dump()); CHECK(std::get(contacts.push()) == 0); c.set_name("Joe"); - c.created = created_ts * 1'000; + c.created = session::to_sys_seconds(created_ts * 1'000); contacts.set_blinded(c); REQUIRE(contacts.get_blinded(definitely_real_id, true).has_value()); @@ -970,7 +970,7 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK_FALSE(x->profile_picture); - CHECK(x->created == created_ts); + CHECK(x->created.time_since_epoch() == created_ts * 1s); CHECK(x->legacy_blinding == true); auto another_id = "251111111111111111111111111111111111111111111111111111111111111111"sv; @@ -1094,4 +1094,4 @@ TEST_CASE("Contacts", "[config][blinded_contacts]") { CHECK(names[1] == "Name 3"); CHECK_FALSE(legacy_blindings[0]); CHECK(legacy_blindings[1]); -} \ No newline at end of file +} From 547bcab162f370e596ba6b9eca157dbe9eb15ed7 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 28 Jul 2025 16:35:12 -0300 Subject: [PATCH 14/14] Type-safe timestamps values Taking raw integers has led to clients passing invalid values in past releases (milliseconds or microseconds). This is, however, quite damaging as it means there are *three* possible representations of the same config, depending on which of s/ms/us gets stored, and while we have patched around those issues, there is still a type safety concern as it could happen again. This fixes it so that we *always* store timestamps as std::chrono::sys_seconds making it completely unambiguous what it holds, and ensuring that the proper values gets passed in (either through direct construction, or by calling session::to_sys_seconds to explicitly guess based on the magnitude). This should keep any milliseconds or microseconds usage strictly within the API and make it very difficult for such an erroneous value to end up inside the actual serialized config. This also updates volatile last_read in a similar way: but since that one is always unix epoch milliseconds, using a std::chrono::sys_time (typedefed to session::sys_milliseconds) for the type safety. Unlike the above, this one has never had ambiguous values and so doesn't need the same heuristics. Making it typesafe makes it stay that way. --- CMakeLists.txt | 2 +- include/session/config/contacts.h | 2 +- include/session/config/contacts.hpp | 18 +++- .../session/config/convo_info_volatile.hpp | 2 +- include/session/config/groups/info.hpp | 41 ++++---- include/session/config/user_groups.h | 1 - include/session/config/user_groups.hpp | 10 +- include/session/util.hpp | 14 +-- src/config/contacts.cpp | 25 +++-- src/config/convo_info_volatile.cpp | 48 ++++------ src/config/groups/info.cpp | 59 ++++++++---- src/config/user_groups.cpp | 24 ++--- tests/test_config_contacts.cpp | 12 +-- tests/test_config_convo_info_volatile.cpp | 50 +++++----- tests/test_config_user_groups.cpp | 96 ++++++++++--------- tests/test_group_info.cpp | 28 +++--- 16 files changed, 243 insertions(+), 189 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c71fee55..3b4f2e7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.5.1 + VERSION 1.6.0 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 08c47fbc..e71b1bcf 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -28,7 +28,7 @@ typedef struct contacts_contact { int priority; CONVO_NOTIFY_MODE notifications; - int64_t mute_until; + int64_t mute_until; // unix timestamp (seconds) CONVO_EXPIRATION_MODE exp_mode; int exp_seconds; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 8f38ef06..1d80e014 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -82,12 +82,13 @@ struct contact_info { // conversation is hidden. Otherwise (0) this is a regular, unpinned // conversation. notify_mode notifications = notify_mode::defaulted; - int64_t mute_until = 0; // If non-zero, disable notifications until the given unix timestamp - // (seconds, overriding whatever the current `notifications` value is - // until the timestamp expires). + std::chrono::sys_seconds mute_until{0s}; // If timestamp is non-zero, disable notifications + // until the given unix timestamp (seconds, overriding + // whatever the current `notifications` value is until + // the timestamp expires). expiration_mode exp_mode = expiration_mode::none; // The expiry time; none if not expiring. std::chrono::seconds exp_timer{0}; // The expiration timer (in seconds) - int64_t created = 0; // Unix timestamp (seconds) when this contact was added + std::chrono::sys_seconds created{0s}; // Unix timestamp (seconds) when this contact was added explicit contact_info(std::string sid); @@ -384,7 +385,14 @@ class Contacts : public ConfigBase { /// Inputs: /// - `session_id` -- hex string of the session id /// - `timestamp` -- standard unix timestamp of the time contact was created - void set_created(std::string_view session_id, int64_t timestamp); + void set_created(std::string_view session_id, std::chrono::sys_seconds timestamp); + + /// Deprecated: takes timestamp as an integer and guess whether it is seconds, milliseconds, or + /// microseconds. + [[deprecated( + "pass a std::chrono::sys_seconds instead (perhaps using " + "session::to_sys_seconds)")]] void + set_created(std::string_view session_id, int64_t timestamp); /// API: contacts/contacts::erase /// diff --git a/include/session/config/convo_info_volatile.hpp b/include/session/config/convo_info_volatile.hpp index ce954ddb..a713b8e2 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -67,7 +67,7 @@ class val_loader; namespace convo { struct base { - int64_t last_read = 0; + sys_milliseconds last_read{}; bool unread = false; protected: diff --git a/include/session/config/groups/info.hpp b/include/session/config/groups/info.hpp index 64c2e48f..8d405560 100644 --- a/include/session/config/groups/info.hpp +++ b/include/session/config/groups/info.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include "../base.hpp" @@ -220,7 +219,11 @@ class Info : public ConfigBase { /// Inputs: /// - `session_id` -- hex string of the session id /// - `timestamp` -- standard unix timestamp when the group was created - void set_created(int64_t timestamp); + void set_created(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether the input is seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_created(int64_t timestamp); /// API: groups/Info::get_created /// @@ -229,9 +232,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `std::optional` -- the unix timestamp when the group was created, or nullopt if - /// the creation timestamp is not set. - std::optional get_created() const; + /// - `std::chrono::sys_seconds` -- the unix timestamp when the group was + /// created, or nullopt if the creation timestamp is not set. + std::optional get_created() const; /// API: groups/Info::set_delete_before /// @@ -239,13 +242,14 @@ class Info : public ConfigBase { /// the closed group history with a timestamp earlier than this value. Returns nullopt if no /// delete-before timestamp is set. /// - /// The given value is checked for sanity (e.g. if you pass milliseconds it will be - /// interpreted as such) - /// /// Inputs: /// - `timestamp` -- the new unix timestamp before which clients should delete messages. Pass 0 /// (or negative) to disable the delete-before timestamp. - void set_delete_before(int64_t timestamp); + void set_delete_before(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether you meant seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_delete_before(int64_t timestamp); /// API: groups/Info::get_delete_before /// @@ -257,8 +261,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `int64_t` -- the unix timestamp for which all older messages shall be delete - std::optional get_delete_before() const; + /// - `sys_seconds` -- the unix timestamp for which all older messages shall be deleted, or + /// nullopt if there is no delete-before timestamp set. + std::optional get_delete_before() const; /// API: groups/Info::set_delete_attach_before /// @@ -272,9 +277,12 @@ class Info : public ConfigBase { /// /// Inputs: /// - `timestamp` -- the new unix timestamp before which clients should delete attachments. Pass - /// 0 - /// (or negative) to disable the delete-attachment-before timestamp. - void set_delete_attach_before(int64_t timestamp); + /// 0 (or negative) to disable the delete-attachment-before timestamp. + void set_delete_attach_before(std::chrono::sys_seconds timestamp); + /// Deprecated version that attempts to guess whether you meant seconds, milliseconds, or + /// microseconds. + [[deprecated("pass a std::chrono::sys_seconds instead (or use session::to_sys_seconds)")]] void + set_delete_attach_before(int64_t timestamp); /// API: groups/Info::get_delete_attach_before /// @@ -286,8 +294,9 @@ class Info : public ConfigBase { /// Inputs: none. /// /// Outputs: - /// - `int64_t` -- the unix timestamp for which all older message attachments shall be deleted - std::optional get_delete_attach_before() const; + /// - `sys_seconds` -- the unix timestamp for which all older message attachments shall be + /// deleted, or nullopt if delete-attach-before is not enabled. + std::optional get_delete_attach_before() const; /// API: groups/Info::destroy_group /// diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index ef279c28..48e470a6 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -6,7 +6,6 @@ extern "C" { #include "base.h" #include "notify.h" -#include "util.h" // Maximum length of a group name, in bytes LIBSESSION_EXPORT extern const size_t GROUP_NAME_MAX_LENGTH; diff --git a/include/session/config/user_groups.hpp b/include/session/config/user_groups.hpp index 55febbc9..057c6994 100644 --- a/include/session/config/user_groups.hpp +++ b/include/session/config/user_groups.hpp @@ -82,11 +82,13 @@ namespace session::config { struct base_group_info { static constexpr size_t NAME_MAX_LENGTH = 100; // in bytes; name will be truncated if exceeded - int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means - // pinned higher (i.e. higher priority conversations come first). - int64_t joined_at = 0; // unix timestamp (seconds) when the group was joined (or re-joined) + int priority = 0; // The priority; 0 means unpinned, -1 means hidden, positive means + // pinned higher (i.e. higher priority conversations come first). + std::chrono::sys_seconds joined_at{}; // unix timestamp (seconds) when the group + // was joined (or re-joined) notify_mode notifications = notify_mode::defaulted; // When the user wants notifications - int64_t mute_until = 0; // unix timestamp (seconds) until which notifications are disabled + std::chrono::sys_seconds mute_until{}; // unix timestamp (seconds) until which + // notifications are disabled std::string name; // human-readable; always set for a legacy closed group, only used before // joining a new closed group (after joining the group info provide the name) diff --git a/include/session/util.hpp b/include/session/util.hpp index fc57908f..9cd6d452 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -249,13 +249,7 @@ inline std::string utf8_truncate(std::string val, size_t n) { return val; } -// Helper function to transform a timestamp provided in seconds, milliseconds or microseconds to -// seconds -inline int64_t to_epoch_seconds(int64_t timestamp) { - return timestamp > 9'000'000'000'000 ? timestamp / 1'000'000 - : timestamp > 9'000'000'000 ? timestamp / 1'000 - : timestamp; -} +using sys_milliseconds = std::chrono::sys_time; // Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it. inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) { @@ -276,4 +270,10 @@ static_assert(std::is_same_v< std::chrono::seconds, decltype(std::declval().time_since_epoch())>); +// Takes a timestamp as unix epoch milliseconds (not seconds, or microseconds) and wraps it in a +// sys_ms containing it. +inline sys_milliseconds as_sys_ms(int64_t timestamp) { + return sys_milliseconds{std::chrono::milliseconds{timestamp}}; +} + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 5c1be6b4..2f52f972 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -92,7 +93,9 @@ void contact_info::load(const dict& info_dict) { } else { notifications = notify_mode::defaulted; } - mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); + // Older client versions might have accidentally stored this as ms, so run it through + // to_sys_seconds: + mute_until = to_sys_seconds(int_or_0(info_dict, "!")); int exp_mode_ = int_or_0(info_dict, "e"); if (exp_mode_ >= static_cast(expiration_mode::none) && @@ -113,7 +116,9 @@ void contact_info::load(const dict& info_dict) { } } - created = to_epoch_seconds(int_or_0(info_dict, "j")); + // Older client versions might have accidentally stored this as ms, so run it through + // to_sys_seconds: + created = to_sys_seconds(int_or_0(info_dict, "j")); } void contact_info::into(contacts_contact& c) const { @@ -132,12 +137,12 @@ void contact_info::into(contacts_contact& c) const { c.blocked = blocked; c.priority = priority; c.notifications = static_cast(notifications); - c.mute_until = to_epoch_seconds(mute_until); + c.mute_until = mute_until.time_since_epoch().count(); c.exp_mode = static_cast(exp_mode); c.exp_seconds = exp_timer.count(); if (c.exp_seconds <= 0 && c.exp_mode != CONVO_EXPIRATION_NONE) c.exp_mode = CONVO_EXPIRATION_NONE; - c.created = to_epoch_seconds(created); + c.created = created.time_since_epoch().count(); } contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, 66} { @@ -156,12 +161,12 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, blocked = c.blocked; priority = c.priority; notifications = static_cast(c.notifications); - mute_until = to_epoch_seconds(c.mute_until); + mute_until = to_sys_seconds(c.mute_until); exp_mode = static_cast(c.exp_mode); exp_timer = exp_mode == expiration_mode::none ? 0s : std::chrono::seconds{c.exp_seconds}; if (exp_timer <= 0s && exp_mode != expiration_mode::none) exp_mode = expiration_mode::none; - created = to_epoch_seconds(c.created); + created = to_sys_seconds(c.created); } std::optional Contacts::get(std::string_view pubkey_hex) const { @@ -211,7 +216,7 @@ void Contacts::set(const contact_info& contact) { if (notify == notify_mode::mentions_only) notify = notify_mode::all; set_positive_int(info["@"], static_cast(notify)); - set_positive_int(info["!"], to_epoch_seconds(contact.mute_until)); + set_ts(info["!"], contact.mute_until); set_pair_if( contact.exp_mode != expiration_mode::none && contact.exp_timer > 0s, @@ -220,7 +225,7 @@ void Contacts::set(const contact_info& contact) { info["E"], contact.exp_timer.count()); - set_positive_int(info["j"], to_epoch_seconds(contact.created)); + set_ts(info["j"], contact.created); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -285,9 +290,9 @@ void Contacts::set_expiry( set(c); } -void Contacts::set_created(std::string_view session_id, int64_t timestamp) { +void Contacts::set_created(std::string_view session_id, std::chrono::sys_seconds timestamp) { auto c = get_or_construct(session_id); - c.created = to_epoch_seconds(timestamp); + c.created = timestamp; set(c); } diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 54f73b62..cfd6e018 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -5,14 +5,10 @@ #include #include -#include -#include -#include #include #include "internal.hpp" #include "session/config/convo_info_volatile.h" -#include "session/config/error.h" #include "session/export.h" #include "session/types.hpp" #include "session/util.hpp" @@ -30,17 +26,17 @@ namespace convo { check_session_id(session_id); } one_to_one::one_to_one(const convo_info_volatile_1to1& c) : - base{c.last_read, c.unread}, session_id{c.session_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, session_id{c.session_id, 66} {} void one_to_one::into(convo_info_volatile_1to1& c) const { std::memcpy(c.session_id, session_id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } community::community(const convo_info_volatile_community& c) : config::community{c.base_url, c.room, std::span{c.pubkey, 32}}, - base{c.last_read, c.unread} {} + base{as_sys_ms(c.last_read), c.unread} {} void community::into(convo_info_volatile_community& c) const { static_assert(sizeof(c.base_url) == BASE_URL_MAX_LENGTH + 1); @@ -48,7 +44,7 @@ namespace convo { copy_c_str(c.base_url, base_url()); copy_c_str(c.room, room_norm()); std::memcpy(c.pubkey, pubkey().data(), 32); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -59,11 +55,11 @@ namespace convo { check_session_id(id, "03"); } group::group(const convo_info_volatile_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, id{c.group_id, 66} {} void group::into(convo_info_volatile_group& c) const { std::memcpy(c.group_id, id.c_str(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -74,11 +70,11 @@ namespace convo { check_session_id(id); } legacy_group::legacy_group(const convo_info_volatile_legacy_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{as_sys_ms(c.last_read), c.unread}, id{c.group_id, 66} {} void legacy_group::into(convo_info_volatile_legacy_group& c) const { std::memcpy(c.group_id, id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; } @@ -91,19 +87,19 @@ namespace convo { check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); } blinded_one_to_one::blinded_one_to_one(const convo_info_volatile_blinded_1to1& c) : - base{c.last_read, c.unread}, + base{as_sys_ms(c.last_read), c.unread}, blinded_session_id{c.blinded_session_id, 66}, legacy_blinding{c.legacy_blinding} {} void blinded_one_to_one::into(convo_info_volatile_blinded_1to1& c) const { std::memcpy(c.blinded_session_id, blinded_session_id.data(), 67); - c.last_read = last_read; + c.last_read = last_read.time_since_epoch().count(); c.unread = unread; c.legacy_blinding = legacy_blinding; } void base::load(const dict& info_dict) { - last_read = int_or_0(info_dict, "r"); + last_read = as_sys_ms(int_or_0(info_dict, "r")); unread = (bool)int_or_0(info_dict, "u"); } @@ -263,23 +259,21 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) { void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { auto r = info["r"]; - // If we're making the last_read value *older* for some reason then ignore the prune cutoff - // (because we might be intentionally resetting the value after a deletion, for instance). - if (auto* val = r.integer(); val && c.last_read < *val) - r = c.last_read; - else { - std::chrono::system_clock::time_point last_read{std::chrono::milliseconds{c.last_read}}; - if (last_read > std::chrono::system_clock::now() - PRUNE_LOW) - info["r"] = c.last_read; - } + if (auto* val = r.integer(); + // If we're making the last_read value *older* for some reason then ignore the prune cutoff + // (because we might be intentionally resetting the value after a deletion, for instance). + (val && c.last_read < sys_milliseconds{std::chrono::milliseconds{*val}}) // + || + // Otherwise set it if it's more recent than the prune cutoff + c.last_read > std::chrono::system_clock::now() - PRUNE_LOW) + + r = c.last_read.time_since_epoch().count(); set_flag(info["u"], c.unread); } void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { - const int64_t cutoff = std::chrono::duration_cast( - (std::chrono::system_clock::now() - prune).time_since_epoch()) - .count(); + const auto cutoff = std::chrono::system_clock::now() - prune; std::vector stale; for (auto it = begin_1to1(); it != end(); ++it) diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 2025ca0a..92cdc41c 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -3,13 +3,12 @@ #include #include -#include +#include #include "../internal.hpp" #include "session/config/error.h" #include "session/config/groups/info.h" #include "session/export.h" -#include "session/types.hpp" #include "session/util.hpp" using namespace std::literals; @@ -84,33 +83,47 @@ void Info::set_expiry_timer(std::chrono::seconds expiration_timer) { set_positive_int(data["E"], expiration_timer.count()); } +void Info::set_created(std::chrono::sys_seconds timestamp) { + set_ts(data["c"], timestamp); +} void Info::set_created(int64_t timestamp) { - set_positive_int(data["c"], to_epoch_seconds(timestamp)); + set_created(to_sys_seconds(timestamp)); } -std::optional Info::get_created() const { +std::optional Info::get_created() const { if (auto* ts = data["c"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } +void Info::set_delete_before(std::chrono::sys_seconds timestamp) { + set_ts(data["d"], timestamp); +} + void Info::set_delete_before(int64_t timestamp) { - set_positive_int(data["d"], to_epoch_seconds(timestamp)); + set_delete_before(to_sys_seconds(timestamp)); } -std::optional Info::get_delete_before() const { +std::optional Info::get_delete_before() const { if (auto* ts = data["d"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } +void Info::set_delete_attach_before(std::chrono::sys_seconds timestamp) { + set_ts(data["D"], timestamp); +} + void Info::set_delete_attach_before(int64_t timestamp) { - set_positive_int(data["D"], to_epoch_seconds(timestamp)); + set_delete_attach_before(to_sys_seconds(timestamp)); } -std::optional Info::get_delete_attach_before() const { +std::optional Info::get_delete_attach_before() const { if (auto* ts = data["D"].integer()) - return to_epoch_seconds(*ts); + // Pass through to_sys_seconds because some past client bug may have stored ms here: + return to_sys_seconds(*ts); return std::nullopt; } @@ -306,7 +319,11 @@ LIBSESSION_C_API void groups_info_set_expiry_timer(config_object* conf, int expi /// Outputs: /// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { - return unbox(conf)->get_created().value_or(0); + return unbox(conf) + ->get_created() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_created @@ -318,7 +335,7 @@ LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { - unbox(conf)->set_created(std::max(0, ts)); + unbox(conf)->set_created(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_get_delete_before @@ -332,7 +349,11 @@ LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { /// Outputs: /// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_before().value_or(0); + return unbox(conf) + ->get_delete_before() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_delete_before @@ -344,7 +365,7 @@ LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_before(std::max(0, ts)); + unbox(conf)->set_delete_before(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_get_attach_delete_before @@ -358,7 +379,11 @@ LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t /// Outputs: /// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_attach_before().value_or(0); + return unbox(conf) + ->get_delete_attach_before() + .value_or(std::chrono::sys_seconds{}) + .time_since_epoch() + .count(); } /// API: groups_info/groups_info_set_attach_delete_before @@ -370,7 +395,7 @@ LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_objec /// - `conf` -- [in] Pointer to the config object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. LIBSESSION_C_API void groups_info_set_attach_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_attach_before(std::max(0, ts)); + unbox(conf)->set_delete_attach_before(to_sys_seconds(std::max(0, ts))); } /// API: groups_info/groups_info_is_destroyed(const config_object* conf); diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 7c8bf724..3fa7f357 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -6,16 +6,12 @@ #include #include -#include #include -#include #include #include "internal.hpp" -#include "session/config/error.h" #include "session/config/user_groups.h" #include "session/export.h" -#include "session/types.hpp" #include "session/util.hpp" using namespace std::literals; @@ -34,18 +30,18 @@ namespace session::config { template static void base_into(const base_group_info& self, T& c) { c.priority = self.priority; - c.joined_at = to_epoch_seconds(self.joined_at); + c.joined_at = self.joined_at.time_since_epoch().count(); c.notifications = static_cast(self.notifications); - c.mute_until = to_epoch_seconds(self.mute_until); + c.mute_until = self.mute_until.time_since_epoch().count(); c.invited = self.invited; } template static void base_from(base_group_info& self, const T& c) { self.priority = c.priority; - self.joined_at = to_epoch_seconds(c.joined_at); + self.joined_at = to_sys_seconds(c.joined_at); self.notifications = static_cast(c.notifications); - self.mute_until = to_epoch_seconds(c.mute_until); + self.mute_until = to_sys_seconds(c.mute_until); self.invited = c.invited; } @@ -127,7 +123,9 @@ void legacy_group_info::into(ugroups_legacy_group_info& c) && { void base_group_info::load(const dict& info_dict) { priority = int_or_0(info_dict, "+"); - joined_at = to_epoch_seconds(std::max(0, int_or_0(info_dict, "j"))); + // This value could have been accidentally stored in ms by a previous version, so pass it + // through to_sys_seconds: + joined_at = to_sys_seconds(std::max(0, int_or_0(info_dict, "j"))); int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) @@ -135,7 +133,9 @@ void base_group_info::load(const dict& info_dict) { else notifications = notify_mode::defaulted; - mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); + // This value could have been accidentally stored in ms by a previous version, so pass it + // through to_sys_seconds: + mute_until = to_sys_seconds(int_or_0(info_dict, "!")); invited = int_or_0(info_dict, "i"); } @@ -406,9 +406,9 @@ void UserGroups::set(const community_info& c) { void UserGroups::set_base(const base_group_info& bg, DictFieldProxy& info) const { set_nonzero_int(info["+"], bg.priority); - set_positive_int(info["j"], to_epoch_seconds(bg.joined_at)); + set_ts(info["j"], bg.joined_at); set_positive_int(info["@"], static_cast(bg.notifications)); - set_positive_int(info["!"], to_epoch_seconds(bg.mute_until)); + set_ts(info["!"], bg.mute_until); set_flag(info["i"], bg.invited); // We don't set n here because it's subtly different in the three group types } diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index e30ac306..835ec486 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -55,9 +55,9 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); CHECK_FALSE(c.profile_picture); - CHECK(c.created == 0); + CHECK(c.created.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); CHECK_FALSE(contacts.needs_push()); CHECK_FALSE(contacts.needs_dump()); @@ -68,9 +68,9 @@ TEST_CASE("Contacts", "[config][contacts]") { c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; - c.created = created_ts * 1'000; + c.created = session::to_sys_seconds(created_ts * 1'000); // test setting ms c.notifications = session::config::notify_mode::all; - c.mute_until = (now + 1800) * 1'000'000; + c.mute_until = session::to_sys_seconds((now + 1800) * 1'000'000); // test setting us contacts.set(c); @@ -116,9 +116,9 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); CHECK_FALSE(x->blocked); - CHECK(x->created == created_ts); + CHECK(x->created.time_since_epoch() == created_ts * 1s); CHECK(x->notifications == session::config::notify_mode::all); - CHECK(x->mute_until == now + 1800); + CHECK(x->mute_until.time_since_epoch() == (now + 1800) * 1s); auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"sv; auto c2 = contacts2.get_or_construct(another_id); diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 502ea132..a1e26273 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/util.hpp" #include "utils.hpp" TEST_CASE("Conversations", "[config][conversations]") { @@ -48,15 +49,15 @@ TEST_CASE("Conversations", "[config][conversations]") { auto c = convos.get_or_construct_1to1(definitely_real_id); CHECK(c.session_id == definitely_real_id); - CHECK(c.last_read == 0); + CHECK(c.last_read.time_since_epoch() == 0s); CHECK_FALSE(convos.needs_push()); CHECK_FALSE(convos.needs_dump()); CHECK(std::get(convos.push()) == 0); - auto now_ms = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); + auto now_ms = std::chrono::time_point_cast( + std::chrono::system_clock::now()); + static_assert(std::same_as); c.last_read = now_ms; @@ -88,7 +89,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto g = convos.get_or_construct_group(benders_nightmare_group); CHECK(g.id == benders_nightmare_group); - CHECK(g.last_read == 0); + CHECK(g.last_read.time_since_epoch() == 0s); CHECK_FALSE(g.unread); g.last_read = now_ms; @@ -100,7 +101,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto lb = convos.get_or_construct_blinded_1to1(legacy_blinded_id, true); CHECK(lb.blinded_session_id == legacy_blinded_id); - CHECK(lb.last_read == 0); + CHECK(lb.last_read.time_since_epoch() == 0s); CHECK_FALSE(lb.unread); lb.last_read = now_ms; @@ -109,7 +110,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto b = convos.get_or_construct_blinded_1to1(blinded_id, false); CHECK(b.blinded_session_id == blinded_id); - CHECK(b.last_read == 0); + CHECK(b.last_read.time_since_epoch() == 0s); CHECK_FALSE(b.unread); b.last_read = now_ms; @@ -174,7 +175,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto c3 = convos2.get_or_construct_legacy_group( "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"); - c3.last_read = now_ms - 50; + c3.last_read = now_ms - 50ms; convos2.set(c3); auto c4 = convos2.get_or_construct_blinded_1to1( @@ -605,29 +606,26 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { auto pk = some_pubkey(x); return "05" + oxenc::to_hex(pk.begin(), pk.end()); }; - const auto now = std::chrono::system_clock::now() - 1ms; - auto unix_timestamp = [&now](int days_ago) -> int64_t { - return std::chrono::duration_cast( - (now - days_ago * 24h).time_since_epoch()) - .count(); - }; + const auto now = std::chrono::time_point_cast( + std::chrono::system_clock::now()) - + 1ms; for (int i = 0; i <= 65; i++) { if (i % 3 == 0) { auto c = convos.get_or_construct_1to1(some_session_id(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); } else if (i % 3 == 1) { auto c = convos.get_or_construct_legacy_group(some_session_id(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); } else { auto c = convos.get_or_construct_community( "https://example.org", "room{}"_format(i), some_pubkey(i)); - c.last_read = unix_timestamp(i); + c.last_read = now - i * 24h; if (i % 5 == 0) c.unread = true; convos.set(c); @@ -652,13 +650,19 @@ TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { // internals like this!) // These ones wouldn't be stored by the normal `set()` interface, but won't get pruned either: - convos.data["1"][oxenc::from_hex(some_session_id(80))]["r"] = unix_timestamp(33); - convos.data["1"][oxenc::from_hex(some_session_id(81))]["r"] = unix_timestamp(40); - convos.data["1"][oxenc::from_hex(some_session_id(82))]["r"] = unix_timestamp(44); + convos.data["1"][oxenc::from_hex(some_session_id(80))]["r"] = + (now - 33 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(81))]["r"] = + (now - 40 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(82))]["r"] = + (now - 44 * 24h).time_since_epoch().count(); // These ones should get pruned as soon as we push: - convos.data["1"][oxenc::from_hex(some_session_id(83))]["r"] = unix_timestamp(45); - convos.data["1"][oxenc::from_hex(some_session_id(84))]["r"] = unix_timestamp(46); - convos.data["1"][oxenc::from_hex(some_session_id(85))]["r"] = unix_timestamp(1000); + convos.data["1"][oxenc::from_hex(some_session_id(83))]["r"] = + (now - 45 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(84))]["r"] = + (now - 46 * 24h).time_since_epoch().count(); + convos.data["1"][oxenc::from_hex(some_session_id(85))]["r"] = + (now - 1000 * 24h).time_since_epoch().count(); CHECK(convos.size_1to1() == 19); int count = 0; diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index db94916b..be738a1f 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -4,12 +4,12 @@ #include #include -#include #include #include #include #include "session/config/notify.hpp" +#include "session/util.hpp" #include "utils.hpp" static constexpr int64_t created_ts = 1680064059; @@ -117,9 +117,9 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(c.priority == 0); CHECK(c.name == ""); CHECK(c.members().empty()); - CHECK(c.joined_at == 0); + CHECK(c.joined_at.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); CHECK_FALSE(groups.needs_push()); CHECK_FALSE(groups.needs_dump()); @@ -136,9 +136,9 @@ TEST_CASE("User Groups", "[config][groups]") { c.name = "Englishmen"; c.disappearing_timer = 60min; - c.joined_at = created_ts * 1000; // milliseconds + c.joined_at = session::to_sys_seconds(created_ts * 1000); // milliseconds c.notifications = session::config::notify_mode::mentions_only; - c.mute_until = now + 3600; + c.mute_until = session::to_sys_seconds(now + 3600); CHECK(c.insert(users[0], false)); CHECK(c.insert(users[1], true)); CHECK(c.insert(users[2], false)); @@ -243,9 +243,9 @@ TEST_CASE("User Groups", "[config][groups]") { CHECK(c1.priority == 3); CHECK(c1.members() == expected_members); CHECK(c1.name == "Englishmen"); - CHECK(c1.joined_at == created_ts); + CHECK(c1.joined_at.time_since_epoch() == created_ts * 1s); CHECK(c1.notifications == session::config::notify_mode::mentions_only); - CHECK(c1.mute_until == now + 3600); + CHECK(c1.mute_until.time_since_epoch() == (now + 3600) * 1s); CHECK_FALSE(g2.needs_push()); CHECK_FALSE(g2.needs_dump()); @@ -457,9 +457,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c.secretkey.empty()); CHECK(c.id == definitely_real_id); CHECK(c.priority == 0); - CHECK(c.joined_at == 0); + CHECK(c.joined_at.time_since_epoch() == 0s); CHECK(c.notifications == session::config::notify_mode::defaulted); - CHECK(c.mute_until == 0); + CHECK(c.mute_until.time_since_epoch() == 0s); c.secretkey = session::to_vector(ed_sk); // This *isn't* the right secret key for the group, so // won't propagate, and so auth data will: @@ -485,16 +485,16 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { CHECK(c2->id == definitely_real_id); CHECK(c2->priority == 0); - CHECK(c2->joined_at == 0); + CHECK(c2->joined_at.time_since_epoch() == 0s); CHECK(c2->notifications == session::config::notify_mode::defaulted); - CHECK(c2->mute_until == 0); + CHECK(c2->mute_until.time_since_epoch() == 0s); CHECK_FALSE(c2->invited); CHECK(c2->name == ""); c2->priority = 123; - c2->joined_at = (int64_t)1'234'567'890 * 1'000; + c2->joined_at = session::to_sys_seconds((int64_t)1'234'567'890 * 1'000); // ms c2->notifications = session::config::notify_mode::mentions_only; - c2->mute_until = (int64_t)456'789'012 * 1'000'000; + c2->mute_until = session::to_sys_seconds((int64_t)456'789'012 * 1'000'000); // µs c2->invited = true; c2->name = "Magic Special Room"; @@ -526,9 +526,9 @@ TEST_CASE("User Groups -- (non-legacy) groups", "[config][groups][new]") { "0000000000000000000000000000"); CHECK(c3->id == definitely_real_id); CHECK(c3->priority == 123); - CHECK(c3->joined_at == 1234567890); + CHECK(c3->joined_at.time_since_epoch() == 1234567890s); CHECK(c3->notifications == session::config::notify_mode::mentions_only); - CHECK(c3->mute_until == 456789012); + CHECK(c3->mute_until.time_since_epoch() == 456789012s); CHECK(c3->invited); CHECK(c3->name == "Magic Special Room"); @@ -731,7 +731,7 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { auto grp = c2.get_legacy_group(definitely_real_id); REQUIRE(grp); CHECK(grp->members() == expected_members); - CHECK(grp->joined_at == created_ts); + CHECK(grp->joined_at.time_since_epoch() == created_ts * 1s); } TEST_CASE("User groups empty member bug", "[config][groups][bug]") { @@ -843,30 +843,35 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro { auto lg = c.get_or_construct_legacy_group( "051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - int64_t joined_at = get_timestamp_us(); - int64_t mute_until = get_timestamp_s(); - lg.joined_at = joined_at; - lg.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_us(); + int64_t mute_until_raw = get_timestamp_s(); + auto joined_at = joined_at_raw * 1us; + auto mute_until = mute_until_raw * 1s; + lg.joined_at = session::to_sys_seconds(joined_at_raw); // µs + lg.mute_until = session::to_sys_seconds(mute_until_raw); // s c.set(lg); auto lg2 = c.get_or_construct_legacy_group( "051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(lg2.joined_at == joined_at / 1'000'000); // joined_at was given in microseconds - CHECK(lg2.mute_until == mute_until); // mute_until was given in seconds + CHECK(lg2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); + CHECK(lg2.mute_until.time_since_epoch() == mute_until); c.erase_legacy_group("051234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); } { auto gr = c.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - int64_t joined_at = get_timestamp_ms(); - int64_t mute_until = get_timestamp_us(); - gr.joined_at = joined_at; - gr.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_ms(); + int64_t mute_until_raw = get_timestamp_us(); + auto joined_at = joined_at_raw * 1ms; + auto mute_until = mute_until_raw * 1us; + gr.joined_at = session::to_sys_seconds(joined_at_raw); // ms + gr.mute_until = session::to_sys_seconds(mute_until_raw); // µs c.set(gr); auto gr2 = c.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(gr2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds - CHECK(gr2.mute_until == mute_until / 1'000'000); // mute_until was given in microseconds + // Non-whole second timestamp components should have been truncate: + CHECK(gr2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); + CHECK(gr2.mute_until.time_since_epoch() == mute_until - mute_until % 1s); c.erase_group("031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); } @@ -876,14 +881,16 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro const auto url = "http://example.org:5678"; const auto room = "sudoku_room"; auto comm = c.get_or_construct_community(url, room, open_group_pubkey); - int64_t joined_at = get_timestamp_ms(); - int64_t mute_until = get_timestamp_ms(); - comm.joined_at = joined_at; - comm.mute_until = mute_until; + int64_t joined_at_raw = get_timestamp_ms(); + int64_t mute_until_raw = get_timestamp_ms(); + auto joined_at = joined_at_raw * 1ms; + auto mute_until = mute_until_raw * 1ms; + comm.joined_at = session::to_sys_seconds(joined_at_raw); + comm.mute_until = session::to_sys_seconds(mute_until_raw); c.set(comm); auto comm2 = c.get_or_construct_community(url, room, open_group_pubkey); - CHECK(comm2.joined_at == joined_at / 1'000); // joined_at was given in milliseconds - CHECK(comm2.mute_until == mute_until / 1'000); // mute_until was given in milliseconds + CHECK(comm2.joined_at.time_since_epoch() == joined_at - joined_at % 1s); // ms + CHECK(comm2.mute_until.time_since_epoch() == mute_until - mute_until % 1s); // ms c.erase_community(url, room); } { @@ -891,24 +898,19 @@ TEST_CASE("User groups mute_until & joined_at are always seconds", "[config][gro // - an invalid joined_at (1'733'979'503'520) and // - an invalid mute_until (1'733'979'503'520'780) values const auto dump_with_not_seconds = - "64313a21693165313a243231303a64313a23693165313a2664313a676433333a031234567890abcdef" - "1234" - "567890abcdef1234567890abcdef1234567890abcdef64313a21693137333339373935303335323037" - "3830" - "65313a4b303a313a6a693137333339373935303335323065656565313a3c6c6c69306533323aea173b" - "57be" - "ca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a3d64313a67643333" - "3a03" - "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef64313a21303a313a4b" - "303a" - "313a6a303a65656565313a28303a313a296c6565"_hexbytes; + "64313a21693165313a243231303a64313a23693165313a2664313a676433333a031234567890abcd" + "ef1234567890abcdef1234567890abcdef1234567890abcdef64313a216931373333393739353033" + "35323037383065313a4b303a313a6a693137333339373935303335323065656565313a3c6c6c6930" + "6533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565" + "313a3d64313a676433333a031234567890abcdef1234567890abcdef1234567890abcdef12345678" + "90abcdef64313a21303a313a4b303a313a6a303a65656565313a28303a313a296c6565"_hexbytes; session::config::UserGroups c2{std::span{seed}, dump_with_not_seconds}; auto gr = c2.get_or_construct_group( "031234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"); - CHECK(gr.joined_at == 1'733'979'503'520 / 1'000); - CHECK(gr.mute_until == 1'733'979'503'520'780 / 1'000'000); + CHECK(gr.joined_at.time_since_epoch() == 1'733'979'503'520ms - 520ms); + CHECK(gr.mute_until.time_since_epoch() == 1'733'979'503'520'780us - 520'780us); } } diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 4dfea5c1..638786c7 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -76,9 +76,11 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); ginfo2.set_expiry_timer(1h); constexpr int64_t create_time{1682529839}; - ginfo2.set_created(create_time); - ginfo2.set_delete_before((create_time + 50 * 86400) * 1'000'000); // as microseconds - ginfo2.set_delete_attach_before((create_time + 70 * 86400) * 1'000); // as milliseconds + ginfo2.set_created(session::to_sys_seconds(create_time)); + // µs: + ginfo2.set_delete_before(session::to_sys_seconds((create_time + 50 * 86400) * 1'000'000)); + // ms: + ginfo2.set_delete_attach_before(session::to_sys_seconds((create_time + 70 * 86400) * 1'000)); ginfo2.destroy_group(); auto [s2, p2, o2] = ginfo2.push(); @@ -106,14 +108,18 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo1.needs_push()); auto [s3, p3, o3] = ginfo1.push(); + constexpr std::chrono::sys_seconds expected_created{create_time * 1s}; + constexpr std::chrono::sys_seconds expected_del_before{create_time * 1s + 50 * 24h}; + constexpr std::chrono::sys_seconds expected_del_attach{create_time * 1s + 70 * 24h}; + CHECK(ginfo1.get_name() == "Better name!"); CHECK(ginfo1.get_profile_pic().url == "http://example.com/12345"); CHECK(ginfo1.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo1.get_expiry_timer() == 1h); - CHECK(ginfo1.get_created() == create_time); - CHECK(ginfo1.get_delete_before() == create_time + 50 * 86400); - CHECK(ginfo1.get_delete_attach_before() == create_time + 70 * 86400); + CHECK(ginfo1.get_created() == expected_created); + CHECK(ginfo1.get_delete_before() == expected_del_before); + CHECK(ginfo1.get_delete_attach_before() == expected_del_attach); CHECK(ginfo1.is_destroyed()); ginfo1.confirm_pushed(s3, {"fakehash3"}); @@ -126,9 +132,9 @@ TEST_CASE("Group Info settings", "[config][groups][info]") { CHECK(ginfo2.get_profile_pic().key == "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes); CHECK(ginfo2.get_expiry_timer() == 1h); - CHECK(ginfo2.get_created() == create_time); - CHECK(ginfo2.get_delete_before() == create_time + 50 * 86400); - CHECK(ginfo2.get_delete_attach_before() == create_time + 70 * 86400); + CHECK(ginfo2.get_created() == expected_created); + CHECK(ginfo2.get_delete_before() == expected_del_before); + CHECK(ginfo2.get_delete_attach_before() == expected_del_attach); CHECK(ginfo2.is_destroyed()); CHECK_THROWS( @@ -245,7 +251,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { // Now let's get more complicated: we will have *two* valid signers who submit competing updates ginfo_rw2.set_name("Super Group 2"); - ginfo_rw2.set_created(12345); + ginfo_rw2.set_created(session::to_sys_seconds(12345)); ginfo_rw.set_name("Super Group 3"); ginfo_rw.set_expiry_timer(365 * 24h); @@ -299,7 +305,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(*n == "Super Group 2"); auto c = g.get_created(); REQUIRE(c); - CHECK(*c == 12345); + CHECK(c->time_since_epoch() == 12345s); auto et = g.get_expiry_timer(); REQUIRE(et); CHECK(*et == 365 * 24h);