diff --git a/.drone.jsonnet b/.drone.jsonnet index e8701988..9477df61 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -357,7 +357,7 @@ local static_build(name, clang(17), full_llvm(17), debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'), - debian_build('Debian 12', docker_base + 'debian-bookworm', extra_setup=debian_backports('bookworm', ['cmake'])), + debian_build('Debian 12', docker_base + 'debian-bookworm'), debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'), debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'), diff --git a/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 e2752153..e71b1bcf 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -20,6 +20,7 @@ typedef struct contacts_contact { char name[101]; char nickname[101]; user_profile_pic profile_pic; + int64_t profile_updated; // unix timestamp (seconds) bool approved; bool approved_me; @@ -27,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; @@ -36,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 string 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`. @@ -208,6 +234,147 @@ 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(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_id, +/// [in] bool legacy_blinding, +/// [out] contacts_blinded_contact* blinded_contact +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `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( + config_object* conf, + const char* blinded_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) LIBSESSION_WARN_UNUSED; + +/// 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. +/// +/// 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( + config_object* conf, const contacts_blinded_contact* bc); + +/// 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( +/// [in, out] config_object* conf, +/// [in] const char* community_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* community_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 757e6cd0..1d80e014 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; @@ -44,8 +46,25 @@ namespace session::config { /// E - Disappearing message timer, in seconds. Omitted when `e` is omitted. /// j - Unix timestamp (seconds) when the contact was created ("j" to match user_groups /// equivalent "j"oined field). Omitted if 0. +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. +/// +/// 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: +/// +/// 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; @@ -53,6 +72,8 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; + std::chrono::sys_seconds profile_updated{}; /// The unix timestamp (seconds) that this + /// profile information was last updated. bool approved = false; bool approved_me = false; bool blocked = false; @@ -61,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); @@ -97,6 +119,55 @@ struct contact_info { void load(const dict& info_dict); }; +struct blinded_contact_info { + community comm; + + const std::string session_id() const; // in hex + std::string name; + profile_pic profile_picture; + bool legacy_blinding; + std::chrono::sys_seconds created{}; // Unix timestamp (seconds) when this contact was added + + blinded_contact_info() = default; + explicit blinded_contact_info( + std::string_view community_base_url, + std::span community_pubkey, + std::string_view blinded_id, + 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); + + /// 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; + void load(const dict& info_dict); +}; + class Contacts : public ConfigBase { public: @@ -230,6 +301,17 @@ class Contacts : public ConfigBase { /// - `profile_pic` -- profile pic of the contact void set_profile_pic(std::string_view session_id, profile_pic pic); + /// API: contacts/contacts::set_profile_updated + /// + /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you + /// should use `set()` instead). + /// + /// Inputs: + /// - `session_id` -- hex string of the session id + /// - `profile_updated` -- profile updated unix timestamp (seconds) of the contact. (To convert + /// a raw s/ms/µs integer value, use session::to_sys_seconds). + void set_profile_updated(std::string_view session_id, std::chrono::sys_seconds profile_updated); + /// API: contacts/contacts::set_approved /// /// Alternative to `set()` for setting a single field. (If setting multiple fields at once you @@ -303,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 /// @@ -339,6 +428,96 @@ class Contacts : public ConfigBase { bool accepts_protobuf() const override { return true; } + protected: + // Drills into the nested dicts to access community details + DictFieldProxy blinded_contact_field( + const blinded_contact_info& bc, + std::span* get_pubkey = nullptr) const; + + public: + /// API: contacts/Contacts::blinded + /// + /// Retrieves a list of all known blinded contacts. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::vector` - Returns a list of blinded_contact_info + std::vector blinded() 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: + /// - `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 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 + /// + /// 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, e.g.: + /// + ///```cpp + /// auto c = contacts.get_blinded(pubkey, legacy_blinding); + /// c.name = "Session User 42"; + /// contacts.set_blinded(c); + ///``` + /// + /// Inputs: + /// - `bc` -- set_blinded value to set + void set_blinded(const blinded_contact_info& bc); + + /// 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 + /// 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( + 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..a713b8e2 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,11 +56,18 @@ 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 { struct base { - int64_t last_read = 0; + sys_milliseconds last_read{}; bool unread = false; protected: @@ -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/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/groups/members.h b/include/session/config/groups/members.h index d502fbe2..da07bbfb 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -38,6 +38,7 @@ typedef struct config_group_member { // These two will be 0-length strings when unset: char name[101]; user_profile_pic profile_pic; + int64_t profile_updated; // unix timestamp (seconds) bool admin; int invited; // 0 == unset, STATUS_SENT = invited, STATUS_FAILED = invite failed to send, diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index d35fa52c..0ea32b00 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -40,6 +40,7 @@ using namespace std::literals; /// resent) /// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet. /// - omitted once the promotion is accepted (i.e. once `A` gets set). +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. constexpr int STATUS_SENT = 1, STATUS_FAILED = 2, STATUS_NOT_SENT = 3; constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; @@ -100,6 +101,13 @@ struct member { /// member. profile_pic profile_picture; + /// API: groups/member::profile_updated + /// + /// Member variable + /// + /// The unix timestamp (seconds) that this profile information was last updated. + std::chrono::sys_seconds profile_updated{}; + /// API: groups/member::admin /// /// Member variable diff --git a/include/session/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 2cd85840..9cd6d452 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -249,12 +249,31 @@ 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) { + 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())>); + +// 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 093c0a9c..2f52f972 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,8 +1,13 @@ #include "session/config/contacts.hpp" +#include +#include #include #include +#include +#include +#include #include #include "internal.hpp" @@ -14,8 +19,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,18 +65,9 @@ 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 = maybe_string(info_dict, "n").value_or(""); - nickname = maybe_string(info_dict, "N").value_or(""); + name = string_or_empty(info_dict, "n"); + nickname = string_or_empty(info_dict, "N"); auto url = maybe_string(info_dict, "p"); auto key = maybe_vector(info_dict, "q"); @@ -83,13 +78,14 @@ void contact_info::load(const dict& info_dict) { profile_picture.clear(); } - approved = maybe_int(info_dict, "a").value_or(0); - approved_me = maybe_int(info_dict, "A").value_or(0); - blocked = maybe_int(info_dict, "b").value_or(0); + profile_updated = ts_or_epoch(info_dict, "t"); + approved = int_or_0(info_dict, "a"); + approved_me = int_or_0(info_dict, "A"); + blocked = int_or_0(info_dict, "b"); - priority = maybe_int(info_dict, "+").value_or(0); + priority = int_or_0(info_dict, "+"); - int notify = maybe_int(info_dict, "@").value_or(0); + int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) { notifications = static_cast(notify); if (notifications == notify_mode::mentions_only) @@ -97,9 +93,11 @@ 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)); + // 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_ = maybe_int(info_dict, "e").value_or(0); + int exp_mode_ = int_or_0(info_dict, "e"); if (exp_mode_ >= static_cast(expiration_mode::none) && exp_mode_ <= static_cast(expiration_mode::after_read)) exp_mode = static_cast(exp_mode_); @@ -109,7 +107,7 @@ void contact_info::load(const dict& info_dict) { if (exp_mode == expiration_mode::none) exp_timer = 0s; else { - int secs = maybe_int(info_dict, "E").value_or(0); + int secs = int_or_0(info_dict, "E"); if (secs <= 0) { exp_mode = expiration_mode::none; exp_timer = 0s; @@ -118,7 +116,9 @@ void contact_info::load(const dict& info_dict) { } } - created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); + // 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 { @@ -131,17 +131,18 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } + c.profile_updated = profile_updated.time_since_epoch().count(); c.approved = approved; c.approved_me = approved_me; c.blocked = blocked; 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} { @@ -154,17 +155,18 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, profile_picture.url = c.profile_pic.url; profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); } + profile_updated = to_sys_seconds(c.profile_updated); approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; 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 { @@ -179,20 +181,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); @@ -200,17 +188,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]; @@ -227,6 +204,8 @@ void Contacts::set(const contact_info& contact) { info["q"], contact.profile_picture.key); + set_ts(info["t"], contact.profile_updated); + set_flag(info["a"], contact.approved); set_flag(info["A"], contact.approved_me); set_flag(info["b"], contact.blocked); @@ -237,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, @@ -246,17 +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)); -} - -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); + set_ts(info["j"], contact.created); } void Contacts::set_name(std::string_view session_id, std::string name) { @@ -279,6 +248,12 @@ void Contacts::set_profile_pic(std::string_view session_id, profile_pic pic) { c.profile_picture = std::move(pic); set(c); } +void Contacts::set_profile_updated( + std::string_view session_id, std::chrono::sys_seconds profile_updated) { + auto c = get_or_construct(session_id); + c.profile_updated = profile_updated; + set(c); +} void Contacts::set_approved(std::string_view session_id, bool approved) { auto c = get_or_construct(session_id); c.approved = approved; @@ -315,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); } @@ -329,22 +304,192 @@ 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(); +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 = string_or_empty(info_dict, "n"); + + 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 = int_or_0(info_dict, "y"); + created = ts_or_epoch(info_dict, "j"); +} + +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 = created.time_since_epoch().count(); +} + +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_sys_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()]; + 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.comm.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 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()}; + std::shared_ptr val; + + while (!comm.done()) { + if (comm.load(val)) + if (auto* ptr = std::get_if(val.get()); + ptr && ptr->session_id() == blinded_id_hex) + return *ptr; + comm.advance(); + } + } + + return std::nullopt; +} + +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()) { + 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; +} + +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] + + // 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_ts(info["j"], bc.created); +} + +bool Contacts::erase_blinded( + std::string_view base_url_, std::string_view blinded_id, bool legacy_blinding) { + check_session_id(blinded_id, legacy_blinding ? "15" : "25"); + + auto base_url = community::canonical_url(base_url_); + auto pk = std::string(blinded_id.substr(2)); + auto info = data["b"][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 @@ -387,6 +532,173 @@ 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( + config_object* conf, + const char* blinded_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) { + return wrap_exceptions( + conf, + [&] { + if (auto bc = unbox(conf)->get_blinded(blinded_id, legacy_blinding)) { + bc->into(*blinded_contact); + return true; + } + return false; + }, + false); +} + +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(); + + 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( + config_object* conf, const contacts_blinded_contact* bc) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_blinded(blinded_contact_info{*bc}); + return true; + }, + false); +} + +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( + community_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()}; @@ -409,3 +721,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" diff --git a/src/config/convo_info_volatile.cpp b/src/config/convo_info_volatile.cpp index 8d1206c7..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,17 +70,37 @@ 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; } + 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{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.time_since_epoch().count(); + c.unread = unread; + c.legacy_blinding = legacy_blinding; + } + 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 = as_sys_ms(int_or_0(info_dict, "r")); + unread = (bool)int_or_0(info_dict, "u"); } } // namespace convo @@ -213,6 +229,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); @@ -221,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) @@ -286,6 +322,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 +359,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 +380,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 +419,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 +455,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 +470,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 +481,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 +500,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 +523,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 +544,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 +684,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 +759,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 +792,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 +818,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 +856,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 +909,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); +} 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/groups/members.cpp b/src/config/groups/members.cpp index ca515e66..86c3b085 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -66,6 +66,7 @@ void Members::set(const member& mem) { info["q"], mem.profile_picture.key); + set_ts(info["t"], mem.profile_updated); set_flag(info["A"], mem.admin); set_positive_int(info["P"], mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); @@ -84,7 +85,7 @@ void Members::set(const member& mem) { } void member::load(const dict& info_dict) { - name = maybe_string(info_dict, "n").value_or(""); + name = string_or_empty(info_dict, "n"); auto url = maybe_string(info_dict, "p"); auto key = maybe_vector(info_dict, "q"); @@ -95,13 +96,13 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } - admin = maybe_int(info_dict, "A").value_or(0); - invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); - promotion_status = maybe_int(info_dict, "P").value_or(0); - removed_status = maybe_int(info_dict, "R").value_or(0); - supplement = invite_status > 0 && !(admin || promotion_status > 0) - ? maybe_int(info_dict, "s").value_or(0) - : 0; + profile_updated = ts_or_epoch(info_dict, "t"); + admin = int_or_0(info_dict, "A"); + invite_status = admin ? 0 : int_or_0(info_dict, "I"); + promotion_status = int_or_0(info_dict, "P"); + removed_status = int_or_0(info_dict, "R"); + supplement = + invite_status > 0 && !(admin || promotion_status > 0) ? int_or_0(info_dict, "s") : 0; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -187,6 +188,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { profile_picture.url = m.profile_pic.url; profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32); } + profile_updated = to_sys_seconds(m.profile_updated); admin = m.admin; invite_status = (m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT) @@ -211,6 +213,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } + m.profile_updated = profile_updated.time_since_epoch().count(); m.admin = admin; static_assert(groups::STATUS_SENT == ::STATUS_SENT); static_assert(groups::STATUS_FAILED == ::STATUS_FAILED); diff --git a/src/config/internal.cpp b/src/config/internal.cpp index a81446ef..0b2051dc 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -81,18 +81,49 @@ std::optional maybe_int(const session::config::dict& d, const char* key return std::nullopt; } +int64_t int_or_0(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return *i; + return 0; +} + +std::optional maybe_ts(const session::config::dict& d, const char* key) { + std::optional result; + if (auto* i = maybe_scalar(d, key)) + result.emplace(std::chrono::seconds{*i}); + return result; +} + +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return std::chrono::sys_seconds{std::chrono::seconds{*i}}; + return std::chrono::sys_seconds{}; +} + std::optional maybe_string(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string string_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""s; +} + std::optional maybe_sv(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string_view sv_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""sv; +} + std::optional> maybe_vector( const session::config::dict& d, const char* key) { std::optional> result; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 74cc31fd..337523c5 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -147,18 +147,38 @@ const config::set* maybe_set(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out an int64_t; nullopt if not there (or not int) std::optional maybe_int(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out an int64_t; returns 0 if the value is not there or not an +// int. Equivalent to `maybe_int(d, key).value_or(0)`. +int64_t int_or_0(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out an int64_t containing unix timestamp seconds, returns it +// wrapped in a std::chrono::sys_seconds. Returns nullopt if not there (or not int). +std::optional maybe_ts(const session::config::dict& d, const char* key); + +// Works like maybe_ts, except that if the value isn't present it returns a default-constructed +// sys_seconds (i.e. unix timestamp 0). Equivalent to `maybe_ts(d, +// key).value_or(std::chrono::sys_seconds{})`. +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key); + // Digs into a config `dict` to get out a string; nullopt if not there (or not string) std::optional maybe_string(const session::config::dict& d, const char* key); -// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not -// string) -std::optional> maybe_vector( - const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string; ""s if not there (or not string) +std::string string_or_empty(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out a string view; nullopt if not there (or not string). The // string view is only valid as long as the dict stays unchanged. std::optional maybe_sv(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string view; ""sv if not there (or not string). The +// string view is only valid as long as the dict stays unchanged. +std::string_view sv_or_empty(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not +// string) +std::optional> maybe_vector( + const session::config::dict& d, const char* key); + /// Sets a value to 1 if true, removes it if false. void set_flag(ConfigBase::DictFieldProxy&& field, bool val); @@ -172,6 +192,11 @@ void set_nonzero_int(ConfigBase::DictFieldProxy&& field, int64_t val); /// Sets an integer value, if positive; removes it if <= 0. void set_positive_int(ConfigBase::DictFieldProxy&& field, int64_t val); +/// Sets a unix timestamp as an integer, if positive; removes it if <= 0. +inline void set_ts(ConfigBase::DictFieldProxy&& field, std::chrono::sys_seconds val) { + set_positive_int(std::move(field), val.time_since_epoch().count()); +} + /// Sets a pair of values if the given condition is satisfied, clears both values otherwise. template void set_pair_if( diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index a93da2a5..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; } @@ -126,18 +122,22 @@ 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, "+"); + // 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 = 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)); + // 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 = 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() { @@ -409,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/src/util.cpp b/src/util.cpp index 7669d0e1..60409c58 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -87,4 +87,8 @@ std::tuple, std::optional().time_since_epoch())>); + } // namespace session diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 06a55166..835ec486 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,13 +50,14 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); + CHECK(c.profile_updated == std::chrono::sys_seconds{}); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); 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()); @@ -62,11 +65,12 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); + c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; - c.created = created_ts * 1'000; + 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); @@ -74,6 +78,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(contacts.get(definitely_real_id)->name == "Joe"); CHECK(contacts.get(definitely_real_id)->nickname == "Joey"); + CHECK(contacts.get(definitely_real_id)->profile_updated.time_since_epoch() == 1s); CHECK(contacts.get(definitely_real_id)->approved); CHECK(contacts.get(definitely_real_id)->approved_me); CHECK_FALSE(contacts.get(definitely_real_id)->profile_picture); @@ -106,13 +111,14 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); + CHECK(x->profile_updated.time_since_epoch() == 1s); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); 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); @@ -137,11 +143,13 @@ TEST_CASE("Contacts", "[config][contacts]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts.size() == 2); CHECK_FALSE(contacts.empty()); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); @@ -150,6 +158,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0].time_since_epoch() == 1s); + CHECK(profile_updateds[1].time_since_epoch() == 0s); // Conflict! Oh no! @@ -159,6 +169,7 @@ TEST_CASE("Contacts", "[config][contacts]") { // Client 2 adds a new friend: auto third_id = "052222222222222222222222222222222222222222222222222222222222222222"sv; contacts2.set_nickname(third_id, "Nickname 3"); + contacts2.set_profile_updated(third_id, session::to_sys_seconds(2)); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -216,15 +227,19 @@ TEST_CASE("Contacts", "[config][contacts]") { session_ids.clear(); nicknames.clear(); + profile_updateds.clear(); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); CHECK(session_ids[0] == another_id); CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); + CHECK(profile_updateds[0].time_since_epoch() == 0s); + CHECK(profile_updateds[1].time_since_epoch() == 2s); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" @@ -279,6 +294,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(strlen(c.name) == 0); CHECK(strlen(c.nickname) == 0); + CHECK(c.profile_updated == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -287,6 +303,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { strcpy(c.name, "Joe"); strcpy(c.nickname, "Joey"); + c.profile_updated = 1; c.approved = true; c.approved_me = true; c.created = created_ts; @@ -298,6 +315,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); + CHECK(c2.profile_updated == 1); CHECK(c2.approved); CHECK(c2.approved_me); CHECK_FALSE(c2.blocked); @@ -333,6 +351,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get(conf2, &c3, definitely_real_id)); CHECK(c3.name == "Joe"sv); CHECK(c3.nickname == "Joey"sv); + CHECK(c3.profile_updated == 1); CHECK(c3.approved); CHECK(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -343,6 +362,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get_or_construct(conf, &c3, another_id)); CHECK(strlen(c3.name) == 0); CHECK(strlen(c3.nickname) == 0); + CHECK(c3.profile_updated == 0); CHECK_FALSE(c3.approved); CHECK_FALSE(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -372,6 +392,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts_size(conf) == 2); contacts_iterator* it = contacts_iterator_new(conf); @@ -379,6 +400,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { session_ids.push_back(ci.session_id); nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); + profile_updateds.emplace_back(ci.profile_updated); } contacts_iterator_free(it); @@ -387,6 +409,8 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0] == 1); + CHECK(profile_updateds[1] == 0); // Changing things while iterating: it = contacts_iterator_new(conf); @@ -862,3 +886,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.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 = session::to_sys_seconds(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.time_since_epoch() == created_ts * 1s); + 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]); +} diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index daf1ed3a..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]") { @@ -35,6 +36,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()); @@ -43,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; @@ -83,13 +89,34 @@ 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; 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.time_since_epoch() == 0s); + 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.time_since_epoch() == 0s); + 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 +154,20 @@ 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; @@ -134,9 +175,14 @@ 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( + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false); + c4.unread = true; + convos2.set(c4); + CHECK(convos2.needs_push()); std::tie(seqno, to_push, obs) = convos2.push(); @@ -152,6 +198,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 +210,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 +236,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,32 +251,43 @@ 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(); 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{ + "150000000000000000000000000000000000101010111010000110100001210000", + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }); } TEST_CASE("Conversations (C API)", "[config][conversations][c]") { @@ -314,6 +380,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 +437,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 +470,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,19 +488,25 @@ 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); CHECK(seen == std::vector{ - {"1-to-1: " - "051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: " - "055000000000000000000000000000000000000000000000000000000000000000", - "comm: http://example.org:5678/r/sudokuroom", - "lgr: " - "05ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - "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)); @@ -420,9 +515,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(); @@ -448,9 +546,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; @@ -461,9 +559,22 @@ 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; + 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]") { @@ -495,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); @@ -542,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); diff --git a/tests/test_group_members.cpp b/tests/test_group_members.cpp index 072d105c..017abe75 100644 --- a/tests/test_group_members.cpp +++ b/tests/test_group_members.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include #include #include @@ -72,6 +72,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + m.profile_updated = std::chrono::sys_seconds{1s}; gmem1.set(m); } // 10 members: @@ -81,6 +82,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { m.profile_picture.url = "http://example.com/{}"_format(i); m.profile_picture.key = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes; + m.profile_updated = session::to_sys_seconds(2); gmem1.set(m); } // 5 members with no attributes (not even a name): @@ -131,6 +133,7 @@ TEST_CASE("Group Members", "[config][groups][members]") { session::config::groups::member::Status::invite_not_sent); CHECK(m.admin); CHECK(m.name == "Admin {}"_format(i)); + CHECK(m.profile_updated.time_since_epoch() == 1s); CHECK_FALSE(m.profile_picture.empty()); CHECK(gmem2.get_status(m) == session::config::groups::member::Status::promotion_accepted); @@ -144,10 +147,12 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK_FALSE(m.admin); if (i < 20) { CHECK(m.name == "Member {}"_format(i)); + CHECK(m.profile_updated.time_since_epoch() == 2s); CHECK_FALSE(m.profile_picture.empty()); } else { CHECK(m.name.empty()); CHECK(m.profile_picture.empty()); + CHECK(m.profile_updated.time_since_epoch() == 0s); } } i++; @@ -155,9 +160,15 @@ TEST_CASE("Group Members", "[config][groups][members]") { CHECK(i == 25); } + for (int i = 5; i < 15; i++) { + auto m = gmem2.get_or_construct(sids[i]); + m.profile_updated += 1s; + gmem2.set(m); + } for (int i = 22; i < 50; i++) { auto m = gmem2.get_or_construct(sids[i]); m.name = "Member {}"_format(i); + m.profile_updated = std::chrono::sys_seconds{1s}; gmem2.set(m); } for (int i = 50; i < 55; i++) { @@ -211,6 +222,20 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); + if (i < 5) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 5 && i < 10) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 10 && i < 15) + CHECK(m.profile_updated.time_since_epoch() == 3s); + if (i >= 15 && i < 20) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 20 && i < 22) + CHECK(m.profile_updated.time_since_epoch() == 0s); + if (i >= 22 && i < 50) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 50) + CHECK(m.profile_updated.time_since_epoch() == 0s); if (i >= 10 && i < 25) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_sending); @@ -281,6 +306,20 @@ TEST_CASE("Group Members", "[config][groups][members]") { (i < 20 ? "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"_hexbytes : ""_hexbytes)); CHECK(m.profile_picture.url == (i < 20 ? "http://example.com/{}"_format(i) : "")); + if (i < 5) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 5 && i < 10) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 10 && i < 15) + CHECK(m.profile_updated.time_since_epoch() == 3s); + if (i >= 15 && i < 20) + CHECK(m.profile_updated.time_since_epoch() == 2s); + if (i >= 20 && i < 22) + CHECK(m.profile_updated.time_since_epoch() == 0s); + if (i >= 22 && i < 50) + CHECK(m.profile_updated.time_since_epoch() == 1s); + if (i >= 50) + CHECK(m.profile_updated.time_since_epoch() == 0s); if (is_prime100(i) || (i >= 25 && i < 50)) CHECK(gmem1.get_status(m) == session::config::groups::member::Status::invite_not_sent);