diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index eeda53e..d8bd516 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1407,37 +1407,121 @@ class ConfigBase : public ConfigSig { } }; +struct IInternalsBase { + virtual ~IInternalsBase() = default; + virtual ConfigBase* getConfigBasePtr() = 0; + virtual const ConfigBase* getConfigBasePtr() const = 0; +}; + // The C++ struct we hold opaquely inside the C internals struct. This is designed so that any // internals has the same layout so that it doesn't matter whether we unbox to an // internals or internals. template < typename ConfigT = ConfigBase, std::enable_if_t, int> = 0> -struct internals final { - std::unique_ptr config; - std::string error; +struct internals final : public IInternalsBase { + private: + std::variant, ConfigBase*> m_config_holder; + + public: + std::string error; + + template + explicit internals(std::in_place_type_t>, Args&&... args) + : m_config_holder(std::make_unique(std::forward(args)...)), error() {} + + explicit internals(ConfigBase* config_ptr_managed_externally) + : m_config_holder(config_ptr_managed_externally), error() { + if (!config_ptr_managed_externally) { + throw std::invalid_argument("Externally managed config pointer cannot be null."); + } + } + + ~internals() override { + // std::variant handles destruction of unique_ptr if it holds one and the raw pointer needs no special cleanup here + } + + internals(const internals&) = delete; + internals& operator=(const internals&) = delete; + internals(internals&&) = delete; + internals& operator=(internals&&) = delete; + + private: + ConfigBase* getConfigBasePtr() override { + return std::visit( + [](auto&& arg) -> ConfigBase* { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + return arg.get(); + } else if constexpr (std::is_same_v) { + return arg; + } + return nullptr; // Should not happen + }, + m_config_holder); + } + + const ConfigBase* getConfigBasePtr() const override { + return std::visit( + [](auto&& arg) -> const ConfigBase* { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + return arg.get(); + } else if constexpr (std::is_same_v) { + return arg; + } + return nullptr; // Should not happen + }, + m_config_holder); + } + + public: /// Dereferencing falls through to the ConfigBase object ConfigT* operator->() { + ConfigBase* base_ptr = getConfigBasePtr(); + if (!base_ptr) { + assert(false && "ConfigBase pointer is null in internals::operator->"); + return nullptr; + } + if constexpr (std::is_same_v) - return config.get(); + return static_cast(base_ptr); else { - auto* c = dynamic_cast(config.get()); + auto* c = dynamic_cast(base_ptr); assert(c); return c; } } const ConfigT* operator->() const { + const ConfigBase* base_ptr = getConfigBasePtr(); + if (!base_ptr) { + assert(false && "ConfigBase pointer is null in internals::operator-> const"); + return nullptr; + } + if constexpr (std::is_same_v) - return config.get(); + return static_cast(base_ptr); else { - auto* c = dynamic_cast(config.get()); + auto* c = dynamic_cast(base_ptr); assert(c); return c; } } - ConfigT& operator*() { return *operator->(); } - const ConfigT& operator*() const { return *operator->(); } + ConfigT& operator*() { + ConfigT* ptr = operator->(); + if (!ptr) { + throw std::runtime_error("Attempted to dereference a null config pointer via internals::operator*"); + } + return *ptr; + } + const ConfigT& operator*() const { + const ConfigT* ptr = operator->(); + if (!ptr) { + throw std::runtime_error("Attempted to dereference a null config pointer via internals::operator* const"); + } + return *ptr; + } }; template , int> = 0> diff --git a/include/session/config/config_manager.h b/include/session/config/config_manager.h new file mode 100644 index 0000000..0b3a409 --- /dev/null +++ b/include/session/config/config_manager.h @@ -0,0 +1,144 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "../config.h" +#include "../export.h" +#include "base.h" +#include "notify.h" +#include "groups/keys.h" + +// Config manager base type: this type holds the internal object and is initialized by the various +// config-dependent settings (e.g. config_manager_init) then passed to the various functions. +typedef struct config_manager { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} config_manager; + +typedef enum CONVERSATION_TYPE { + CONVERSATION_TYPE_ONE_TO_ONE = 0, + CONVERSATION_TYPE_COMMUNITY = 1, + CONVERSATION_TYPE_GROUP = 2, + CONVERSATION_TYPE_BLINDED_ONE_TO_ONE = 3, + CONVERSATION_TYPE_LEGACY_GROUP = 100, +} CONVERSATION_TYPE; + +typedef struct config_convo { + char id[409]; // needs to be large enough to fit a full community url (base_url: 267, '/': 1, room: 64, '?public_key=': 12, pubkey_hex: 64, null terminator: 1 ) with null terminator. + CONVERSATION_TYPE type; + char name[101]; + + // display_pic pic; // TODO: Need to add a C API version + + int64_t first_active; // ms since unix epoch + int64_t last_active; // ms since unix epoch + int priority; + CONVO_NOTIFY_MODE notifications; + int64_t mute_until; + + union { + struct { + bool is_message_request; + bool is_blocked; + } one_to_one; + + struct { + bool is_message_request; + } group; + + struct { + 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) + bool legacy_blinding; + } blinded_one_to_one; + } specific_data; +} config_convo; + +/// API: config_manager/config_manager_init +/// +/// Constructs a config manager object and sets a pointer to it in `manager`. +/// +/// When done with the object the `config_manager` must be destroyed by passing the pointer to +/// config_manager_free(). +/// +/// Declaration: +/// ```cpp +/// BOOL config_manager_init( +/// [out] config_manager** manager, +/// [in] const unsigned char* ed25519_secretkey, +/// [out] char* error +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [out] Pointer to the config object +/// - `ed25519_secretkey` -- [in] must be the 32-byte secret key seed value. (You can also pass the +/// pointer to the beginning of the 64-byte value libsodium calls the "secret key" as the first 32 +/// bytes of that are the seed). This field cannot be null. +/// - `error` -- [out] the pointer to a buffer in which we will write an error string if an error +/// occurs; error messages are discarded if this is given as NULL. If non-NULL this must be a +/// buffer of at least 256 bytes. +/// +/// Outputs: +/// - `bool` -- Returns true on success; returns false and writes the exception message as a +/// C-string into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool config_manager_init( + config_manager** manager, const unsigned char* ed25519_secretkey_bytes, char* error); + +/// API: base/config_manager_free +/// +/// Frees a config manager object created with config_manager_init. This will also free any configs +/// the manager is storing so any previously returned `config_object` or `config_group_keys` will be +/// invalid once this is called. +/// +/// Declaration: +/// ```cpp +/// VOID config_free( +/// [in, out] config_manager* manager +/// ); +/// ``` +/// +/// Inputs: +/// - `manager` -- [in] Pointer to config_manager object +LIBSESSION_EXPORT void config_manager_free(config_manager* manager); + +LIBSESSION_EXPORT bool config_manager_load( + config_manager* manager, + int16_t namespace_, + const char* group_ed25519_pubkey, + const unsigned char* dump, + size_t dumplen, + char* error); + +LIBSESSION_EXPORT bool config_manager_get_config( + config_manager* manager, + const uint16_t namespace_, + const char* pubkey_hex, + config_object** config, + char* error); + +LIBSESSION_EXPORT bool config_manager_get_keys_config( + config_manager* manager, + const char* pubkey_hex, + config_group_keys** config, + char* error); + +LIBSESSION_EXPORT void config_manager_free_config(config_object* config); +LIBSESSION_EXPORT void config_manager_free_keys_config(config_group_keys* config); + +LIBSESSION_EXPORT bool config_manager_get_conversations( + const config_manager* manager, + config_convo** conversations, + size_t* conversations_len, + char* error); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/config/config_manager.hpp b/include/session/config/config_manager.hpp new file mode 100644 index 0000000..b21dfdc --- /dev/null +++ b/include/session/config/config_manager.hpp @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; +using namespace session; + +namespace session::config { + +enum class conversation_type { + one_to_one = 0, + community = 1, + group = 2, + blinded_one_to_one = 3, + legacy_group = 100, +}; + +struct display_pic_item { + profile_pic pic; + std::string fallback_name; + + display_pic_item( + std::string_view id, + std::optional name, + profile_pic pic): pic{pic}, fallback_name{name.value_or(id)} {} +}; + +struct multi_display_pic { + display_pic_item first_pic; + std::optional additional_pic; +}; + +using display_pic = std::variant; + +/// Common base type with fields shared by all the conversations +struct base_conversation { + std::string id; + conversation_type type; + std::string name; + display_pic pic; + + int64_t first_active; + int64_t last_active; + int priority; + notify_mode notifications; + int64_t mute_until; + + base_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0) : id{id}, type{type}, name{name}, pic{pic}, first_active{first_active}, last_active{last_active}, priority{priority}, notifications{notifications}, mute_until{mute_until} {} +}; + +struct one_to_one_conversation : base_conversation { + bool is_message_request; + bool is_blocked; + + one_to_one_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0, + bool is_message_request = false, + bool is_blocked = false) : is_message_request{is_message_request}, is_blocked{is_blocked}, base_conversation{id, type, name, pic, first_active, last_active, priority, notifications, mute_until} {} +}; + +struct community_conversation : base_conversation { + community_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0) : base_conversation{id, type, name, pic, first_active, last_active, priority, notifications, mute_until} {} +}; + +struct group_conversation : base_conversation { + bool is_message_request; + + group_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0, + bool is_message_request = false) : is_message_request{is_message_request}, base_conversation{id, type, name, pic, first_active, last_active, priority, notifications, mute_until} {} +}; + +struct legacy_group_conversation : base_conversation { + legacy_group_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0) : base_conversation{id, type, name, pic, first_active, last_active, priority, notifications, mute_until} {} +}; + +struct blinded_one_to_one_conversation : base_conversation { + std::string community_base_url; + std::string community_public_key; + bool legacy_blinding; + + blinded_one_to_one_conversation( + std::string id, + conversation_type type, + std::string name, + display_pic pic, + std::string community_base_url, + std::string community_public_key, + bool legacy_blinding, + int64_t first_active = 0, + int64_t last_active = 0, + int priority = 0, + notify_mode notifications = notify_mode::defaulted, + int64_t mute_until = 0, + bool is_message_request = false, + bool is_blocked = false) : community_base_url{community_base_url}, community_public_key{community_public_key}, legacy_blinding{legacy_blinding}, base_conversation{id, type, name, pic, first_active, last_active, priority, notifications, mute_until} {} +}; + +using conversation = std::variant; + +class GroupConfigs { + public: + GroupConfigs( + std::span group_ed25519_pubkey, + std::optional> group_ed25519_secret_key, + std::span user_ed25519_secretkey); + + GroupConfigs(GroupConfigs&&) = delete; + GroupConfigs(const GroupConfigs&) = delete; + GroupConfigs& operator=(GroupConfigs&&) = delete; + GroupConfigs& operator=(const GroupConfigs&) = delete; + + std::unique_ptr info; + std::unique_ptr members; + std::unique_ptr keys; +}; + +class ConfigManager { + private: + Ed25519PubKey _user_pk; + Ed25519Secret _user_sk; + + std::unique_ptr _config_contacts; + std::unique_ptr _config_convo_info_volatile; + std::unique_ptr _config_user_groups; + std::unique_ptr _config_user_profile; + std::unique_ptr _config_local; + std::map> _config_groups; + + public: + // Constructs a ConfigManager with a secretkey that will be used for signing. + ConfigManager(std::span ed25519_secretkey); + + // Constructs a new ConfigManager, this will generate a random secretkey and should only be used + // for creating a new account. + ConfigManager() : ConfigManager(to_span(ed25519::ed25519_key_pair().second)) {}; + + // Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to + // be managed. + ConfigManager(ConfigManager&&) = delete; + ConfigManager(const ConfigManager&) = delete; + ConfigManager& operator=(ConfigManager&&) = delete; + ConfigManager& operator=(const ConfigManager&) = delete; + + /// API: ConfigManager/ConfigManager::load + /// + /// Loads a dump into the ConfigManager. Calling this will replace the current config instance + /// with with a new instance initialised with the provided dump. The configs must be loaded + /// according to the order 'namespace_load_order' in 'namespaces.hpp' or an exception will be + /// thrown. + /// + /// Inputs: + /// - `namespace` -- the namespace where config messages for this dump are stored. + /// - `group_ed25519_pubkey` -- optional pubkey the dump is associated to (in hex, with prefix - + /// 66 bytes). Required for group dumps. + /// - `dump` -- binary state data that was previously dumped by calling `dump()`. + /// + /// Outputs: None + void load( + config::Namespace namespace_, + std::optional group_ed25519_pubkey_hex, + std::optional> dump); + + /// API: ConfigManager/ConfigManager::config + /// + /// Retrieves a user config from the config manager + /// + /// Outputs: + /// - `ConfigType&` -- The instance of the user config requested + template + ConfigType& config(); + + /// API: ConfigManager/ConfigManager::config + /// + /// Retrieves a group config for the given public key + /// + /// Inputs: + /// - `pubkey_hex` -- pubkey for the group to retrieve the config for (in hex, with prefix - 66 + /// bytes). + /// + /// Outputs: + /// - `std::optional` -- The instance of the group config requested or + /// `std::nullopt` if not present + template + ConfigType& config(std::string pubkey_hex); + + void prune_volatile_orphans(); + std::vector conversations() const; +}; + +} // namespace session::config diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index e275215..23073ed 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -36,6 +36,38 @@ typedef struct contacts_contact { } contacts_contact; +typedef struct contacts_blinded_contact { + char session_id[67]; // in hex; 66 hex chars + null terminator. + char base_url[268]; // null-terminated (max length 267), normalized (i.e. always lower-case, + // only has port if non-default, has trailing / removed) + unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls) + + char name[101]; // This will be a 0-length strings when unset + user_profile_pic profile_pic; + + bool legacy_blinding; + int64_t created; // unix timestamp (seconds) + +} contacts_blinded_contact; + +typedef struct contacts_deleted_contact { + char session_id[67]; // in hex; 66 hex chars + null terminator. + + int64_t deleted; // unix timestamp (seconds) + +} contacts_deleted_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 +240,107 @@ LIBSESSION_EXPORT bool contacts_erase(config_object* conf, const char* session_i /// - `size_t` -- number of contacts LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); +/// API: contacts/contacts_blinded_contacts +/// +/// Retrieves a list of blinded contact records. +/// +/// Declaration: +/// ```cpp +/// contacts_blinded_contact_list* contacts_blinded_contacts( +/// [in] config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in, out] Pointer to config_object object +/// +/// Outputs: +/// - `contacts_blinded_contact_list*` -- pointer to the list of blinded contact structs; the +/// pointer belongs to the caller and must be freed when done with it. +LIBSESSION_EXPORT contacts_blinded_contact_list* contacts_blinded_contacts( + const config_object* conf); + +/// API: contacts/contacts_get_blinded_contact +/// +/// Fills `blinded_contact` with the blinded contact info given a blinded session ID (specified as a +/// null-terminated hex string), if the blinded contact exists, and returns true. If the contact +/// does not exist then `blinded_contact` is left unchanged and false is returned. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_get_blinded_contact( +/// [in] config_object* conf, +/// [in] const char* blinded_session_id, +/// [in] bool legacy_blinding, +/// [out] contacts_blinded_contact* blinded_contact +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `blinded_session_id` -- [in] null terminated hex string +/// - `legacy_blinding` -- [in] null terminated hex string +/// - `blinded_contact` -- [out] the blinded contact info data +/// +/// Output: +/// - `bool` -- Returns true if blinded contact exists +LIBSESSION_EXPORT bool contacts_get_blinded_contact( + config_object* conf, + const char* blinded_session_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) LIBSESSION_WARN_UNUSED; + +/// API: contacts/contacts_set_blinded_contact +/// +/// Adds or updates a blinded contact from the given contact info struct. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_set_blinded_contact( +/// [in] config_object* conf, +/// [in] contacts_blinded_contact* bc +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `blinded_contact` -- [in] the blinded contact info data +/// +/// Output: +/// - `bool` -- Returns true if the call succeeds, false if an error occurs. +LIBSESSION_EXPORT bool contacts_set_blinded_contact( + config_object* conf, const contacts_blinded_contact* bc); + +/// API: contacts/contacts_erase_blinded_contact +/// +/// Erases a blinded contact from the blinded contact list. blinded_id is in hex. Returns true if +/// the blinded contact was found and removed, false if the blinded contact was not present. +/// +/// Declaration: +/// ```cpp +/// BOOL contacts_erase_blinded_contact( +/// [in, out] config_object* conf, +/// [in] const char* base_url, +/// [in] const char* blinded_id, +/// [in] bool legacy_blinding +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in, out] Pointer to the config object +/// - `base_url` -- [in] Text containing null terminated base url for the community this blinded +/// contact originated from +/// - `blinded_id` -- [in] Text containing null terminated hex string +/// - `legacy_blinding` -- [in] Flag indicating whether this blinded contact used legacy blinding +/// +/// Outputs: +/// - `bool` -- True if erasing was successful +LIBSESSION_EXPORT bool contacts_erase_blinded_contact( + config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding); + +// TODO: Need to flag that the caller needs to free this once they are done with it +LIBSESSION_EXPORT bool last_deleted_contacts(const config_object* conf, const contacts_deleted_contact** contacts, size_t* contacts_len); + typedef struct contacts_iterator { void* _internals; } contacts_iterator; diff --git a/include/session/config/contacts.hpp b/include/session/config/contacts.hpp index 757e6cd..eef2a7e 100644 --- a/include/session/config/contacts.hpp +++ b/include/session/config/contacts.hpp @@ -7,12 +7,15 @@ #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; +extern "C" struct contacts_deleted_contact; using namespace std::literals; @@ -44,8 +47,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. +/// +/// b - dict of blinded contacts. This is a nested dict where the outkey keys are the BASE_URL of +/// the community the blinded contact originated from and the outer value is a dict containing: +/// `#` - the 32-byte server pubkey +/// `R` - dict of blinded contacts from the server; each key is the blinded session pubkey +/// without the prefix ("R" to match user_groups equivalent "R"oom field, and to make use of +/// existing community iterators, binary, 32 bytes), value is a dict containing keys: +/// +/// 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. +/// +/// d - dict of deleted contacts; within this dict each key is the session pubkey (binary, 33 bytes) and value is a unix timestamp (seconds) indicating when that contact was last deleted. -/// Struct containing contact info. struct contact_info { static constexpr size_t MAX_NAME_LENGTH = 100; @@ -53,6 +73,7 @@ struct contact_info { std::string name; std::string nickname; profile_pic profile_picture; + bool approved = false; bool approved_me = false; bool blocked = false; @@ -72,7 +93,7 @@ struct contact_info { // Internal ctor/method for C API implementations: contact_info(const struct contacts_contact& c); // From c struct - // + /// API: contacts/contact_info::into /// /// converts the contact info into a c struct @@ -97,6 +118,47 @@ struct contact_info { void load(const dict& info_dict); }; +struct blinded_contact_info : community { + using community::community; + + const std::string session_id() const; // in hex + std::string name; + profile_pic profile_picture; + bool legacy_blinding; + int64_t created = 0; // Unix timestamp (seconds) when this contact was added + + explicit blinded_contact_info( + std::string_view base_url, + std::string_view blinded_id, + std::span pubkey, + bool legacy_blinding); + + // Internal ctor/method for C API implementations: + blinded_contact_info(const struct contacts_blinded_contact& c); // From c struct + + /// API: contacts/blinded_contact_info::into + /// + /// converts the contact info into a c struct + /// + /// Inputs: + /// - `c` -- Return Parameter that will be filled with data in blinded_contact_info + void into(contacts_blinded_contact& c) const; + + /// API: contacts/contact_info::set_name + /// + /// Sets a name; this is exactly the same as assigning to .name directly, + /// except that we throw an exception if the given name is longer than MAX_NAME_LENGTH. + /// + /// Inputs: + /// - `name` -- Name to assign to the contact + void set_name(std::string name); + + private: + friend class Contacts; + friend struct session::config::comm_iterator_helper; + void load(const dict& info_dict); +}; + class Contacts : public ConfigBase { public: @@ -337,8 +399,101 @@ class Contacts : public ConfigBase { /// - `bool` - Returns true if the contact list is empty bool empty() const { return size() == 0; } + protected: + // Drills into the nested dicts to access open group details + DictFieldProxy blinded_contact_field( + const blinded_contact_info& bc, std::span* get_pubkey = nullptr) const; + + public: + std::vector blinded_contacts() const; + + + + /// API: contacts/Contacts::get_blinded + /// + /// Looks up and returns a blinded contact by blinded session ID (hex). Returns nullopt if the blinded session ID was + /// not found, otherwise returns a filled out `blinded_contact_info`. + /// + /// Inputs: + /// - `pubkey_hex` -- hex string of the session id + /// - `legacy_blinding` -- flag indicating whether the pubkey is using legacy blinding + /// + /// Outputs: + /// - `std::optional` - Returns nullopt if blinded session ID was not found, otherwise a + /// filled out blinded_contact_info + std::optional get_blinded(std::string_view pubkey_hex, bool legacy_blinding) const; + + bool set_blinded_contact(const blinded_contact_info& bc); + bool erase_blinded_contact(std::string_view base_url_, std::string_view blinded_id, bool legacy_blinding); + + std::vector> last_deleted_contacts() const; + bool accepts_protobuf() const override { return true; } + protected: + // Drills into the nested dicts to access open group details + DictFieldProxy blinded_contact_field( + const blinded_contact_info& bc, + std::span* get_pubkey = nullptr) const; + + public: + /// API: contacts/Contacts::blinded_contacts + /// + /// Retrieves a list of all known blinded contacts. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::vector` - Returns a list of blinded_contact_info + std::vector blinded_contacts() const; + + /// API: contacts/Contacts::get_blinded + /// + /// Looks up and returns a blinded contact by blinded session ID (hex). Returns nullopt if the + /// blinded session ID was not found, otherwise returns a filled out `blinded_contact_info`. + /// + /// Inputs: + /// - `pubkey_hex` -- hex string of the session id + /// - `legacy_blinding` -- flag indicating whether the pubkey is using legacy blinding + /// + /// Outputs: + /// - `std::optional` - Returns nullopt if blinded session ID was not + /// found, otherwise a filled out blinded_contact_info + std::optional get_blinded( + std::string_view pubkey_hex, bool legacy_blinding) const; + + /// API: contacts/contacts::set_blinded_contact + /// + /// Sets or updates multiple blinded contact info values at once with the given info. The usual + /// use is to access the current info, change anything desired, then pass it back into + /// set_blinded_contact, e.g.: + /// + ///```cpp + /// auto c = contacts.get_blinded(pubkey, legacy_blinding); + /// c.name = "Session User 42"; + /// contacts.set_blinded_contact(c); + ///``` + /// + /// Inputs: + /// - `bc` -- set_blinded_contact value to set + bool set_blinded_contact(const blinded_contact_info& bc); + + /// API: contacts/contacts::erase_blinded_contact + /// + /// Removes a blinded contact, if present. Returns true if it was found and removed, false + /// otherwise. Note that this removes all fields related to a blinded contact, even fields we do + /// not know about. + /// + /// Inputs: + /// - `base_url` -- the base url for the community this blinded contact originated from + /// - `blinded_id` -- hex string of the blinded id + /// - `legacy_blinding` -- flag indicating whether `blinded_id` is using legacy blinding + /// + /// Outputs: + /// - `bool` - Returns true if contact was found and removed, false otherwise + bool erase_blinded_contact( + std::string_view base_url, std::string_view blinded_id, bool legacy_blinding); + struct iterator; /// API: contacts/contacts::begin /// diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 952b6ff..0550679 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -10,8 +10,9 @@ extern "C" { typedef struct convo_info_volatile_1to1 { char session_id[67]; // in hex; 66 hex chars + null terminator. - int64_t last_read; // milliseconds since unix epoch - bool unread; // true if the conversation is explicitly marked unread + int64_t last_read; // milliseconds since unix epoch + int64_t last_active; // ms since unix epoch + bool unread; // true if the conversation is explicitly marked unread } convo_info_volatile_1to1; typedef struct convo_info_volatile_community { @@ -20,24 +21,35 @@ typedef struct convo_info_volatile_community { char room[65]; // null-terminated (max length 64), normalized (always lower-case) unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls) - int64_t last_read; // ms since unix epoch - bool unread; // true if marked unread + int64_t last_read; // ms since unix epoch + int64_t last_active; // ms since unix epoch + bool unread; // true if marked unread } convo_info_volatile_community; typedef struct convo_info_volatile_group { - char group_id[67]; // in hex; 66 hex chars + null terminator. Begins with "03". - int64_t last_read; // ms since unix epoch - bool unread; // true if marked unread + char group_id[67]; // in hex; 66 hex chars + null terminator. Begins with "03". + int64_t last_read; // ms since unix epoch + int64_t last_active; // ms since unix epoch + bool unread; // true if marked unread } convo_info_volatile_group; typedef struct convo_info_volatile_legacy_group { - char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID, - // though isn't really one. - - int64_t last_read; // ms since unix epoch - bool unread; // true if marked unread + char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID, + // though isn't really one. + int64_t last_read; // ms since unix epoch + int64_t last_active; // ms since unix epoch + 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 + int64_t last_active; // 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 +357,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 +511,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 +623,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 +738,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 +768,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 +779,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 +896,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 +1055,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 3871a69..91f63ae 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 { @@ -32,6 +33,9 @@ class val_loader; /// Values are dicts with keys: /// r - the unix timestamp (in integer milliseconds) of the last-read message. Always /// included, but will be 0 if no messages are read. +/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always +/// included, but will be 0 for empty conversations. u - will be present and set to 1 if this +/// conversation is specifically marked unread. /// u - will be present and set to 1 if this conversation is specifically marked unread. /// /// o - community conversations. This is a nested dict where the outer keys are the BASE_URL of the @@ -41,12 +45,18 @@ class val_loader; /// containing keys: /// r - the unix timestamp (in integer milliseconds) of the last-read message. Always /// included, but will be 0 if no messages are read. +/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always +/// included, but will be 0 for empty conversations. u - will be present and set to 1 if this +/// conversation is specifically marked unread. /// u - will be present and set to 1 if this conversation is specifically marked unread. /// /// g - group conversations (aka new, non-legacy closed groups). The key is the group identifier /// (beginning with 03). Values are dicts with keys: /// r - the unix timestamp (in integer milliseconds) of the last-read message. Always /// included, but will be 0 if no messages are read. +/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always +/// included, but will be 0 for empty conversations. u - will be present and set to 1 if this +/// conversation is specifically marked unread. /// u - will be present and set to 1 if this conversation is specifically marked unread. /// /// C - legacy group conversations (aka closed groups). The key is the group identifier (which @@ -54,12 +64,26 @@ class val_loader; /// 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. +/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always +/// included, but will be 0 for empty conversations. u - will be present and set to 1 if this +/// conversation is specifically marked unread. /// 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. +/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always +/// included, but will be 0 if the user hasn't properly interacted with the covnersation. u - +/// will be present and set to 1 if this conversation is specifically marked unread. y - flag +/// indicating whether the blinded message request is using legac"y" blinding. namespace convo { struct base { int64_t last_read = 0; + int64_t last_active = 0; bool unread = false; protected: @@ -149,7 +173,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 { @@ -195,45 +246,6 @@ class ConvoInfoVolatile : public ConfigBase { /// - `const char*` - Will return "ConvoInfoVolatile" const char* encryption_domain() const override { return "ConvoInfoVolatile"; } - /// Our pruning ages. We ignore added conversations that are more than PRUNE_LOW before now, - /// and we actively remove (when doing a new push) any conversations that are more than - /// PRUNE_HIGH before now. Clients can mostly ignore these and just add all conversations; the - /// class just transparently ignores (or removes) pruned values. - static constexpr auto PRUNE_LOW = 30 * 24h; - static constexpr auto PRUNE_HIGH = 45 * 24h; - - /// API: convo_info_volatile/ConvoInfoVolatile::prune_stale - /// - /// Prunes any "stale" conversations: that is, ones with a last read more than `prune` ago that - /// are not specifically "marked as unread" by the client. - /// - /// This method is called automatically by `push()` and does not typically need to be invoked - /// directly. - /// - /// Inputs: - /// - `prune` the "too old" time; any conversations with a last_read time more than this - /// duration ago will be removed (unless they have the explicit `unread` flag set). If - /// omitted, defaults to the PRUNE_HIGH constant (45 days). - /// - /// Outputs: - /// - returns nothing. - void prune_stale(std::chrono::milliseconds prune = PRUNE_HIGH); - - /// API: convo_info_volatile/ConvoInfoVolatile::push - /// - /// Overrides push() to prune stale last-read values before we do the push. - /// - /// Inputs: None - /// - /// Outputs: - /// - `std::tuple, std::vector>` - Returns a - /// tuple containing - /// - `seqno_t` -- sequence number - /// - `std::vector>` -- data message(s) to push to the server - /// - `std::vector` -- list of known message hashes - std::tuple>, std::vector> push() - override; - /// API: convo_info_volatile/ConvoInfoVolatile::get_1to1 /// /// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was @@ -298,6 +310,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 +413,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 +446,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 +456,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 +515,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 +537,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 +550,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 +567,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 +582,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 +612,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 +635,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 +663,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 +680,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 +689,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 +718,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/user_profile.h b/include/session/config/user_profile.h index 87d2c0c..8faa9e1 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -128,6 +128,44 @@ LIBSESSION_EXPORT user_profile_pic user_profile_get_pic(const config_object* con /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_EXPORT int user_profile_set_pic(config_object* conf, user_profile_pic pic); +/// API: user_profile/user_profile_get_nts_last_active +/// +/// Gets the Note-to-self conversation last_active timestamp in integer milliseconds. +/// +/// Declaration: +/// ```cpp +/// INT user_profile_get_nts_last_active( +/// [in] const config_object* conf +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// +/// Outputs: +/// - `int64_t` -- Returns the timestamp, in integer milliseconds, when the Note-to-self was last active +LIBSESSION_EXPORT int64_t user_profile_get_nts_last_active(const config_object* conf); + +/// API: user_profile/user_profile_set_nts_last_active +/// +/// Sets the Note-to-self conversation last_active value. +/// +/// Declaration: +/// ```cpp +/// VOID user_profile_set_nts_last_active( +/// [in] config_object* conf, +/// [in] int64_t last_active +/// ); +/// ``` +/// +/// Inputs: +/// - `conf` -- [in] Pointer to the config object +/// - `last_active` -- [in] Timestamp, in integer milliseconds, that the conversation was last active +/// +/// Outputs: +/// - `void` -- Returns Nothing +LIBSESSION_EXPORT void user_profile_set_nts_last_active(config_object* conf, int64_t last_active); + /// API: user_profile/user_profile_get_nts_priority /// /// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 6b9dfa8..cda5ca5 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -18,6 +18,7 @@ using namespace std::literals; /// n - user profile name /// p - user profile url /// q - user profile decryption key (binary) +/// l - the timestamp (in ms since epoch) that the "Note to Self" conversation was last active /// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the /// conversation list). Omitted when 0. -1 means hidden. /// e - the expiry timer (in seconds) for the "Note to Self" pseudo-conversation. Omitted when 0. @@ -129,6 +130,24 @@ class UserProfile : public ConfigBase { void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); + /// API: user_profile/UserProfile::get_nts_last_active + /// + /// Gets the Note-to-self conversation last_active timestamp in integer milliseconds. + /// + /// Inputs: None + /// + /// Outputs: + /// - `int64_t` - Timestamp, in integer milliseconds, when the Note-to-self was last active + int64_t get_nts_last_active() const; + + /// API: user_profile/UserProfile::set_nts_last_active + /// + /// Sets the Note-to-self conversation last_active value. + /// + /// Inputs: + /// - `last_active` -- Timestamp, in integer milliseconds, that the conversation was last active + void set_nts_last_active(int64_t last_active); + /// API: user_profile/UserProfile::get_nts_priority /// /// Gets the Note-to-self conversation priority. Negative means hidden; 0 means unpinned; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e4ad56..384e89b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -60,6 +60,7 @@ add_libsession_util_library(config config/base.cpp config/community.cpp config/contacts.cpp + config/config_manager.cpp config/convo_info_volatile.cpp config/encrypt.cpp config/error.c diff --git a/src/config/config_manager.cpp b/src/config/config_manager.cpp new file mode 100644 index 0000000..c81943b --- /dev/null +++ b/src/config/config_manager.cpp @@ -0,0 +1,688 @@ +#include "session/config/config_manager.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "internal.hpp" +#include "session/config/base.hpp" +#include "session/config/config_manager.h" + +using namespace std::literals; +using namespace oxen::log::literals; + +namespace session::config { + +GroupConfigs::GroupConfigs( + std::span group_ed25519_pubkey, + std::optional> group_ed25519_secret_key, + std::span user_ed25519_secretkey) { + info = std::make_unique( + group_ed25519_pubkey, group_ed25519_secret_key, std::nullopt); + members = std::make_unique( + group_ed25519_pubkey, group_ed25519_secret_key, std::nullopt); + keys = std::make_unique( + user_ed25519_secretkey, + group_ed25519_pubkey, + group_ed25519_secret_key, + std::nullopt, + *info, + *members); +} + +ConfigManager::ConfigManager(std::span ed25519_secretkey) { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + if (ed25519_secretkey.size() != 64) + throw std::invalid_argument{"Invalid ed25519_secretkey: expected 64 bytes"}; + + // Setup the keys + std::array user_x_pk; + _user_sk.reset(64); + std::memcpy(_user_sk.data(), ed25519_secretkey.data(), ed25519_secretkey.size()); + crypto_sign_ed25519_sk_to_pk(_user_pk.data(), _user_sk.data()); + + if (0 != crypto_sign_ed25519_pk_to_curve25519(user_x_pk.data(), _user_pk.data())) + throw std::runtime_error{"Ed25519 pubkey to x25519 pubkey conversion failed"}; + + // Initialise empty config states for any missing required config types + if (!_config_contacts) + _config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); + + if (!_config_convo_info_volatile) + _config_convo_info_volatile = + std::make_unique(ed25519_secretkey, std::nullopt); + + if (!_config_user_groups) + _config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); + + if (!_config_user_profile) + _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); +} + +void ConfigManager::load( + config::Namespace namespace_, + std::optional group_ed25519_pubkey_hex, + std::optional> dump) { + switch (namespace_) { + case Namespace::Contacts: + _config_contacts = std::make_unique(to_span(_user_sk), dump); + return; + + case Namespace::UserGroups: + _config_user_groups = std::make_unique(to_span(_user_sk), dump); + return; + + case Namespace::UserProfile: + _config_user_profile = std::make_unique(to_span(_user_sk), dump); + return; + + case Namespace::ConvoInfoVolatile: + _config_convo_info_volatile = + std::make_unique(to_span(_user_sk), dump); + prune_volatile_orphans(); + return; + + case Namespace::Local: + _config_local = std::make_unique(to_span(_user_sk), dump); + return; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!group_ed25519_pubkey_hex) + throw std::invalid_argument{ + "load: Invalid pubkey_hex - required for group config namespaces"}; + if (group_ed25519_pubkey_hex->size() != 66) + throw std::invalid_argument{"load: Invalid pubkey_hex - expected 66 bytes"}; + + // Retrieve any keys for the group + std::string pubkey_hex = *group_ed25519_pubkey_hex; + check_session_id(pubkey_hex, "03"); + auto user_group_info = _config_user_groups->get_group(pubkey_hex); + + if (!user_group_info) + throw std::runtime_error{ + "Unable to retrieve group {} from user_groups config"_format(pubkey_hex)}; + + std::span user_ed25519_secretkey = {_user_sk.data(), 64}; + std::span group_pubkey = to_span(oxenc::from_hex(pubkey_hex)); + std::optional> opt_dump = dump; + std::optional> group_ed25519_secretkey; + + if (!user_group_info.value().secretkey.empty()) + group_ed25519_secretkey = {user_group_info.value().secretkey.data(), 64}; + + // Create a fresh `GroupConfigs` state + if (auto [it, b] = _config_groups.try_emplace(pubkey_hex, nullptr); b) { + if (namespace_ == Namespace::GroupKeys) + throw std::runtime_error{ + "Attempted to load groups_keys config before groups_info or groups_members " + "configs"}; + + _config_groups[pubkey_hex] = std::make_unique( + group_pubkey, group_ed25519_secretkey, user_ed25519_secretkey); + } + + // Reload the specified namespace with the dump + if (namespace_ == Namespace::GroupInfo) + _config_groups.at(pubkey_hex)->info = + std::make_unique(group_pubkey, group_ed25519_secretkey, dump); + else if (namespace_ == Namespace::GroupMembers) + _config_groups.at(pubkey_hex)->members = + std::make_unique(group_pubkey, group_ed25519_secretkey, dump); + else if (namespace_ == Namespace::GroupKeys) { + auto info = _config_groups.at(pubkey_hex)->info.get(); + auto members = _config_groups.at(pubkey_hex)->members.get(); + auto keys = std::make_unique( + user_ed25519_secretkey, + group_pubkey, + group_ed25519_secretkey, + dump, + *info, + *members); + _config_groups.at(pubkey_hex)->keys = std::move(keys); + } else + throw std::runtime_error{"Attempted to load unknown namespace"}; +} + +template Contacts& ConfigManager::config(); +template ConvoInfoVolatile& ConfigManager::config(); +template UserGroups& ConfigManager::config(); +template UserProfile& ConfigManager::config(); +template Local& ConfigManager::config(); + +template +ConfigType& ConfigManager::config() { + if constexpr (std::is_same_v) { + if (!_config_contacts) + throw std::runtime_error("Contacts config is not initialized."); + return *_config_contacts; + } + + if constexpr (std::is_same_v) { + if (!_config_convo_info_volatile) + throw std::runtime_error("ConvoInfoVolatile config is not initialized."); + return *_config_convo_info_volatile; + } + + if constexpr (std::is_same_v) { + if (!_config_user_groups) + throw std::runtime_error("UserGroups config is not initialized."); + return *_config_user_groups; + } + + if constexpr (std::is_same_v) { + if (!_config_user_profile) + throw std::runtime_error("UserProfile config is not initialized."); + return *_config_user_profile; + } + + if constexpr (std::is_same_v) { + if (!_config_local) + throw std::runtime_error("Local config is not initialized."); + return *_config_local; + } + + throw std::runtime_error{"Unsupported user config type requested."}; +} + +template groups::Info& ConfigManager::config(std::string pubkey_hex); +template groups::Members& ConfigManager::config(std::string pubkey_hex); +template groups::Keys& ConfigManager::config(std::string pubkey_hex); + +template +ConfigType& ConfigManager::config(std::string pubkey_hex) { + if (pubkey_hex.size() != 66) + throw std::invalid_argument("pubkey_hex_ must be 66 characters"); + + auto it = _config_groups.find(pubkey_hex); + + if (it == _config_groups.end() || !it->second) + throw std::out_of_range("GroupConfigs not found for key: {}"_format(pubkey_hex)); + + GroupConfigs& group_cfg = *(it->second); + + if constexpr (std::is_same_v) { + if (!group_cfg.info) + throw std::runtime_error( + "groups::Info not initialized for group: {}"_format(pubkey_hex)); + return *group_cfg.info; + } + + if constexpr (std::is_same_v) { + if (!group_cfg.members) + throw std::runtime_error( + "groups::Members not initialized for group: {}"_format(pubkey_hex)); + return *group_cfg.members; + } + + if constexpr (std::is_same_v) { + if (!group_cfg.keys) + throw std::runtime_error( + "groups::Keys not initialized for group: {}"_format(pubkey_hex)); + return *group_cfg.keys; + } + + throw std::runtime_error("Unsupported group config type requested."); +} + +void ConfigManager::prune_volatile_orphans() { + // Remove orphaned 1to1 conversations + std::vector stale; + for (auto it = _config_convo_info_volatile->begin_1to1(); it != _config_convo_info_volatile->end(); ++it) + if (!_config_contacts->get(it->session_id)) + stale.push_back(it->session_id); + for (const auto& sid : stale) + _config_convo_info_volatile->erase_1to1(sid); + + // Remove orphaned community conversdations + std::vector> stale_comms; + for (auto it = _config_convo_info_volatile->begin_communities(); it != _config_convo_info_volatile->end(); ++it) + if (!_config_user_groups->get_community(it->base_url(), it->room_norm())) + stale_comms.emplace_back(it->base_url(), it->room()); + for (const auto& [base, room] : stale_comms) + _config_convo_info_volatile->erase_community(base, room); + + // Remove orphaned group conversations + stale.clear(); + for (auto it = _config_convo_info_volatile->begin_groups(); it != _config_convo_info_volatile->end(); ++it) + if (!_config_user_groups->get_group(it->id)) + stale.push_back(it->id); + for (const auto& id : stale) + _config_convo_info_volatile->erase_group(id); + + // Remove orphaned legacy group conversations + stale.clear(); + for (auto it = _config_convo_info_volatile->begin_legacy_groups(); it != _config_convo_info_volatile->end(); ++it) + if (!_config_user_groups->get_legacy_group(it->id)) + stale.push_back(it->id); + for (const auto& id : stale) + _config_convo_info_volatile->erase_legacy_group(id); +} + +std::vector ConfigManager::conversations() const { + if (!_config_user_profile || !_config_contacts || !_config_user_groups || !_config_convo_info_volatile) + throw std::runtime_error("Some configs are not initialized."); + + // We `+ 1` for the "Note to Self" conversation (in case it is visible) + auto conversation_size = _config_contacts->size() + _config_user_groups->size() + 1; + std::vector conversations; + conversations.reserve(conversation_size); + + // If the "Note to Self" conversation should be visible then add it to the list + if (_config_user_profile->get_nts_priority() >= 0) { + auto user_pubkey = "05{}"_format(oxenc::to_hex(_user_pk.begin(), _user_pk.end())); + std::string name = std::string(_config_user_profile->get_name().value_or("")); + conversations.emplace_back(one_to_one_conversation{ + user_pubkey, + conversation_type::one_to_one, + name, + display_pic_item{ + user_pubkey, + name, + _config_user_profile->get_profile_pic()}, + 0, // No "created" timestamp for NTS + _config_user_profile->get_nts_last_active(), + _config_user_profile->get_nts_priority(), + notify_mode::disabled, // notify_mode not supported in NTS + 0, // mute_until not supported in NTS + false, // is_message_request not supported in NTS + false // is_blocked not supported in NTS + }); + } + + // One to one conversations + for (auto it = _config_contacts->begin(); it != _config_contacts->end(); ++it) { + auto display_name = it->nickname; + int64_t last_active = 0; + + if (display_name.empty()) + display_name = it->name; + + if (auto v = _config_convo_info_volatile->get_1to1(it->session_id)) + last_active = v->last_active; + + conversations.emplace_back(one_to_one_conversation{ + it->session_id, + conversation_type::one_to_one, + display_name, + display_pic_item{ + it->session_id, + display_name, + it->profile_picture}, + it->created, + last_active, + it->priority, + it->notifications, + it->mute_until, + !it->approved, + it->blocked + }); + } + + // Community conversations + for (auto it = _config_user_groups->begin_communities(); it != _config_user_groups->end(); ++it) { + int64_t last_active = 0; + + if (auto v = _config_convo_info_volatile->get_community(it->base_url(), it->room_norm())) + last_active = v->last_active; + + conversations.emplace_back(community_conversation{ + it->full_url(), + conversation_type::community, + it->room(), + display_pic_item{it->full_url(), it->room(), profile_pic{}}, + it->joined_at, + last_active, + it->priority, + it->notifications, + it->mute_until + }); + } + + // Group conversations + for (auto it = _config_user_groups->begin_groups(); it != _config_user_groups->end(); ++it) { + auto name = it->name; + int64_t last_active = 0; + + // Retrieve the name from the `groups::Info` config if available since that's the source of truth + if (auto group = _config_groups.find(it->id); group != _config_groups.end()) + name = group->second->info->get_name().value_or(name); + + if (auto v = _config_convo_info_volatile->get_group(it->id)) + last_active = v->last_active; + + conversations.emplace_back(group_conversation{ + it->id, + conversation_type::group, + name, + display_pic_item{it->id, name, profile_pic{}}, + it->joined_at, + last_active, + it->priority, + it->notifications, + it->mute_until, + it->invited + }); + } + + // Legacy group conversations + for (auto it = _config_user_groups->begin_legacy_groups(); it != _config_user_groups->end(); ++it) { + int64_t last_active = 0; + + if (auto v = _config_convo_info_volatile->get_legacy_group(it->session_id)) + last_active = v->last_active; + + conversations.emplace_back(legacy_group_conversation{ + it->session_id, + conversation_type::legacy_group, + it->name, + display_pic_item{it->session_id, it->name, profile_pic{}}, + it->joined_at, + last_active, + it->priority, + it->notifications, + it->mute_until + }); + } + + // Outgoing blinded message requests + auto blinded_contacts = _config_contacts->blinded_contacts(); + for (auto& bc : blinded_contacts) { + int64_t last_active = 0; + + if (auto v = _config_convo_info_volatile->get_blinded_1to1(bc.session_id(), bc.legacy_blinding)) + last_active = v->last_active; + + conversations.emplace_back(blinded_one_to_one_conversation{ + bc.session_id(), + conversation_type::blinded_one_to_one, + bc.name, + display_pic_item{bc.session_id(), bc.name, profile_pic{}}, + bc.base_url(), + bc.pubkey_hex(), + bc.legacy_blinding, + bc.created, + last_active, + 0, // priority not supported in blinded conversations + notify_mode::disabled, // notify_mode not supported in blinded conversations + 0 // mute_until not supported in blinded conversations + }); + } + + // Sort the conversations + std::sort(conversations.begin(), conversations.end(), [](const auto& a, const auto& b) { + auto get = [](const auto& conv, auto ptr) { + return std::visit([ptr](const auto& c) { return c.*ptr; }, conv); + }; + auto get_string = [](const auto& conv, auto ptr) -> const std::string& { + return std::visit([ptr](const auto& c) -> const std::string& { return c.*ptr; }, conv); + }; + + // 1. Sort by last_active (descending) + int64_t last_active_a = get(a, &base_conversation::last_active); + int64_t last_active_b = get(b, &base_conversation::last_active); + bool has_last_active_a = (last_active_a != 0); + bool has_last_active_b = (last_active_b != 0); + + if (has_last_active_a != has_last_active_b) + return has_last_active_a; + else if (last_active_a != last_active_b) + return last_active_a > last_active_b; + + // 2. Sort by first_active (descending) + int64_t first_active_a = get(a, &base_conversation::first_active); + int64_t first_active_b = get(b, &base_conversation::first_active); + bool has_first_active_a = (first_active_a != 0); + bool has_first_active_b = (first_active_b != 0); + + if (has_first_active_a != has_first_active_b) + return has_first_active_a; + else if (first_active_a != first_active_b) + return first_active_a > first_active_b; + + // 3. Sort by id (ascending) + const std::string& id_a = get_string(a, &base_conversation::id); + const std::string& id_b = get_string(b, &base_conversation::id); + + return id_a < id_b; + }); + + return conversations; +} + +} // namespace session::config + +using namespace session; +using namespace session::config; + +namespace { + +ConfigManager& unbox(config_manager* manager) { + assert(manager && manager->internals); + return *static_cast(manager->internals); +} +const ConfigManager& unbox(const config_manager* manager) { + assert(manager && manager->internals); + return *static_cast(manager->internals); +} +template +[[nodiscard]] bool c_wrapper_for_config(config_object** conf, ConfigT& config) { + auto c = std::make_unique>(static_cast(&config)); + auto c_conf = std::make_unique(); + c_conf->internals = c.release(); + c_conf->last_error = nullptr; + *conf = c_conf.release(); + return true; +} +[[nodiscard]] bool c_wrapper_for_keys_config(config_group_keys** conf, groups::Keys& config) { + auto c_conf = std::make_unique(); + c_conf->internals = &config; + c_conf->last_error = nullptr; + *conf = c_conf.release(); + return true; +} +inline bool set_error_value(char* error, std::string_view e) { + if (!error) + return false; + + std::string msg = {e.data(), e.size()}; + if (msg.size() > 255) + msg.resize(255); + std::memcpy(error, msg.c_str(), msg.size() + 1); + return false; +} + +} // namespace + +extern "C" { + +LIBSESSION_C_API bool config_manager_init( + config_manager** manager, const unsigned char* ed25519_secretkey_bytes, char* error) { + auto c_manager = std::make_unique(); + + try { + std::span ed25519_secretkey = {ed25519_secretkey_bytes, 64}; + auto m = std::make_unique(ed25519_secretkey); + c_manager->internals = m.release(); + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } + + *manager = c_manager.release(); + return true; +} + +LIBSESSION_C_API void config_manager_free(config_manager* manager) { + delete static_cast(manager->internals); + delete manager; +} + +LIBSESSION_C_API bool config_manager_load( + config_manager* manager, + int16_t namespace_, + const char* group_ed25519_pubkey_hex_, + const unsigned char* dump_, + size_t dumplen, + char* error) { + try { + std::optional group_ed25519_pubkey_hex; + if (group_ed25519_pubkey_hex_) + group_ed25519_pubkey_hex = {group_ed25519_pubkey_hex_, 64}; + + std::optional> dump; + if (dump_ && dumplen > 0) + dump = {dump_, dumplen}; + + unbox(manager).load( + static_cast(namespace_), group_ed25519_pubkey_hex, dump); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API bool config_manager_get_config( + config_manager* manager, + const uint16_t namespace_, + const char* pubkey_hex, + config_object** config, + char* error) { + try { + if (!manager || !config) + throw std::invalid_argument{"Null argument(s) provided."}; + + switch (static_cast(namespace_)) { + case Namespace::Contacts: + return c_wrapper_for_config(config, unbox(manager).config()); + + case Namespace::ConvoInfoVolatile: + return c_wrapper_for_config(config, unbox(manager).config()); + + case Namespace::UserGroups: + return c_wrapper_for_config(config, unbox(manager).config()); + + case Namespace::UserProfile: + return c_wrapper_for_config(config, unbox(manager).config()); + + case Namespace::Local: + return c_wrapper_for_config(config, unbox(manager).config()); + + case Namespace::GroupInfo: + if (!pubkey_hex) + throw std::invalid_argument{"Invalid pubkey_hex - required for group configs"}; + return c_wrapper_for_config(config, unbox(manager).config({pubkey_hex, 66})); + + case Namespace::GroupMembers: + if (!pubkey_hex) + throw std::invalid_argument{"Invalid pubkey_hex - required for group configs"}; + return c_wrapper_for_config( + config, unbox(manager).config({pubkey_hex, 66})); + + case Namespace::GroupKeys: + throw std::runtime_error{"Use 'config_manager_get_keys_config' to retrieve the group keys config"}; + + default: + throw std::runtime_error{"Attempted to get config for unknown namespace"}; + } + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API bool config_manager_get_keys_config( + config_manager* manager, + const char* pubkey_hex, + config_group_keys** config, + char* error) { + try { + if (!manager || !config || !pubkey_hex) + throw std::invalid_argument{"Null argument(s) provided."}; + + return c_wrapper_for_keys_config(config, unbox(manager).config()); + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API void config_manager_free_config(config_object* config) { + if (config->internals) { + delete static_cast(config->internals); + } + delete config; +} + +LIBSESSION_C_API void config_manager_free_keys_config(config_group_keys* config) { + // We intentionally don't delete config->internals because that owned by the ConfigManager + delete config; +} + +LIBSESSION_C_API bool config_manager_get_conversations(const config_manager* manager, config_convo** conversations, size_t* conversations_len, char* error) { + try { + if (!manager || !conversations || !conversations_len) + throw std::invalid_argument{"Null argument(s) provided."}; + + auto cpp_conversations = unbox(manager).conversations(); + *conversations_len = cpp_conversations.size(); + + if (cpp_conversations.size() == 0) { + return true; + } + + *conversations = static_cast(std::malloc(cpp_conversations.size() * sizeof(config_convo))); + if (!conversations) { + *conversations_len = 0; + throw std::runtime_error{"Memory allocation failed."}; + } + + unsigned char* pos = reinterpret_cast(*conversations); + memset(*conversations, 0, cpp_conversations.size() * sizeof(config_convo)); + + for (size_t i = 0; i < cpp_conversations.size(); ++i) { + config_convo* conversation = (config_convo*)pos; + const auto& cpp_conversation_variant = cpp_conversations[i]; + + std::visit([&](const auto& cpp_conversation) { + copy_c_str(conversation->id, cpp_conversation.id); + copy_c_str(conversation->name, cpp_conversation.name); + conversation->type = static_cast(cpp_conversation.type); + conversation->first_active = cpp_conversation.first_active; + conversation->last_active = cpp_conversation.last_active; + conversation->priority = cpp_conversation.priority; + conversation->notifications = static_cast(cpp_conversation.notifications); + conversation->mute_until = cpp_conversation.mute_until; + + // Assign type-specific fields + if constexpr (std::is_same_v, one_to_one_conversation>) { + conversation->specific_data.one_to_one.is_message_request = cpp_conversation.is_message_request; + conversation->specific_data.one_to_one.is_blocked = cpp_conversation.is_blocked; + } else if constexpr (std::is_same_v, group_conversation>) { + conversation->specific_data.group.is_message_request = cpp_conversation.is_message_request; + } else if constexpr (std::is_same_v, blinded_one_to_one_conversation>) { + copy_c_str(conversation->specific_data.blinded_one_to_one.base_url, cpp_conversation.community_base_url); + std::memcpy(conversation->specific_data.blinded_one_to_one.pubkey, cpp_conversation.community_public_key.data(), 32); + conversation->specific_data.blinded_one_to_one.legacy_blinding = cpp_conversation.legacy_blinding; + } + }, cpp_conversation_variant); + + pos += sizeof(config_convo); + } + + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +} // extern "C" diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index 093c0a9..8b51142 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -1,8 +1,14 @@ #include "session/config/contacts.hpp" +#include +#include #include +#include +#include #include +#include +#include #include #include "internal.hpp" @@ -14,8 +20,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); @@ -34,7 +39,7 @@ LIBSESSION_C_API bool session_id_is_valid(const char* session_id) { } contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { - check_session_id(session_id); + check_session_id(session_id, "05"); } void contact_info::set_name(std::string n) { @@ -61,15 +66,6 @@ Contacts::Contacts( load_key(ed25519_secretkey); } -LIBSESSION_C_API int contacts_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - return c_wrapper_init(conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - void contact_info::load(const dict& info_dict) { name = maybe_string(info_dict, "n").value_or(""); nickname = maybe_string(info_dict, "N").value_or(""); @@ -167,6 +163,73 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, created = to_epoch_seconds(c.created); } +blinded_contact_info::blinded_contact_info( + std::string_view base_url, + std::string_view blinded_id, + std::span pubkey, + bool legacy_blinding) : + legacy_blinding{legacy_blinding}, + community(std::move(base_url), blinded_id.substr(2), std::move(pubkey)) { + check_session_id(blinded_id, legacy_blinding ? "15" : "25"); +} + +const std::string blinded_contact_info::session_id() const { + return "{}{}"_format(legacy_blinding ? "15" : "25", room()); +} + +void blinded_contact_info::set_name(std::string n) { + if (n.size() > contact_info::MAX_NAME_LENGTH) + name = utf8_truncate(std::move(n), contact_info::MAX_NAME_LENGTH); + else + name = std::move(n); +} + +void blinded_contact_info::load(const dict& info_dict) { + name = maybe_string(info_dict, "n").value_or(""); + + auto url = maybe_string(info_dict, "p"); + auto key = maybe_vector(info_dict, "q"); + if (url && key && !url->empty() && key->size() == 32) { + profile_picture.url = std::move(*url); + profile_picture.key = std::move(*key); + } else { + profile_picture.clear(); + } + legacy_blinding = maybe_int(info_dict, "y").value_or(0); + created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0)); +} + +void blinded_contact_info::into(contacts_blinded_contact& c) const { + copy_c_str(c.base_url, base_url()); + c.session_id[0] = (legacy_blinding ? '1' : '2'); + c.session_id[1] = '5'; + std::memcpy(c.session_id + 2, session_id().data(), 64); + c.session_id[66] = '\0'; + std::memcpy(c.pubkey, pubkey().data(), 32); + copy_c_str(c.name, name); + if (profile_picture) { + copy_c_str(c.profile_pic.url, profile_picture.url); + std::memcpy(c.profile_pic.key, profile_picture.key.data(), 32); + } else { + copy_c_str(c.profile_pic.url, ""); + } + c.legacy_blinding = legacy_blinding; + c.created = to_epoch_seconds(created); +} + +blinded_contact_info::blinded_contact_info(const contacts_blinded_contact& c) : + community(c.base_url, {c.session_id + 2, 64}, c.pubkey) { + assert(std::strlen(c.name) <= contact_info::MAX_NAME_LENGTH); + name = c.name; + assert(std::strlen(c.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); + if (std::strlen(c.profile_pic.url)) { + profile_picture.url = c.profile_pic.url; + profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32); + } + legacy_blinding = c.legacy_blinding; + created = to_epoch_seconds(c.created); +} + std::optional Contacts::get(std::string_view pubkey_hex) const { std::string pubkey = session_id_to_bytes(pubkey_hex); @@ -179,20 +242,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 +249,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]; @@ -249,16 +287,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)); @@ -329,22 +357,107 @@ bool Contacts::erase(std::string_view session_id) { return ret; } -LIBSESSION_C_API bool contacts_erase(config_object* conf, const char* session_id) { - try { - return unbox(conf)->erase(session_id); - } catch (...) { - return false; - } -} - size_t Contacts::size() const { if (auto* c = data["c"].dict()) return c->size(); return 0; } -LIBSESSION_C_API size_t contacts_size(const config_object* conf) { - return unbox(conf)->size(); +ConfigBase::DictFieldProxy Contacts::blinded_contact_field( + const blinded_contact_info& bc, std::span* get_pubkey) const { + auto record = data["b"][bc.base_url()]; + if (get_pubkey) { + auto pkrec = record["#"]; + if (auto pk = pkrec.string_view_or(""); pk.size() == 32) + *get_pubkey = std::span{ + reinterpret_cast(pk.data()), pk.size()}; + } + return record["R"][bc.room()]; // The `room` value is the blinded id without the prefix +} + +using any_blinded_contact = std::variant; + +std::optional Contacts::get_blinded( + std::string_view pubkey_hex, bool legacy_blinding) const { + check_session_id(pubkey_hex, legacy_blinding ? "15" : "25"); + + if (auto* b = data["b"].dict()) { + auto comm = comm_iterator_helper{b->begin(), b->end()}; + std::shared_ptr val; + + while (!comm.done()) { + if (comm.load(val)) // TODO: This is untested + if (auto* ptr = std::get_if(val.get()); + ptr && ptr->session_id() == pubkey_hex) + return *ptr; + comm.advance(); + } + } + + return std::nullopt; +} + +std::vector Contacts::blinded_contacts() const { + std::vector ret; + + if (auto* b = data["b"].dict()) { + auto comm = comm_iterator_helper{b->begin(), b->end()}; + std::shared_ptr val; + + while (!comm.done()) { + if (comm.load(val)) + if (auto* ptr = std::get_if(val.get())) + ret.emplace_back(*ptr); + comm.advance(); + } + } + + return ret; +} + +bool Contacts::set_blinded_contact(const blinded_contact_info& bc) { + data["b"][bc.base_url()]["#"] = bc.pubkey(); + auto info = blinded_contact_field(bc); // data["b"][base]["R"][bc_session_id_without_prefix] + + // Always set the name, even if empty, to keep the dict from getting pruned if there are no + // other entries. + info["n"] = bc.name.substr(0, contact_info::MAX_NAME_LENGTH); + + set_pair_if( + bc.profile_picture, + info["p"], + bc.profile_picture.url, + info["q"], + bc.profile_picture.key); + + set_positive_int(info["y"], bc.legacy_blinding); + set_positive_int(info["j"], to_epoch_seconds(bc.created)); +} + +bool Contacts::erase_blinded_contact( + std::string_view base_url_, std::string_view blinded_id, bool legacy_blinding) { + std::string pk = session_id_to_bytes(blinded_id, legacy_blinding ? "15" : "25").substr(2); + + auto base_url = community::canonical_url(base_url_); + auto info = data["d"][base_url]["R"][pk]; + bool ret = info.exists(); + info.erase(); + return ret; +} + +std::vector> Contacts::last_deleted_contacts() const { + std::vector> ret; + + if (auto info = data["d"].dict()) { + ret.reserve(info->size()); + + for (const auto& [key, value] : *info) + if (auto* exp_scalar = std::get_if(&value)) + if (auto* exp = std::get_if(exp_scalar)) + ret.emplace_back(key, *exp); + } + + return ret; } /// Load _val from the current iterator position; if it is invalid, skip to the next key until we @@ -387,6 +500,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_contact( + config_object* conf, + const char* blinded_session_id, + bool legacy_blinding, + contacts_blinded_contact* blinded_contact) { + return wrap_exceptions( + conf, + [&] { + if (auto bc = unbox(conf)->get_blinded( + blinded_session_id, legacy_blinding)) { + bc->into(*blinded_contact); + return true; + } + return false; + }, + false); +} + +LIBSESSION_C_API contacts_blinded_contact_list* contacts_blinded_contacts( + const config_object* conf) { + try { + auto cpp_contacts = unbox(conf)->blinded_contacts(); + + if (cpp_contacts.empty()) + return nullptr; + + // We malloc space for the contacts_blinded_contact_list struct itself, plus the required + // number of contacts_blinded_contact pointers to store its records, and the space to + // actually contain a copy of the data. When we're done, the malloced memory we grab is + // going to look like this: + // + // {contacts_blinded_contact_list} + // {pointer1}{pointer2}... + // {contacts_blinded_contact data 1\0}{contacts_blinded_contact data 2\0}... + // + // where contacts_blinded_contact.value points at the beginning of {pointer1}, and each + // pointerN points at the beginning of the {contacts_blinded_contact data N\0} struct. + // + // Since we malloc it all at once, when the user frees it, they also free the entire thing. + size_t sz = sizeof(contacts_blinded_contact_list) + + (cpp_contacts.size() * sizeof(contacts_blinded_contact*)) + + (cpp_contacts.size() * sizeof(contacts_blinded_contact)); + auto* ret = static_cast(std::malloc(sz)); + ret->len = cpp_contacts.size(); + + // value points at the space immediately after the struct itself, which is the first element + // in the array of contacts_blinded_contact pointers. + ret->value = reinterpret_cast(ret + 1); + contacts_blinded_contact* next_struct = + reinterpret_cast(ret->value + ret->len); + + for (size_t i = 0; i < cpp_contacts.size(); ++i) { + ret->value[i] = next_struct; + cpp_contacts[i].into(*next_struct); + next_struct++; + } + + return ret; + } catch (...) { + return nullptr; + } +} + +LIBSESSION_C_API bool contacts_set_blinded_contact( + config_object* conf, const contacts_blinded_contact* bc) { + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_blinded_contact(blinded_contact_info{*bc}); + return true; + }, + false); +} + +LIBSESSION_C_API bool contacts_erase_blinded_contact( + config_object* conf, const char* base_url, const char* blinded_id, bool legacy_blinding) { + try { + return unbox(conf)->erase_blinded_contact(base_url, blinded_id, legacy_blinding); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool last_deleted_contacts(const config_object* conf, const contacts_deleted_contact** contacts, size_t* contacts_len) { + try { + auto cpp_contacts = unbox(conf)->last_deleted_contacts(); + + if (cpp_contacts.empty()){ + *contacts = nullptr; + return true; + } + + auto* ret = (contacts_deleted_contact*)malloc(cpp_contacts.size() * sizeof(contacts_deleted_contact)); + + for (size_t i = 0; i < cpp_contacts.size(); ++i) { + std::memcpy(ret[i].session_id, cpp_contacts[i].first.data(), 67); + ret[i].session_id[66] = '\0'; // Ensure null termination + ret[i].deleted = cpp_contacts[i].second; + } + + *contacts_len = cpp_contacts.size(); + return true; + } 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 +689,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 8d1206c..4442e0c 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -30,17 +30,18 @@ namespace convo { check_session_id(session_id); } one_to_one::one_to_one(const convo_info_volatile_1to1& c) : - base{c.last_read, c.unread}, session_id{c.session_id, 66} {} + base{c.last_read, c.last_active, c.unread}, session_id{c.session_id, 66} {} void one_to_one::into(convo_info_volatile_1to1& c) const { std::memcpy(c.session_id, session_id.data(), 67); c.last_read = last_read; + c.last_active = last_active; c.unread = unread; } community::community(const convo_info_volatile_community& c) : config::community{c.base_url, c.room, std::span{c.pubkey, 32}}, - base{c.last_read, c.unread} {} + base{c.last_read, c.last_active, c.unread} {} void community::into(convo_info_volatile_community& c) const { static_assert(sizeof(c.base_url) == BASE_URL_MAX_LENGTH + 1); @@ -49,6 +50,7 @@ namespace convo { copy_c_str(c.room, room_norm()); std::memcpy(c.pubkey, pubkey().data(), 32); c.last_read = last_read; + c.last_active = last_active; c.unread = unread; } @@ -59,11 +61,12 @@ namespace convo { check_session_id(id, "03"); } group::group(const convo_info_volatile_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{c.last_read, c.last_active, c.unread}, id{c.group_id, 66} {} void group::into(convo_info_volatile_group& c) const { std::memcpy(c.group_id, id.c_str(), 67); c.last_read = last_read; + c.last_active = last_active; c.unread = unread; } @@ -74,16 +77,39 @@ namespace convo { check_session_id(id); } legacy_group::legacy_group(const convo_info_volatile_legacy_group& c) : - base{c.last_read, c.unread}, id{c.group_id, 66} {} + base{c.last_read, c.last_active, c.unread}, id{c.group_id, 66} {} void legacy_group::into(convo_info_volatile_legacy_group& c) const { std::memcpy(c.group_id, id.data(), 67); c.last_read = last_read; + c.last_active = last_active; 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.last_active, c.unread}, + blinded_session_id{c.blinded_session_id, 66}, + legacy_blinding{c.legacy_blinding} {} + + void blinded_one_to_one::into(convo_info_volatile_blinded_1to1& c) const { + std::memcpy(c.blinded_session_id, blinded_session_id.data(), 67); + c.last_read = last_read; + c.last_active = last_active; + 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); + last_active = maybe_int(info_dict, "l").value_or(0); unread = (bool)maybe_int(info_dict, "u").value_or(0); } @@ -213,61 +239,37 @@ convo::legacy_group ConvoInfoVolatile::get_or_construct_legacy_group( return convo::legacy_group{std::string{pubkey_hex}}; } -void ConvoInfoVolatile::set(const convo::one_to_one& c) { - auto info = data["1"][session_id_to_bytes(c.session_id)]; - set_base(c, info); -} - -void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { - auto r = info["r"]; +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"); - // If we're making the last_read value *older* for some reason then ignore the prune cutoff - // (because we might be intentionally resetting the value after a deletion, for instance). - if (auto* val = r.integer(); val && c.last_read < *val) - r = c.last_read; - else { - std::chrono::system_clock::time_point last_read{std::chrono::milliseconds{c.last_read}}; - if (last_read > std::chrono::system_clock::now() - PRUNE_LOW) - info["r"] = c.last_read; - } + auto* info_dict = data["b"][pubkey].dict(); + if (!info_dict) + return std::nullopt; - set_flag(info["u"], c.unread); + auto result = + std::make_optional(std::string{pubkey_hex}, legacy_blinding); + result->load(*info_dict); + return result; } -void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) { - const int64_t cutoff = std::chrono::duration_cast( - (std::chrono::system_clock::now() - prune).time_since_epoch()) - .count(); - - std::vector stale; - for (auto it = begin_1to1(); it != end(); ++it) - if (!it->unread && it->last_read < cutoff) - stale.push_back(it->session_id); - for (const auto& sid : stale) - erase_1to1(sid); - - stale.clear(); - for (auto it = begin_legacy_groups(); it != end(); ++it) - if (!it->unread && it->last_read < cutoff) - stale.push_back(it->id); - for (const auto& id : stale) - erase_legacy_group(id); +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); - std::vector> stale_comms; - for (auto it = begin_communities(); it != end(); ++it) - if (!it->unread && it->last_read < cutoff) - stale_comms.emplace_back(it->base_url(), it->room()); - for (const auto& [base, room] : stale_comms) - erase_community(base, room); + return convo::blinded_one_to_one{std::string{pubkey_hex}, legacy_blinding}; } -std::tuple>, std::vector> -ConvoInfoVolatile::push() { - // Prune off any conversations with last_read timestamps more than PRUNE_HIGH ago (unless they - // also have a `unread` flag set, in which case we keep them indefinitely). - prune_stale(); +void ConvoInfoVolatile::set(const convo::one_to_one& c) { + auto info = data["1"][session_id_to_bytes(c.session_id)]; + set_base(c, info); +} - return ConfigBase::push(); +void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) { + set_nonzero_int(info["r"], c.last_read); + set_nonzero_int(info["l"], c.last_active); + set_flag(info["u"], c.unread); } void ConvoInfoVolatile::set(const convo::community& c) { @@ -286,6 +288,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 +325,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 +346,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 +385,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 +421,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 +436,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 +447,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 +466,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 +489,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 +510,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 +650,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( @@ -644,6 +724,27 @@ 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_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( @@ -667,6 +768,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 +794,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 +832,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 +885,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/internal.hpp b/src/config/internal.hpp index 74cc31f..f02c986 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -16,11 +16,13 @@ namespace session::config { template [[nodiscard]] int c_wrapper_init_generic(config_object** conf, char* error, Args&&... args) { - auto c = std::make_unique>(); + std::unique_ptr internal_wrapper; auto c_conf = std::make_unique(); try { - c->config = std::make_unique(std::forward(args)...); + internal_wrapper = std::make_unique>( + std::in_place_type_t>{}, + std::forward(args)...); } catch (const std::exception& e) { if (error) { std::string msg = e.what(); @@ -31,7 +33,7 @@ template return SESSION_ERR_INVALID_DUMP; } - c_conf->internals = c.release(); + c_conf->internals = internal_wrapper.release(); c_conf->last_error = nullptr; *conf = c_conf.release(); return SESSION_ERR_NONE; diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index f14b925..0b4d348 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -52,6 +52,14 @@ void UserProfile::set_profile_pic(profile_pic pic) { set_profile_pic(pic.url, pic.key); } +void UserProfile::set_nts_last_active(int64_t last_active) { + set_nonzero_int(data["l"], last_active); +} + +int64_t UserProfile::get_nts_last_active() const { + return data["l"].integer_or(0); +} + void UserProfile::set_nts_priority(int priority) { set_nonzero_int(data["+"], priority); } @@ -141,6 +149,14 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic static_cast(SESSION_ERR_BAD_VALUE)); } +LIBSESSION_C_API int64_t user_profile_get_nts_last_active(const config_object* conf) { + return unbox(conf)->get_nts_last_active(); +} + +LIBSESSION_EXPORT void user_profile_set_nts_last_active(config_object* conf, int64_t last_active) { + unbox(conf)->set_nts_last_active(last_active); +} + LIBSESSION_C_API int user_profile_get_nts_priority(const config_object* conf) { return unbox(conf)->get_nts_priority(); } diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index daf1ed3..c1aa421 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -44,6 +44,7 @@ TEST_CASE("Conversations", "[config][conversations]") { CHECK(c.session_id == definitely_real_id); CHECK(c.last_read == 0); + CHECK(c.last_active == 0); CHECK_FALSE(convos.needs_push()); CHECK_FALSE(convos.needs_dump()); @@ -84,9 +85,11 @@ TEST_CASE("Conversations", "[config][conversations]") { auto g = convos.get_or_construct_group(benders_nightmare_group); CHECK(g.id == benders_nightmare_group); CHECK(g.last_read == 0); + CHECK(g.last_active == 0); CHECK_FALSE(g.unread); g.last_read = now_ms; + g.last_active = now_ms + 1; g.unread = true; convos.set(g); @@ -112,6 +115,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto x1 = convos2.get_1to1(definitely_real_id); REQUIRE(x1); CHECK(x1->last_read == now_ms); + CHECK(x1->last_active == 0); CHECK(x1->session_id == definitely_real_id); CHECK_FALSE(x1->unread); @@ -125,6 +129,7 @@ TEST_CASE("Conversations", "[config][conversations]") { auto x3 = convos2.get_group(benders_nightmare_group); REQUIRE(x3); CHECK(x3->last_read == now_ms); + CHECK(x3->last_active == now_ms + 1); CHECK(x3->unread); auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"sv; @@ -260,6 +265,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(c.last_read == 0); + CHECK(c.last_active == 0); CHECK_FALSE(c.unread); CHECK_FALSE(config_needs_push(conf)); @@ -270,6 +276,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { .count(); c.last_read = now_ms; + c.last_active = now_ms; // The new data doesn't get stored until we call this: convo_info_volatile_set_1to1(conf, &c); @@ -278,6 +285,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { REQUIRE_FALSE(convo_info_volatile_get_legacy_group(conf, &cg, definitely_real_id)); REQUIRE(convo_info_volatile_get_1to1(conf, &c, definitely_real_id)); CHECK(c.last_read == now_ms); + CHECK(c.last_active == now_ms); CHECK(config_needs_push(conf)); CHECK(config_needs_dump(conf)); @@ -340,6 +348,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { REQUIRE(convo_info_volatile_get_1to1(conf2, &c, definitely_real_id)); CHECK(c.last_read == now_ms); + CHECK(c.last_active == now_ms); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK_FALSE(c.unread); @@ -466,102 +475,6 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { }}); } -TEST_CASE("Conversation pruning", "[config][conversations][pruning]") { - - 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::ConvoInfoVolatile convos{std::span{seed}, std::nullopt}; - - auto some_pubkey = [](unsigned char x) -> std::vector { - std::vector s = - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes; - s[31] = x; - return s; - }; - auto some_session_id = [&](unsigned char x) -> std::string { - auto pk = some_pubkey(x); - return "05" + oxenc::to_hex(pk.begin(), pk.end()); - }; - const auto now = std::chrono::system_clock::now() - 1ms; - auto unix_timestamp = [&now](int days_ago) -> int64_t { - return std::chrono::duration_cast( - (now - days_ago * 24h).time_since_epoch()) - .count(); - }; - for (int i = 0; i <= 65; i++) { - if (i % 3 == 0) { - auto c = convos.get_or_construct_1to1(some_session_id(i)); - c.last_read = unix_timestamp(i); - if (i % 5 == 0) - c.unread = true; - convos.set(c); - } else if (i % 3 == 1) { - auto c = convos.get_or_construct_legacy_group(some_session_id(i)); - c.last_read = unix_timestamp(i); - if (i % 5 == 0) - c.unread = true; - convos.set(c); - } else { - auto c = convos.get_or_construct_community( - "https://example.org", "room{}"_format(i), some_pubkey(i)); - c.last_read = unix_timestamp(i); - if (i % 5 == 0) - c.unread = true; - convos.set(c); - } - } - - // 0, 3, 6, ..., 30 == 11 not-too-old last_read entries - // 45, 60 have unread flags - CHECK(convos.size_1to1() == 11 + 2); - // 1, 4, 7, ..., 28 == 10 last_read's - // 40, 55 = 2 unread flags - CHECK(convos.size_legacy_groups() == 10 + 2); - // 2, 5, 8, ..., 29 == 10 last_read's - // 35, 50, 65 = 3 unread flags - CHECK(convos.size_communities() == 10 + 3); - // 31 (0-30) were recent enough to be kept - // 5 more (35, 40, 45, 50, 55) have `unread` set. - CHECK(convos.size() == 38); - - // Now we deliberately set some values in the internals that are too old to see that they get - // properly pruned when we push. (This is only for testing, clients should never touch the - // internals like this!) - - // These ones wouldn't be stored by the normal `set()` interface, but won't get pruned either: - convos.data["1"][oxenc::from_hex(some_session_id(80))]["r"] = unix_timestamp(33); - convos.data["1"][oxenc::from_hex(some_session_id(81))]["r"] = unix_timestamp(40); - convos.data["1"][oxenc::from_hex(some_session_id(82))]["r"] = unix_timestamp(44); - // These ones should get pruned as soon as we push: - convos.data["1"][oxenc::from_hex(some_session_id(83))]["r"] = unix_timestamp(45); - convos.data["1"][oxenc::from_hex(some_session_id(84))]["r"] = unix_timestamp(46); - convos.data["1"][oxenc::from_hex(some_session_id(85))]["r"] = unix_timestamp(1000); - - CHECK(convos.size_1to1() == 19); - int count = 0; - for (auto it = convos.begin_1to1(); it != convos.end(); it++) { - count++; - } - CHECK(count == 19); - - CHECK(convos.size() == 44); - auto [seqno, push_data, obs] = convos.push(); - CHECK(convos.size() == 41); -} - TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load]") { const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes;