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..a93ce855 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,7 @@ if(CCACHE_PROGRAM) endif() project(libsession-util - VERSION 1.5.1 + VERSION 1.5.2 DESCRIPTION "Session client utility library" LANGUAGES ${LANGS}) diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index e2752153..08c47fbc 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; @@ -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..8f38ef06 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; @@ -97,6 +118,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 +300,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 @@ -339,6 +420,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..ce954ddb 100644 --- a/include/session/config/convo_info_volatile.hpp +++ b/include/session/config/convo_info_volatile.hpp @@ -16,6 +16,7 @@ struct convo_info_volatile_1to1; struct convo_info_volatile_community; struct convo_info_volatile_group; struct convo_info_volatile_legacy_group; +struct convo_info_volatile_blinded_1to1; } namespace session::config { @@ -55,6 +56,13 @@ class val_loader; /// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, /// but will be 0 if no messages are read. /// u - will be present and set to 1 if this conversation is specifically marked unread. +/// +/// b - outgoing blinded message request conversations. The key is the blinded Session ID without +/// the prefix. Values are dicts with keys: +/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included, +/// but will be 0 if no messages are read. +/// u - will be present and set to 1 if this conversation is specifically marked unread. +/// y - flag indicating whether the blinded message request is using legac"y" blinding. namespace convo { @@ -149,7 +157,34 @@ namespace convo { void into(convo_info_volatile_legacy_group& c) const; // Into c struct }; - using any = std::variant; + struct blinded_one_to_one : base { + std::string blinded_session_id; // in hex + bool legacy_blinding; + + /// API: convo_info_volatile/blinded_one_to_one::blinded_one_to_one + /// + /// Constructs an empty blinded_one_to_one from a blinded_session_id. Session ID can be + /// either bytes (33) or hex (66). + /// + /// Declaration: + /// ```cpp + /// explicit blinded_one_to_one(std::string&& blinded_session_id); + /// explicit blinded_one_to_one(std::string_view blinded_session_id); + /// ``` + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string of the blinded session id + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + explicit blinded_one_to_one(std::string&& blinded_session_id, bool legacy_blinding); + explicit blinded_one_to_one(std::string_view blinded_session_id, bool legacy_blinding); + + // Internal ctor/method for C API implementations: + blinded_one_to_one(const struct convo_info_volatile_blinded_1to1& c); // From c struct + void into(convo_info_volatile_blinded_1to1& c) const; // Into c struct + }; + + using any = std::variant; } // namespace convo class ConvoInfoVolatile : public ConfigBase { @@ -298,6 +333,22 @@ class ConvoInfoVolatile : public ConfigBase { /// - `std::optional` - Returns a group std::optional get_legacy_group(std::string_view pubkey_hex) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_blinded_1to1 + /// + /// Looks up and returns a blinded contact by blinded session ID (hex). Returns nullopt if the + /// blinded session ID was not found, otherwise returns a filled out + /// `convo::blinded_one_to_one`. + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string of the blinded Session ID + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + /// + /// Outputs: + /// - `std::optional` - Returns a contact + std::optional get_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_1to1 /// /// These are the same as the above `get` methods (without "_or_construct" in the name), except @@ -385,6 +436,22 @@ class ConvoInfoVolatile : public ConfigBase { /// - `convo::community` - Returns a group convo::community get_or_construct_community(std::string_view full_url) const; + /// API: convo_info_volatile/ConvoInfoVolatile::get_or_construct_blinded_1to1 + /// + /// These are the same as the above `get` methods (without "_or_construct" in the name), except + /// that when the conversation doesn't exist a new one is created, prefilled with the + /// pubkey/url/etc. + /// + /// Inputs: + /// - `blinded_session_id` -- Hex string blinded Session ID + /// - `legacy_blinding` -- flag indicating whether this blinded contact should use legacy + /// blinding + /// + /// Outputs: + /// - `convo::blinded_one_to_one` - Returns a blinded contact + convo::blinded_one_to_one get_or_construct_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) const; + /// API: convo_info_volatile/ConvoInfoVolatile::set /// /// Inserts or replaces existing conversation info. For example, to update a 1-to-1 @@ -402,6 +469,7 @@ class ConvoInfoVolatile : public ConfigBase { /// void set(const convo::group& c); /// void set(const convo::legacy_group& c); /// void set(const convo::community& c); + /// void set(const convo::blinded_one_to_one& c); /// void set(const convo::any& c); // Variant which can be any of the above /// ``` /// @@ -411,6 +479,7 @@ class ConvoInfoVolatile : public ConfigBase { void set(const convo::legacy_group& c); void set(const convo::group& c); void set(const convo::community& c); + void set(const convo::blinded_one_to_one& c); void set(const convo::any& c); // Variant which can be any of the above protected: @@ -469,6 +538,19 @@ class ConvoInfoVolatile : public ConfigBase { /// - `bool` - Returns true if found and removed, otherwise false bool erase_legacy_group(std::string_view pubkey_hex); + /// API: convo_info_volatile/ConvoInfoVolatile::erase_blinded_1to1 + /// + /// Removes a blinded one-to-one conversation. Returns true if found and removed, false if not + /// present. + /// + /// Inputs: + /// - `pubkey` -- hex blinded session id + /// - `legacy_blinding` -- flag indicating whether this blinded contact is using legacy blinding + /// + /// Outputs: + /// - `bool` - Returns true if found and removed, otherwise false + bool erase_blinded_1to1(std::string_view pubkey, bool legacy_blinding); + /// API: convo_info_volatile/ConvoInfoVolatile::erase /// /// Removes a conversation taking the convo::whatever record (rather than the pubkey/url). @@ -478,6 +560,7 @@ class ConvoInfoVolatile : public ConfigBase { /// bool erase(const convo::one_to_one& c); /// bool erase(const convo::community& c); /// bool erase(const convo::legacy_group& c); + /// bool erase(const convo::blinded_one_to_one& c); /// bool erase(const convo::any& c); // Variant of any of them /// ``` /// @@ -490,6 +573,7 @@ class ConvoInfoVolatile : public ConfigBase { bool erase(const convo::community& c); bool erase(const convo::group& c); bool erase(const convo::legacy_group& c); + bool erase(const convo::blinded_one_to_one& c); bool erase(const convo::any& c); // Variant of any of them @@ -506,6 +590,7 @@ class ConvoInfoVolatile : public ConfigBase { /// size_t size_communities() const; /// size_t size_groups() const; /// size_t size_legacy_groups() const; + /// size_t size_blinded_1to1() const; /// ``` /// /// Inputs: None @@ -520,6 +605,7 @@ class ConvoInfoVolatile : public ConfigBase { size_t size_communities() const; size_t size_groups() const; size_t size_legacy_groups() const; + size_t size_blinded_1to1() const; /// API: convo_info_volatile/ConvoInfoVolatile::empty /// @@ -549,6 +635,8 @@ class ConvoInfoVolatile : public ConfigBase { /// // use cg->id, cg->last_read /// } else if (const auto* lcg = std::get_if(&convo)) { /// // use lcg->id, lcg->last_read + /// } else if (const auto* bc = std::get_if(&convo)) { + /// // use bc->id, bc->last_read /// } /// } /// ``` @@ -570,6 +658,7 @@ class ConvoInfoVolatile : public ConfigBase { /// subtype_iterator begin_communities() const; /// subtype_iterator begin_groups() const; /// subtype_iterator begin_legacy_groups() const; + /// subtype_iterator begin_blinded_one_to_one() const; /// ``` /// /// Inputs: None @@ -597,10 +686,15 @@ class ConvoInfoVolatile : public ConfigBase { subtype_iterator begin_communities() const { return {data}; } subtype_iterator begin_groups() const { return {data}; } subtype_iterator begin_legacy_groups() const { return {data}; } + subtype_iterator begin_blinded_1to1() const { return {data}; } using iterator_category = std::input_iterator_tag; - using value_type = - std::variant; + using value_type = std::variant< + convo::one_to_one, + convo::community, + convo::group, + convo::legacy_group, + convo::blinded_one_to_one>; using reference = value_type&; using pointer = value_type*; using difference_type = std::ptrdiff_t; @@ -609,7 +703,7 @@ class ConvoInfoVolatile : public ConfigBase { protected: std::shared_ptr _val; std::optional _it_11, _end_11, _it_group, _end_group, _it_lgroup, - _end_lgroup; + _end_lgroup, _it_b11, _end_b11; std::optional _it_comm; void _load_val(); iterator() = default; // Constructs an end tombstone @@ -618,8 +712,10 @@ class ConvoInfoVolatile : public ConfigBase { bool oneto1, bool communities, bool groups, - bool legacy_groups); - explicit iterator(const DictFieldRoot& data) : iterator(data, true, true, true, true) {} + bool legacy_groups, + bool blinded_1to1); + explicit iterator(const DictFieldRoot& data) : + iterator(data, true, true, true, true, true) {} friend class ConvoInfoVolatile; public: @@ -645,7 +741,8 @@ class ConvoInfoVolatile : public ConfigBase { std::is_same_v, std::is_same_v, std::is_same_v, - std::is_same_v) {} + std::is_same_v, + std::is_same_v) {} friend class ConvoInfoVolatile; public: diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index d502fbe2..da07bbfb 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -38,6 +38,7 @@ typedef struct config_group_member { // These two will be 0-length strings when unset: char name[101]; user_profile_pic profile_pic; + int64_t profile_updated; // unix timestamp (seconds) bool admin; int invited; // 0 == unset, STATUS_SENT = invited, STATUS_FAILED = invite failed to send, diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index d35fa52c..0ea32b00 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -40,6 +40,7 @@ using namespace std::literals; /// resent) /// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet. /// - omitted once the promotion is accepted (i.e. once `A` gets set). +/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information. constexpr int STATUS_SENT = 1, STATUS_FAILED = 2, STATUS_NOT_SENT = 3; constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2; @@ -100,6 +101,13 @@ struct member { /// member. profile_pic profile_picture; + /// API: groups/member::profile_updated + /// + /// Member variable + /// + /// The unix timestamp (seconds) that this profile information was last updated. + std::chrono::sys_seconds profile_updated{}; + /// API: groups/member::admin /// /// Member variable diff --git a/include/session/util.hpp b/include/session/util.hpp index 2cd85840..fc57908f 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -257,4 +257,23 @@ inline int64_t to_epoch_seconds(int64_t timestamp) { : timestamp; } +// Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it. +inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) { + return std::chrono::sys_seconds{std::chrono::seconds{timestamp}}; +} + +// Helper function to transform a timestamp integer that might be seconds, milliseconds or +// microseconds to typesafe system clock seconds unix timestamp. +inline std::chrono::sys_seconds to_sys_seconds(int64_t timestamp) { + if (timestamp > 9'000'000'000'000) + timestamp /= 1'000'000; + else if (timestamp > 9'000'000'000) + timestamp /= 1'000; + return as_sys_seconds(timestamp); +} + +static_assert(std::is_same_v< + std::chrono::seconds, + decltype(std::declval().time_since_epoch())>); + } // namespace session diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 093c0a9c..5c1be6b4 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,8 +1,12 @@ #include "session/config/contacts.hpp" +#include +#include #include #include +#include +#include #include #include "internal.hpp" @@ -14,8 +18,7 @@ using namespace std::literals; using namespace session::config; - -LIBSESSION_C_API const size_t CONTACT_MAX_NAME_LENGTH = contact_info::MAX_NAME_LENGTH; +using namespace oxen::log::literals; // Check for agreement between various C/C++ types static_assert(sizeof(contacts_contact::name) == contact_info::MAX_NAME_LENGTH + 1); @@ -61,18 +64,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 +77,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 +92,9 @@ void contact_info::load(const dict& info_dict) { } else { notifications = notify_mode::defaulted; } - mute_until = to_epoch_seconds(maybe_int(info_dict, "!").value_or(0)); + mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); - int exp_mode_ = maybe_int(info_dict, "e").value_or(0); + int exp_mode_ = int_or_0(info_dict, "e"); if (exp_mode_ >= static_cast(expiration_mode::none) && exp_mode_ <= static_cast(expiration_mode::after_read)) exp_mode = static_cast(exp_mode_); @@ -109,7 +104,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 +113,7 @@ void contact_info::load(const dict& info_dict) { } } - created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); + created = to_epoch_seconds(int_or_0(info_dict, "j")); } void contact_info::into(contacts_contact& c) const { @@ -131,6 +126,7 @@ void contact_info::into(contacts_contact& c) const { } else { copy_c_str(c.profile_pic.url, ""); } + c.profile_updated = profile_updated.time_since_epoch().count(); c.approved = approved; c.approved_me = approved_me; c.blocked = blocked; @@ -154,6 +150,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, profile_picture.url = c.profile_pic.url; profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); } + profile_updated = to_sys_seconds(c.profile_updated); approved = c.approved; approved_me = c.approved_me; blocked = c.blocked; @@ -179,20 +176,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 +183,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 +199,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); @@ -249,16 +223,6 @@ void Contacts::set(const contact_info& contact) { set_positive_int(info["j"], to_epoch_seconds(contact.created)); } -LIBSESSION_C_API bool contacts_set(config_object* conf, const contacts_contact* contact) { - return wrap_exceptions( - conf, - [&] { - unbox(conf)->set(contact_info{*contact}); - return true; - }, - false); -} - void Contacts::set_name(std::string_view session_id, std::string name) { auto c = get_or_construct(session_id); c.set_name(std::move(name)); @@ -279,6 +243,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; @@ -329,22 +299,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 +527,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 +716,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..54f73b62 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -82,9 +82,29 @@ namespace convo { c.unread = unread; } + blinded_one_to_one::blinded_one_to_one(std::string&& sid, bool legacy_blinding) : + blinded_session_id{std::move(sid)}, legacy_blinding{legacy_blinding} { + check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); + } + blinded_one_to_one::blinded_one_to_one(std::string_view sid, bool legacy_blinding) : + blinded_session_id{sid}, legacy_blinding{legacy_blinding} { + check_session_id(blinded_session_id, legacy_blinding ? "15" : "25"); + } + blinded_one_to_one::blinded_one_to_one(const convo_info_volatile_blinded_1to1& c) : + base{c.last_read, c.unread}, + blinded_session_id{c.blinded_session_id, 66}, + legacy_blinding{c.legacy_blinding} {} + + void blinded_one_to_one::into(convo_info_volatile_blinded_1to1& c) const { + std::memcpy(c.blinded_session_id, blinded_session_id.data(), 67); + c.last_read = last_read; + c.unread = unread; + c.legacy_blinding = legacy_blinding; + } + void base::load(const dict& info_dict) { - last_read = maybe_int(info_dict, "r").value_or(0); - unread = (bool)maybe_int(info_dict, "u").value_or(0); + last_read = int_or_0(info_dict, "r"); + unread = (bool)int_or_0(info_dict, "u"); } } // namespace convo @@ -213,6 +233,28 @@ convo::legacy_group ConvoInfoVolatile::get_or_construct_legacy_group( return convo::legacy_group{std::string{pubkey_hex}}; } +std::optional ConvoInfoVolatile::get_blinded_1to1( + std::string_view pubkey_hex, bool legacy_blinding) const { + std::string pubkey = session_id_to_bytes(pubkey_hex, legacy_blinding ? "15" : "25"); + + auto* info_dict = data["b"][pubkey].dict(); + if (!info_dict) + return std::nullopt; + + auto result = + std::make_optional(std::string{pubkey_hex}, legacy_blinding); + result->load(*info_dict); + return result; +} + +convo::blinded_one_to_one ConvoInfoVolatile::get_or_construct_blinded_1to1( + std::string_view pubkey_hex, bool legacy_blinding) const { + if (auto maybe = get_blinded_1to1(pubkey_hex, legacy_blinding)) + return *std::move(maybe); + + return convo::blinded_one_to_one{std::string{pubkey_hex}, legacy_blinding}; +} + void ConvoInfoVolatile::set(const convo::one_to_one& c) { auto info = data["1"][session_id_to_bytes(c.session_id)]; set_base(c, info); @@ -286,6 +328,14 @@ void ConvoInfoVolatile::set(const convo::legacy_group& c) { set_base(c, info); } +void ConvoInfoVolatile::set(const convo::blinded_one_to_one& c) { + std::string pubkey = session_id_to_bytes(c.blinded_session_id, c.legacy_blinding ? "15" : "25"); + + auto info = data["b"][pubkey]; + set_nonzero_int(info["y"], c.legacy_blinding); + set_base(c, info); +} + template static bool erase_impl(Field convo) { bool ret = convo.exists(); @@ -315,6 +365,11 @@ bool ConvoInfoVolatile::erase(const convo::group& c) { bool ConvoInfoVolatile::erase(const convo::legacy_group& c) { return erase_impl(data["C"][session_id_to_bytes(c.id)]); } +bool ConvoInfoVolatile::erase(const convo::blinded_one_to_one& c) { + std::string pubkey = session_id_to_bytes(c.blinded_session_id, c.legacy_blinding ? "15" : "25"); + + return erase_impl(data["b"][pubkey]); +} bool ConvoInfoVolatile::erase(const convo::any& c) { return std::visit([this](const auto& c) { return erase(c); }, c); @@ -331,6 +386,10 @@ bool ConvoInfoVolatile::erase_group(std::string_view id) { bool ConvoInfoVolatile::erase_legacy_group(std::string_view id) { return erase(convo::legacy_group{id}); } +bool ConvoInfoVolatile::erase_blinded_1to1( + std::string_view blinded_session_id, bool legacy_blinding) { + return erase(convo::blinded_one_to_one{blinded_session_id, legacy_blinding}); +} size_t ConvoInfoVolatile::size_1to1() const { if (auto* d = data["1"].dict()) @@ -366,12 +425,24 @@ size_t ConvoInfoVolatile::size_legacy_groups() const { return 0; } +size_t ConvoInfoVolatile::size_blinded_1to1() const { + if (auto* d = data["b"].dict()) + return d->size(); + return 0; +} + size_t ConvoInfoVolatile::size() const { - return size_1to1() + size_communities() + size_legacy_groups() + size_groups(); + return size_1to1() + size_communities() + size_legacy_groups() + size_groups() + + size_blinded_1to1(); } ConvoInfoVolatile::iterator::iterator( - const DictFieldRoot& data, bool oneto1, bool communities, bool groups, bool legacy_groups) { + const DictFieldRoot& data, + bool oneto1, + bool communities, + bool groups, + bool legacy_groups, + bool blinded_1to1) { if (oneto1) if (auto* d = data["1"].dict()) { _it_11 = d->begin(); @@ -390,6 +461,11 @@ ConvoInfoVolatile::iterator::iterator( _it_lgroup = d->begin(); _end_lgroup = d->end(); } + if (blinded_1to1) + if (auto* d = data["b"].dict()) { + _it_b11 = d->begin(); + _end_b11 = d->end(); + } _load_val(); } @@ -400,7 +476,8 @@ class val_loader { std::shared_ptr& val, std::optional& it, std::optional& end, - char prefix) { + char prefix, + std::optional legacy_prefix = std::nullopt) { while (it) { if (*it == *end) { it.reset(); @@ -410,9 +487,13 @@ class val_loader { auto& [k, v] = **it; - if (k.size() == 33 && k[0] == prefix) { + if (k.size() == 33 && (k[0] == prefix || (legacy_prefix && k[0] == *legacy_prefix))) { if (auto* info_dict = std::get_if(&v)) { - val = std::make_shared(ConvoType{oxenc::to_hex(k)}); + if constexpr (std::is_same_v) + val = std::make_shared(ConvoType{ + oxenc::to_hex(k), (legacy_prefix && k[0] == *legacy_prefix)}); + else + val = std::make_shared(ConvoType{oxenc::to_hex(k)}); std::get(*val).load(*info_dict); return true; } @@ -425,7 +506,7 @@ class val_loader { /// Load _val from the current iterator position; if it is invalid, skip to the next key until we /// find one that is valid (or hit the end). We also span across four different iterators: we -/// exhaust, in order: _it_11, _it_group, _it_comm, _it_lgroup. +/// exhaust, in order: _it_11, _it_group, _it_comm, _it_lgroup, _it_b11. /// /// We *always* call this after incrementing the iterator (and after iterator initialization), and /// this is responsible for making sure that _it_11, _it_group, etc. are only set to non-nullopt if @@ -448,15 +529,18 @@ void ConvoInfoVolatile::iterator::_load_val() { if (val_loader::load(_val, _it_lgroup, _end_lgroup, 0x05)) return; + + if (val_loader::load(_val, _it_b11, _end_b11, 0x25, 0x15)) + return; } bool ConvoInfoVolatile::iterator::operator==(const iterator& other) const { return _it_11 == other._it_11 && _it_group == other._it_group && _it_comm == other._it_comm && - _it_lgroup == other._it_lgroup; + _it_lgroup == other._it_lgroup && _it_b11 == other._it_b11; } bool ConvoInfoVolatile::iterator::done() const { - return !_it_11 && !_it_group && (!_it_comm || _it_comm->done()) && !_it_lgroup; + return !_it_11 && !_it_group && (!_it_comm || _it_comm->done()) && !_it_lgroup && !_it_b11; } ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { @@ -466,9 +550,11 @@ ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { ++*_it_group; else if (_it_comm && !_it_comm->done()) _it_comm->advance(); - else { - assert(_it_lgroup); + else if (_it_lgroup) ++*_it_lgroup; + else { + assert(_it_b11); + ++*_it_b11; } _load_val(); return *this; @@ -604,6 +690,40 @@ LIBSESSION_C_API bool convo_info_volatile_get_or_construct_legacy_group( false); } +LIBSESSION_C_API bool convo_info_volatile_get_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + if (auto c = unbox(conf)->get_blinded_1to1( + blinded_session_id, legacy_blinding)) { + c->into(*convo); + return true; + } + return false; + }, + false); +} + +LIBSESSION_C_API bool convo_info_volatile_get_or_construct_blinded_1to1( + config_object* conf, + convo_info_volatile_blinded_1to1* convo, + const char* blinded_session_id, + bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + unbox(conf) + ->get_or_construct_blinded_1to1(blinded_session_id, legacy_blinding) + .into(*convo); + return true; + }, + false); +} + LIBSESSION_C_API bool convo_info_volatile_set_1to1( config_object* conf, const convo_info_volatile_1to1* convo) { return wrap_exceptions( @@ -645,6 +765,17 @@ LIBSESSION_C_API bool convo_info_volatile_set_legacy_group( false); } +LIBSESSION_C_API bool convo_info_volatile_set_blinded_1to1( + config_object* conf, const convo_info_volatile_blinded_1to1* convo) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set(convo::blinded_one_to_one{*convo}); + return true; + }, + false); +} + LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { return wrap_exceptions( conf, [&] { return unbox(conf)->erase_1to1(session_id); }, false); @@ -667,6 +798,16 @@ LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( [&] { return unbox(conf)->erase_legacy_group(group_id); }, false); } +LIBSESSION_C_API bool convo_info_volatile_erase_blinded_1to1( + config_object* conf, const char* blinded_session_id, bool legacy_blinding) { + return wrap_exceptions( + conf, + [&] { + return unbox(conf)->erase_blinded_1to1( + blinded_session_id, legacy_blinding); + }, + false); +} LIBSESSION_C_API size_t convo_info_volatile_size(const config_object* conf) { return unbox(conf)->size(); @@ -683,6 +824,9 @@ LIBSESSION_C_API size_t convo_info_volatile_size_groups(const config_object* con LIBSESSION_C_API size_t convo_info_volatile_size_legacy_groups(const config_object* conf) { return unbox(conf)->size_legacy_groups(); } +LIBSESSION_C_API size_t convo_info_volatile_size_blinded_1to1(const config_object* conf) { + return unbox(conf)->size_blinded_1to1(); +} LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new( const config_object* conf) { @@ -718,6 +862,13 @@ LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_ new ConvoInfoVolatile::iterator{unbox(conf)->begin_legacy_groups()}; return it; } +LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_blinded_1to1( + const config_object* conf) { + auto* it = new convo_info_volatile_iterator{}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(conf)->begin_blinded_1to1()}; + return it; +} LIBSESSION_C_API void convo_info_volatile_iterator_free(convo_info_volatile_iterator* it) { delete static_cast(it->_internals); @@ -764,3 +915,8 @@ LIBSESSION_C_API bool convo_info_volatile_it_is_legacy_group( convo_info_volatile_iterator* it, convo_info_volatile_legacy_group* c) { return convo_info_volatile_it_is_impl(it, c); } + +LIBSESSION_C_API bool convo_info_volatile_it_is_blinded_1to1( + convo_info_volatile_iterator* it, convo_info_volatile_blinded_1to1* c) { + return convo_info_volatile_it_is_impl(it, c); +} diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index ca515e66..86c3b085 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -66,6 +66,7 @@ void Members::set(const member& mem) { info["q"], mem.profile_picture.key); + set_ts(info["t"], mem.profile_updated); set_flag(info["A"], mem.admin); set_positive_int(info["P"], mem.promotion_status); set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status); @@ -84,7 +85,7 @@ void Members::set(const member& mem) { } void member::load(const dict& info_dict) { - name = maybe_string(info_dict, "n").value_or(""); + name = string_or_empty(info_dict, "n"); auto url = maybe_string(info_dict, "p"); auto key = maybe_vector(info_dict, "q"); @@ -95,13 +96,13 @@ void member::load(const dict& info_dict) { profile_picture.clear(); } - admin = maybe_int(info_dict, "A").value_or(0); - invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0); - promotion_status = maybe_int(info_dict, "P").value_or(0); - removed_status = maybe_int(info_dict, "R").value_or(0); - supplement = invite_status > 0 && !(admin || promotion_status > 0) - ? maybe_int(info_dict, "s").value_or(0) - : 0; + profile_updated = ts_or_epoch(info_dict, "t"); + admin = int_or_0(info_dict, "A"); + invite_status = admin ? 0 : int_or_0(info_dict, "I"); + promotion_status = int_or_0(info_dict, "P"); + removed_status = int_or_0(info_dict, "R"); + supplement = + invite_status > 0 && !(admin || promotion_status > 0) ? int_or_0(info_dict, "s") : 0; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -187,6 +188,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { profile_picture.url = m.profile_pic.url; profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32); } + profile_updated = to_sys_seconds(m.profile_updated); admin = m.admin; invite_status = (m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT) @@ -211,6 +213,7 @@ void member::into(config_group_member& m) const { } else { copy_c_str(m.profile_pic.url, ""); } + m.profile_updated = profile_updated.time_since_epoch().count(); m.admin = admin; static_assert(groups::STATUS_SENT == ::STATUS_SENT); static_assert(groups::STATUS_FAILED == ::STATUS_FAILED); diff --git a/src/config/internal.cpp b/src/config/internal.cpp index a81446ef..0b2051dc 100644 --- a/src/config/internal.cpp +++ b/src/config/internal.cpp @@ -81,18 +81,49 @@ std::optional maybe_int(const session::config::dict& d, const char* key return std::nullopt; } +int64_t int_or_0(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return *i; + return 0; +} + +std::optional maybe_ts(const session::config::dict& d, const char* key) { + std::optional result; + if (auto* i = maybe_scalar(d, key)) + result.emplace(std::chrono::seconds{*i}); + return result; +} + +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key) { + if (auto* i = maybe_scalar(d, key)) + return std::chrono::sys_seconds{std::chrono::seconds{*i}}; + return std::chrono::sys_seconds{}; +} + std::optional maybe_string(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string string_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""s; +} + std::optional maybe_sv(const session::config::dict& d, const char* key) { if (auto* s = maybe_scalar(d, key)) return *s; return std::nullopt; } +std::string_view sv_or_empty(const session::config::dict& d, const char* key) { + if (auto* s = maybe_scalar(d, key)) + return *s; + return ""sv; +} + std::optional> maybe_vector( const session::config::dict& d, const char* key) { std::optional> result; diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 74cc31fd..337523c5 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -147,18 +147,38 @@ const config::set* maybe_set(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out an int64_t; nullopt if not there (or not int) std::optional maybe_int(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out an int64_t; returns 0 if the value is not there or not an +// int. Equivalent to `maybe_int(d, key).value_or(0)`. +int64_t int_or_0(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out an int64_t containing unix timestamp seconds, returns it +// wrapped in a std::chrono::sys_seconds. Returns nullopt if not there (or not int). +std::optional maybe_ts(const session::config::dict& d, const char* key); + +// Works like maybe_ts, except that if the value isn't present it returns a default-constructed +// sys_seconds (i.e. unix timestamp 0). Equivalent to `maybe_ts(d, +// key).value_or(std::chrono::sys_seconds{})`. +std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key); + // Digs into a config `dict` to get out a string; nullopt if not there (or not string) std::optional maybe_string(const session::config::dict& d, const char* key); -// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not -// string) -std::optional> maybe_vector( - const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string; ""s if not there (or not string) +std::string string_or_empty(const session::config::dict& d, const char* key); // Digs into a config `dict` to get out a string view; nullopt if not there (or not string). The // string view is only valid as long as the dict stays unchanged. std::optional maybe_sv(const session::config::dict& d, const char* key); +// Digs into a config `dict` to get out a string view; ""sv if not there (or not string). The +// string view is only valid as long as the dict stays unchanged. +std::string_view sv_or_empty(const session::config::dict& d, const char* key); + +// Digs into a config `dict` to get out a std::vector; nullopt if not there (or not +// string) +std::optional> maybe_vector( + const session::config::dict& d, const char* key); + /// Sets a value to 1 if true, removes it if false. void set_flag(ConfigBase::DictFieldProxy&& field, bool val); @@ -172,6 +192,11 @@ void set_nonzero_int(ConfigBase::DictFieldProxy&& field, int64_t val); /// Sets an integer value, if positive; removes it if <= 0. void set_positive_int(ConfigBase::DictFieldProxy&& field, int64_t val); +/// Sets a unix timestamp as an integer, if positive; removes it if <= 0. +inline void set_ts(ConfigBase::DictFieldProxy&& field, std::chrono::sys_seconds val) { + set_positive_int(std::move(field), val.time_since_epoch().count()); +} + /// Sets a pair of values if the given condition is satisfied, clears both values otherwise. template void set_pair_if( diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index a93da2a5..7c8bf724 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -126,18 +126,18 @@ void legacy_group_info::into(ugroups_legacy_group_info& c) && { } void base_group_info::load(const dict& info_dict) { - priority = maybe_int(info_dict, "+").value_or(0); - joined_at = to_epoch_seconds(std::max(0, maybe_int(info_dict, "j").value_or(0))); + priority = int_or_0(info_dict, "+"); + joined_at = to_epoch_seconds(std::max(0, int_or_0(info_dict, "j"))); - int notify = maybe_int(info_dict, "@").value_or(0); + int notify = int_or_0(info_dict, "@"); if (notify >= 0 && notify <= 3) notifications = static_cast(notify); else notifications = notify_mode::defaulted; - mute_until = to_epoch_seconds(maybe_int(info_dict, "!").value_or(0)); + mute_until = to_epoch_seconds(int_or_0(info_dict, "!")); - invited = maybe_int(info_dict, "i").value_or(0); + invited = int_or_0(info_dict, "i"); } void legacy_group_info::load(const dict& info_dict) { @@ -157,10 +157,7 @@ void legacy_group_info::load(const dict& info_dict) { enc_pubkey.clear(); enc_seckey.clear(); } - if (auto secs = maybe_int(info_dict, "E").value_or(0); secs > 0) - disappearing_timer = std::chrono::seconds{secs}; - else - disappearing_timer = 0s; + disappearing_timer = std::max(0s, std::chrono::seconds{int_or_0(info_dict, "E")}); members_.clear(); if (auto* members = maybe_set(info_dict, "m")) @@ -244,7 +241,7 @@ void group_info::load(const dict& info_dict) { if (auto sig = maybe_vector(info_dict, "s"); sig && sig->size() == 100) auth_data = std::move(*sig); - removed_status = maybe_int(info_dict, "r").value_or(0); + removed_status = int_or_0(info_dict, "r"); } void group_info::mark_kicked() { diff --git a/src/util.cpp b/src/util.cpp index 7669d0e1..60409c58 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -87,4 +87,8 @@ std::tuple, std::optional().time_since_epoch())>); + } // namespace session diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 06a55166..e30ac306 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include #include @@ -48,6 +50,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(c.name.empty()); CHECK(c.nickname.empty()); + CHECK(c.profile_updated == std::chrono::sys_seconds{}); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -62,6 +65,7 @@ TEST_CASE("Contacts", "[config][contacts]") { c.set_name("Joe"); c.set_nickname("Joey"); + c.profile_updated = std::chrono::sys_seconds{1s}; c.approved = true; c.approved_me = true; c.created = created_ts * 1'000; @@ -74,6 +78,7 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(contacts.get(definitely_real_id)->name == "Joe"); CHECK(contacts.get(definitely_real_id)->nickname == "Joey"); + CHECK(contacts.get(definitely_real_id)->profile_updated.time_since_epoch() == 1s); CHECK(contacts.get(definitely_real_id)->approved); CHECK(contacts.get(definitely_real_id)->approved_me); CHECK_FALSE(contacts.get(definitely_real_id)->profile_picture); @@ -106,6 +111,7 @@ TEST_CASE("Contacts", "[config][contacts]") { REQUIRE(x); CHECK(x->name == "Joe"); CHECK(x->nickname == "Joey"); + CHECK(x->profile_updated.time_since_epoch() == 1s); CHECK(x->approved); CHECK(x->approved_me); CHECK_FALSE(x->profile_picture); @@ -137,11 +143,13 @@ TEST_CASE("Contacts", "[config][contacts]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts.size() == 2); CHECK_FALSE(contacts.empty()); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); @@ -150,6 +158,8 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0].time_since_epoch() == 1s); + CHECK(profile_updateds[1].time_since_epoch() == 0s); // Conflict! Oh no! @@ -159,6 +169,7 @@ TEST_CASE("Contacts", "[config][contacts]") { // Client 2 adds a new friend: auto third_id = "052222222222222222222222222222222222222222222222222222222222222222"sv; contacts2.set_nickname(third_id, "Nickname 3"); + contacts2.set_profile_updated(third_id, session::to_sys_seconds(2)); contacts2.set_approved(third_id, true); contacts2.set_blocked(third_id, true); @@ -216,15 +227,19 @@ TEST_CASE("Contacts", "[config][contacts]") { session_ids.clear(); nicknames.clear(); + profile_updateds.clear(); for (const auto& cc : contacts) { session_ids.push_back(cc.session_id); nicknames.emplace_back(cc.nickname.empty() ? "(N/A)" : cc.nickname); + profile_updateds.emplace_back(cc.profile_updated); } REQUIRE(session_ids.size() == 2); CHECK(session_ids[0] == another_id); CHECK(session_ids[1] == third_id); CHECK(nicknames[0] == "(N/A)"); CHECK(nicknames[1] == "Nickname 3"); + CHECK(profile_updateds[0].time_since_epoch() == 0s); + CHECK(profile_updateds[1].time_since_epoch() == 2s); CHECK_THROWS( c.set_nickname("12345678901234567890123456789012345678901234567890123456789012345678901" @@ -279,6 +294,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(strlen(c.name) == 0); CHECK(strlen(c.nickname) == 0); + CHECK(c.profile_updated == 0); CHECK_FALSE(c.approved); CHECK_FALSE(c.approved_me); CHECK_FALSE(c.blocked); @@ -287,6 +303,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { strcpy(c.name, "Joe"); strcpy(c.nickname, "Joey"); + c.profile_updated = 1; c.approved = true; c.approved_me = true; c.created = created_ts; @@ -298,6 +315,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); + CHECK(c2.profile_updated == 1); CHECK(c2.approved); CHECK(c2.approved_me); CHECK_FALSE(c2.blocked); @@ -333,6 +351,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get(conf2, &c3, definitely_real_id)); CHECK(c3.name == "Joe"sv); CHECK(c3.nickname == "Joey"sv); + CHECK(c3.profile_updated == 1); CHECK(c3.approved); CHECK(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -343,6 +362,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { REQUIRE(contacts_get_or_construct(conf, &c3, another_id)); CHECK(strlen(c3.name) == 0); CHECK(strlen(c3.nickname) == 0); + CHECK(c3.profile_updated == 0); CHECK_FALSE(c3.approved); CHECK_FALSE(c3.approved_me); CHECK_FALSE(c3.blocked); @@ -372,6 +392,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; + std::vector profile_updateds; CHECK(contacts_size(conf) == 2); contacts_iterator* it = contacts_iterator_new(conf); @@ -379,6 +400,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { session_ids.push_back(ci.session_id); nicknames.emplace_back(strlen(ci.nickname) ? ci.nickname : "(N/A)"); + profile_updateds.emplace_back(ci.profile_updated); } contacts_iterator_free(it); @@ -387,6 +409,8 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(session_ids[1] == another_id); CHECK(nicknames[0] == "Joey"); CHECK(nicknames[1] == "(N/A)"); + CHECK(profile_updateds[0] == 1); + CHECK(profile_updateds[1] == 0); // Changing things while iterating: it = contacts_iterator_new(conf); @@ -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..502ea132 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -35,6 +35,11 @@ TEST_CASE("Conversations", "[config][conversations]") { constexpr auto benders_nightmare_group = "030111101001001000101010011011010010101010111010000110100001210000"sv; + constexpr auto legacy_blinded_id = + "150000000000000000000000000000000000101010111010000110100001210000"sv; + constexpr auto blinded_id = + "255000000000000000000000000000000000101010111010000110100001210000"sv; + CHECK_FALSE(convos.get_1to1(definitely_real_id)); CHECK(convos.empty()); @@ -90,6 +95,27 @@ TEST_CASE("Conversations", "[config][conversations]") { g.unread = true; convos.set(g); + CHECK_FALSE(convos.get_blinded_1to1(legacy_blinded_id, true)); + CHECK_FALSE(convos.get_blinded_1to1(blinded_id, false)); + + auto lb = convos.get_or_construct_blinded_1to1(legacy_blinded_id, true); + CHECK(lb.blinded_session_id == legacy_blinded_id); + CHECK(lb.last_read == 0); + CHECK_FALSE(lb.unread); + + lb.last_read = now_ms; + lb.unread = true; + convos.set(lb); + + auto b = convos.get_or_construct_blinded_1to1(blinded_id, false); + CHECK(b.blinded_session_id == blinded_id); + CHECK(b.last_read == 0); + CHECK_FALSE(b.unread); + + b.last_read = now_ms; + b.unread = true; + convos.set(b); + auto [seqno, to_push, obs] = convos.push(); CHECK(seqno == 1); @@ -127,6 +153,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; @@ -137,6 +177,11 @@ TEST_CASE("Conversations", "[config][conversations]") { c3.last_read = now_ms - 50; convos2.set(c3); + auto c4 = convos2.get_or_construct_blinded_1to1( + "2512345ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false); + c4.unread = true; + convos2.set(c4); + CHECK(convos2.needs_push()); std::tie(seqno, to_push, obs) = convos2.push(); @@ -152,6 +197,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 +209,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 +235,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 +250,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 +379,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 +436,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 +469,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 +487,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 +514,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 +545,9 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.emplace_back(ogi.base_url); } convo_info_volatile_iterator_free(it); - CHECK(seen == std::vector{{ + CHECK(seen == std::vector{ "http://example.org:5678", - }}); + }); seen.clear(); convo_info_volatile_legacy_group cgi; @@ -461,9 +558,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]") { 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);