diff --git a/.gitignore b/.gitignore index f13ebfd0..e705fdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build*/ /compile_commands.json /.cache/ -/.vscode/ \ No newline at end of file +/.vscode/ +.DS_Store \ No newline at end of file diff --git a/include/session/config.h b/include/session/config.h index eea54c1d..97f14164 100644 --- a/include/session/config.h +++ b/include/session/config.h @@ -4,10 +4,22 @@ extern "C" { #endif +#include #include typedef int64_t seqno_t; +/// Struct containing a list of C strings. 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 session_string_list can be free()d to free both the list *and* the inner `value` +/// and pointed-at values. +typedef struct session_string_list { + char** value; // array of null-terminated C strings + size_t len; // length of `value` +} session_string_list; + #ifdef __cplusplus } #endif diff --git a/include/session/config/base.h b/include/session/config/base.h deleted file mode 100644 index 8707d903..00000000 --- a/include/session/config/base.h +++ /dev/null @@ -1,539 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include -#include - -#include "../config.h" -#include "../export.h" - -// Config object base type: this type holds the internal object and is initialized by the various -// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions. -typedef struct config_object { - // Internal opaque object pointer; calling code should leave this alone. - void* internals; - // When an error occurs in the C API this string will be set to the specific error message. May - // be empty. - const char* last_error; - - // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. - char _error_buf[256]; -} config_object; - -// Common functions callable on any config instance: - -/// API: base/config_free -/// -/// Frees a config object created with one of the config-dependent ..._init functions (e.g. -/// user_profile_init). -/// -/// Declaration: -/// ```cpp -/// VOID config_free( -/// [in, out] config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -LIBSESSION_EXPORT void config_free(config_object* conf); - -typedef enum config_log_level { - LOG_LEVEL_DEBUG = 0, - LOG_LEVEL_INFO, - LOG_LEVEL_WARNING, - LOG_LEVEL_ERROR -} config_log_level; - -/// API: base/config_set_logger -/// -/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL -/// if not needed). The given function pointer will be invoked with one of the above values, a -/// null-terminated c string containing the log message, and the void* context object given when -/// setting the logger (this is for caller-specific state data and won't be touched). -/// -/// The logging function must have signature: -/// -/// void log(config_log_level lvl, const char* msg, void* ctx); -/// -/// Can be called with callback set to NULL to clear an existing logger. -/// -/// The config object itself has no log level: the caller should filter by level as needed. -/// -/// Declaration: -/// ```cpp -/// VOID config_set_logger( -/// [in, out] config_object* conf, -/// [in] void(*)(config_log_level, const char*, void*) callback, -/// [in] void* ctx -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `callback` -- [in] Callback function -/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused -LIBSESSION_EXPORT void config_set_logger( - config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx); - -/// API: base/config_storage_namespace -/// -/// Returns the numeric namespace in which config messages of this type should be stored. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int16_t` -- integer of the namespace -LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf); - -/// Struct containing a list of C strings. 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 config_string_list can be free()d to free both the list *and* the inner `value` -/// and pointed-at values. -typedef struct config_string_list { - char** value; // array of null-terminated C strings - size_t len; // length of `value` -} config_string_list; - -/// API: base/config_merge -/// -/// Merges the config object with one or more remotely obtained config strings. After this call the -/// config object may be unchanged, complete replaced, or updated and needing a push, depending on -/// the messages that are merged; the caller should check config_needs_push(). -/// -/// Declaration: -/// ```cpp -/// INT config_merge( -/// [in, out] config_object* conf, -/// [in] const char** msg_hashes, -/// [in] const unsigned char** configs, -/// [in] const size_t* lengths, -/// [in] size_t count -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `msg_hashes` -- [in] is an array of null-terminated C strings containing the hashes of the -/// configs being provided. -/// - `configs` -- [in] is an array of pointers to the start of the (binary) data. -/// - `lengths` -- [in] is an array of lengths of the binary data -/// - `count` -- [in] is the length of all three arrays. -/// -/// Outputs: -/// - `config_string_list*` -- pointer to the list of successfully parsed hashes; the pointer -/// belongs to the caller and must be freed when done with it. - -LIBSESSION_EXPORT config_string_list* config_merge( - config_object* conf, - const char** msg_hashes, - const unsigned char** configs, - const size_t* lengths, - size_t count) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; - -/// API: base/config_needs_push -/// -/// Returns true if this config object contains updated data that has not yet been confirmed stored -/// on the server. -/// -/// Declaration: -/// ```cpp -/// BOOL config_needs_push( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `bool` -- returns true if object contains updated data -LIBSESSION_EXPORT bool config_needs_push(const config_object* conf); - -/// Returned struct of config push data. -typedef struct config_push_data { - // The config seqno (to be provided later in `config_confirm_pushed`). - seqno_t seqno; - // The config message to push (binary data, not null-terminated). - unsigned char* config; - // The length of `config` - size_t config_len; - // Array of obsolete message hashes to delete; each element is a null-terminated C string - char** obsolete; - // length of `obsolete` - size_t obsolete_len; -} config_push_data; - -/// API: base/config_push -/// -/// Obtains the configuration data that needs to be pushed to the server. -/// -/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used -/// to re-obtain the current serialized config even if no push is needed (for example, if the client -/// wants to re-submit it after a network error). -/// -/// NB: The returned pointer belongs to the caller: that is, the caller *MUST* free() it when -/// done with it. -/// -/// Declaration: -/// ```cpp -/// CONFIG_PUSH_DATA* config_push( -/// [in, out] config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `config_push_data*` -- pointer to the config object. Pointer belongs to the caller. -LIBSESSION_EXPORT config_push_data* config_push(config_object* conf); - -/// API: base/config_confirm_pushed -/// -/// Reports that data obtained from `config_push` has been successfully stored on the server with -/// message hash `msg_hash`. The seqno value is the one returned by the config_push call that -/// yielded the config data. -/// -/// Declaration: -/// ```cpp -/// VOID config_confirm_pushed( -/// [in, out] config_object* conf, -/// [out] seqno_t seqno, -/// [out] const char* msg_hash -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `seqno` -- [out] Value returned by config_push call -/// - `msg_hash` -- [out] Value returned by config_push call -LIBSESSION_EXPORT void config_confirm_pushed( - config_object* conf, seqno_t seqno, const char* msg_hash); - -/// API: base/config_dump -/// -/// Returns a binary dump of the current state of the config object. This dump can be used to -/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets -/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated -/// C string. -/// -/// NB: It is the caller's responsibility to `free()` the buffer when done with it. -/// -/// Immediately after this is called `config_needs_dump` will start returning true (until the -/// configuration is next modified). -/// -/// Declaration: -/// ```cpp -/// VOID config_dump( -/// [in] config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `out` -- [out] Pointer to the output location -/// - `outlen` -- [out] Length of output -LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen); - -/// API: base/config_needs_dump -/// -/// Returns true if something has changed since the last call to `dump()` that requires calling -/// and saving the `config_dump()` data again. -/// -/// Declaration: -/// ```cpp -/// BOOL config_needs_dump( -/// [in] const config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `bool` -- True if config has changed since last call to `dump()` -LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf); - -/// API: base/config_current_hashes -/// -/// Obtains the current active hashes. Note that this will be empty if the current hash is unknown -/// or not yet determined (for example, because the current state is dirty or because the most -/// recent push is still pending and we don't know the hash yet). -/// -/// The returned pointer belongs to the caller and must be freed via `free()` when done with it. -/// -/// Declaration: -/// ```cpp -/// CONFIG_STRING_LIST* config_current_hashes( -/// [in] const config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `config_string_list*` -- pointer to the list of hashes; the pointer belongs to the caller -LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf) -#ifdef __GNUC__ - __attribute__((warn_unused_result)) -#endif - ; - -/// API: base/config_get_keys -/// -/// Obtains the current group decryption keys. -/// -/// Returns a buffer where each consecutive 32 bytes is an encryption key for the object, in -/// priority order (i.e. the key at 0 is the encryption key, and the first decryption key). -/// -/// This function is mainly for debugging/diagnostics purposes; most config types have one single -/// key (based on the secret key), and multi-keyed configs such as groups have their own methods for -/// encryption/decryption that are already aware of the multiple keys. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config_object object -/// - `len` -- [out] Pointer where the number of keys will be written (that is: the returned pointer -/// will be to a buffer which has a size of of this value times 32). -/// -/// Outputs: -/// - `unsigned char*` -- pointer to newly malloced key data (a multiple of 32 bytes); the pointer -/// belongs to the caller and must be `free()`d when done with it. -LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size_t* len); - -/// Config key management; see the corresponding method docs in base.hpp. All `key` arguments here -/// are 32-byte binary buffers (and since fixed-length, there is no keylen argument). - -/// API: base/config_add_key -/// -/// Adds an encryption/decryption key, without removing existing keys. They key must be exactly -/// 32 bytes long. The newly added key becomes the highest priority key: it will be used for -/// encryption of config pushes after the call, and will be tried first when decrypting, followed by -/// keys present (if any) before this call. If the given key is already present in the key list -/// then this call moves it to the front of the list (if not already at the front). -/// -/// Declaration: -/// ```cpp -/// VOID config_add_key( -/// [in, out] config_object* conf, -/// [in] const unsigned char* key -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key); - -/// API: base/config_add_key_low_prio -/// -/// Adds an encryption/decryption key, without removing existing keys. They key must be exactly -/// 32 bytes long. The newly added key becomes the lowest priority key -/// -/// Declaration: -/// ```cpp -/// VOID config_add_key_low_prio( -/// [in, out] config_object* conf, -/// [in] const unsigned char* key -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in, out] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key); - -/// API: base/config_clear_keys -/// -/// Clears all stored encryption/decryption keys. This is typically immediately followed with -/// one or more `add_key` call to replace existing keys. Returns the number of keys removed. -/// -/// Declaration: -/// ```cpp -/// INT config_clear_keys( -/// [in] config_object* conf -/// ); -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int` -- Number of keys removed -LIBSESSION_EXPORT int config_clear_keys(config_object* conf); - -/// API: base/config_remove_key -/// -/// Removes the given encryption/decryption key, if present. Returns true if it was found and -/// removed, false if it was not in the key list. -/// -/// Declaration: -/// ```cpp -/// BOOL config_remove_key( -/// [in] const config_object* conf, -/// [in] const unsigned char* key -/// ), -/// -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -/// -/// Outputs: -/// - `bool` -- True if key successfully removed -LIBSESSION_EXPORT bool config_remove_key(config_object* conf, const unsigned char* key); - -/// API: base/config_key_count -/// -/// Returns the number of encryption keys. -/// -/// Declaration: -/// ```cpp -/// INT config_key_count( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `int` -- Number of encryption keys -LIBSESSION_EXPORT int config_key_count(const config_object* conf); - -/// API: base/config_key_count -/// -/// Returns true if the given key is already in the keys list. -/// -/// Declaration: -/// ```cpp -/// BOOL config_has_key( -/// [in] const config_object* conf, -/// [in] const unsigned char* key -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `key` -- [in] Pointer to the binary key object, must be 32 bytes -/// -/// Outputs: -/// - `bool` -- True if key exists -LIBSESSION_EXPORT bool config_has_key(const config_object* conf, const unsigned char* key); - -/// API: base/config_has_key -/// -/// Returns a pointer to the 32-byte binary key at position i. This is *not* null terminated (and -/// is exactly 32 bytes long). `i < config_key_count(conf)` must be satisfied. Ownership of the -/// data remains in the object (that is: the caller must not attempt to free it). -/// -/// Declaration: -/// ```cpp -/// CONST UNSIGNED CHAR* config_key( -/// [in] const config_object* conf, -/// [in] size_t i -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// - `i` -- [in] Position of key in config object -/// -/// Outputs: -/// - `unsigned char*` -- binary data of the key, exactly 32 bytes and is not null terminated -LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i); - -/// API: base/config_encryption_domain -/// -/// Returns the encryption domain C-str used to encrypt values for this config object. (This is -/// here only for debugging/testing). -/// -/// Declaration: -/// ```cpp -/// CONST CHAR* config_encryption_domain( -/// [in] const config_object* conf -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] Pointer to config_object object -/// -/// Outputs: -/// - `char*` -- encryption domain C-str used to encrypt values -LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf); - -/// API: base/config_set_sig_keys -/// -/// Sets an Ed25519 keypair pair for signing and verifying config messages. When set, this adds an -/// additional signature for verification into the config message (*after* decryption) that -/// validates a config message. -/// -/// This is used in config contexts where the encryption/decryption keys are insufficient for -/// permission verification to produce new messages, such as in groups where non-admins need to be -/// able to decrypt group data, but are not permitted to push new group data. In such a case only -/// the admins have the secret key with which messages can be signed; regular users can only read, -/// but cannot write, config messages. -/// -/// When a signature public key (with or without a secret key) is set the config object enters a -/// "signing-required" mode, which has some implications worth noting: -/// - incoming messages must contain a signature that verifies with the public key; messages -/// without such a signature will be dropped as invalid. -/// - because of the above, a config object cannot push config updates without the secret key: -/// thus any attempt to modify the config message with a pubkey-only config object will raise -/// an exception. -/// -/// Inputs: -/// - `secret` -- pointer to a 64-byte sodium-style Ed25519 "secret key" buffer (technically the -/// seed+precomputed pubkey concatenated together) that sets both the secret key and public key. -LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret); - -/// API: base/config_set_sig_pubkey -/// -/// Sets a Ed25519 signing pubkey which incoming messages must be signed by to be acceptable. This -/// is intended for use when the secret key is not known (see `config_set_sig_keys()` to set both -/// secret and pubkey keys together). -/// -/// Inputs: -/// - `pubkey` -- pointer to the 32-byte Ed25519 pubkey that must have signed incoming messages. -LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey); - -/// API: base/config_get_sig_pubkey -/// -/// Returns a pointer to the 32-byte Ed25519 signing pubkey, if set. Returns nullptr if there is no -/// current signing pubkey. -/// -/// Inputs: none. -/// -/// Outputs: -/// - pointer to the 32-byte pubkey, or NULL if not set. -LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf); - -/// API: base/config_clear_sig_keys -/// -/// Drops the signature pubkey and/or secret key, if the object has them. -/// -/// Inputs: none. -LIBSESSION_EXPORT void config_clear_sig_keys(config_object* conf); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/include/session/config/base.hpp b/include/session/config/base.hpp index 138ba51f..27f9498a 100644 --- a/include/session/config/base.hpp +++ b/include/session/config/base.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,7 +11,6 @@ #include #include -#include "base.h" #include "namespaces.hpp" namespace session::config { @@ -868,6 +868,16 @@ class ConfigBase : public ConfigSig { /// - `std::optional` -- Returns the compression level virtual std::optional compression_level() const { return 1; } + /// API: base/ConfigBase::default_ttl + /// + /// The default duration the config message should last for before it expires. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::chrono::milliseconds` -- Duration the mesage should last for in milliseconds. + virtual std::chrono::milliseconds default_ttl() const { return std::chrono::hours(30 * 24); } + /// API: base/ConfigBase::config_lags /// /// How many config lags should be used for this object; default to 5. Implementing subclasses @@ -1224,6 +1234,15 @@ class ConfigBase : public ConfigSig { assert(i < _keys.size()); return {_keys[i].data(), _keys[i].size()}; } + + /// API: base/ConfigBase::get_seqno + /// + /// Retrieves the current seqno for the config. If there is a pending push then this will return + /// the updated seqno. + /// + /// Outputs: + /// - `seqno_t` -- current seqno + seqno_t get_seqno() const { return _config->seqno(); }; }; // The C++ struct we hold opaquely inside the C internals struct. This is designed so that any @@ -1259,31 +1278,6 @@ struct internals final { const ConfigT& operator*() const { return *operator->(); } }; -template , int> = 0> -inline internals& unbox(config_object* conf) { - return *static_cast*>(conf->internals); -} -template , int> = 0> -inline const internals& unbox(const config_object* conf) { - return *static_cast*>(conf->internals); -} - -// Sets an error message in the internals.error string and updates the last_error pointer in the -// outer (C) config_object struct to point at it. -void set_error(config_object* conf, std::string e); - -// Same as above, but gets the error string out of an exception and passed through a return value. -// Intended to simplify catch-and-return-error such as: -// try { -// whatever(); -// } catch (const std::exception& e) { -// return set_error(conf, LIB_SESSION_ERR_OHNOES, e); -// } -inline int set_error(config_object* conf, int errcode, const std::exception& e) { - set_error(conf, e.what()); - return errcode; -} - // Copies a value contained in a string into a new malloced char buffer, returning the buffer and // size via the two pointer arguments. void copy_out(ustring_view data, unsigned char** out, size_t* outlen); diff --git a/include/session/config/contacts.h b/include/session/config/contacts.h index 7c6bc0e4..1c4640b6 100644 --- a/include/session/config/contacts.h +++ b/include/session/config/contacts.h @@ -4,7 +4,7 @@ extern "C" { #endif -#include "base.h" +#include "../state.h" #include "expiring.h" #include "notify.h" #include "profile_pic.h" @@ -36,129 +36,67 @@ typedef struct contacts_contact { } contacts_contact; -/// API: contacts/contacts_init -/// -/// Constructs a contacts config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT contacts_init( -/// [out] config_object** conf, -/// [in] const unsigned char* ed25519_secretkey, -/// [in] const unsigned char* dump, -/// [in] size_t dumplen, -/// [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. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is 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: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int contacts_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: contacts/contacts_get +/// API: contacts/state_get_contact /// /// Fills `contact` with the contact info given a session ID (specified as a null-terminated hex /// string), if the contact exists, and returns true. If the contact does not exist then `contact` /// is left unchanged and false is returned. /// -/// Declaration: -/// ```cpp -/// BOOL contacts_get( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `contact` -- [out] the contact info data /// - `session_id` -- [in] null terminated hex string +/// - `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. /// /// Output: /// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool contacts_get( - config_object* conf, contacts_contact* contact, const char* session_id) +LIBSESSION_EXPORT bool state_get_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) __attribute__((warn_unused_result)); -/// API: contacts/contacts_get_or_construct +/// API: contacts/state_get_or_construct_contact /// -/// Same as the above `contacts_get()` except that when the contact does not exist, this sets all -/// the contact fields to defaults and loads it with the given session_id. +/// Same as the above `state_get_contact()` except that when the contact does not exist, this sets +/// all the contact fields to defaults and loads it with the given session_id. /// /// Returns true as long as it is given a valid session_id. A false return is considered an error, /// and means the session_id was not a valid session_id. /// /// This is the method that should usually be used to create or update a contact, followed by -/// setting fields in the contact, and then giving it to contacts_set(). -/// -/// Declaration: -/// ```cpp -/// BOOL contacts_get_or_construct( -/// [in] config_object* conf, -/// [out] contacts_contact* contact, -/// [in] const char* session_id -/// ); -/// ``` +/// setting fields in the contact, and then giving it to state_set_contact(). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `contact` -- [out] the contact info data /// - `session_id` -- [in] null terminated hex string +/// - `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. /// /// Output: /// - `bool` -- Returns true if contact exsts -LIBSESSION_EXPORT bool contacts_get_or_construct( - config_object* conf, contacts_contact* contact, const char* session_id) +LIBSESSION_EXPORT bool state_get_or_construct_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) __attribute__((warn_unused_result)); -/// API: contacts/contacts_set +/// API: contacts/state_set_contact /// /// Adds or updates a contact from the given contact info struct. /// -/// Declaration: -/// ```cpp -/// VOID contacts_set( -/// [in, out] config_object* conf, -/// [in] const contacts_contact* contact -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `contact` -- [in] Pointer containing the contact info data -/// -/// Output: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void contacts_set(config_object* conf, const contacts_contact* contact); +LIBSESSION_EXPORT void state_set_contact( + mutable_user_state_object* state, const contacts_contact* contact); // NB: wrappers for set_name, set_nickname, etc. C++ methods are deliberately omitted as they would // save very little in actual calling code. The procedure for updating a single field without them // is simple enough; for example to update `approved` and leave everything else unchanged: // // contacts_contact c; -// if (contacts_get_or_construct(conf, &c, some_session_id)) { +// if (state_get_or_construct_contact(conf, &c, some_session_id)) { // const char* new_nickname = "Joe"; // c.approved = new_nickname; // contacts_set_or_create(conf, &c); @@ -166,45 +104,31 @@ LIBSESSION_EXPORT void contacts_set(config_object* conf, const contacts_contact* // // some_session_id was invalid! // } -/// API: contacts/contacts_erase +/// API: contacts/state_erase_contact /// /// Erases a contact from the contact list. session_id is in hex. Returns true if the contact was /// found and removed, false if the contact was not present. You must not call this during /// iteration; see details below. /// -/// Declaration: -/// ```cpp -/// BOOL contacts_erase( -/// [in, out] config_object* conf, -/// [in] const char* session_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `session_id` -- [in] Text containing null terminated hex string /// /// Outputs: /// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool contacts_erase(config_object* conf, const char* session_id); +LIBSESSION_EXPORT bool state_erase_contact( + mutable_user_state_object* state, const char* session_id); -/// API: contacts/contacts_size +/// API: contacts/state_size_contacts /// /// Returns the number of contacts. /// -/// Declaration: -/// ```cpp -/// SIZE_T contacts_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- input - Pointer to the config object +/// - `state` -- input - Pointer to the state object /// /// Outputs: /// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t contacts_size(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_contacts(const state_object* state); typedef struct contacts_iterator { void* _internals; @@ -217,7 +141,7 @@ typedef struct contacts_iterator { /// Functions for iterating through the entire contact list, in sorted order. Intended use is: /// /// contacts_contact c; -/// contacts_iterator *it = contacts_iterator_new(contacts); +/// contacts_iterator *it = contacts_iterator_new(state); /// for (; !contacts_iterator_done(it, &c); contacts_iterator_advance(it)) { /// // c.session_id, c.nickname, etc. are loaded /// } @@ -225,31 +149,17 @@ typedef struct contacts_iterator { /// /// It is NOT permitted to add/remove/modify records while iterating. /// -/// Declaration: -/// ```cpp -/// CONTACTS_ITERATOR* contacts_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `contacts_iterator*` -- pointer to the iterator -LIBSESSION_EXPORT contacts_iterator* contacts_iterator_new(const config_object* conf); +LIBSESSION_EXPORT contacts_iterator* contacts_iterator_new(const state_object* state); /// API: contacts/contacts_iterator_free /// /// Frees an iterator once no longer needed. /// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_free( -/// [in] contacts_iterator* it -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator LIBSESSION_EXPORT void contacts_iterator_free(contacts_iterator* it); @@ -259,14 +169,6 @@ LIBSESSION_EXPORT void contacts_iterator_free(contacts_iterator* it); /// Returns true if iteration has reached the end. Otherwise `c` is populated and false is /// returned. /// -/// Declaration: -/// ```cpp -/// BOOL contacts_iterator_done( -/// [in] contacts_iterator* it, -/// [out] contacts_contact* c -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator /// - `c` -- [out] Pointer to the contact, will be populated if false @@ -279,13 +181,6 @@ LIBSESSION_EXPORT bool contacts_iterator_done(contacts_iterator* it, contacts_co /// /// Advances the iterator. /// -/// Declaration: -/// ```cpp -/// VOID contacts_iterator_advance( -/// [in] contacts_iterator* it -/// ); -/// ``` -/// /// Inputs: /// - `it` -- [in] Pointer to the contacts_iterator LIBSESSION_EXPORT void contacts_iterator_advance(contacts_iterator* it); diff --git a/include/session/config/convo_info_volatile.h b/include/session/config/convo_info_volatile.h index 6ddba3eb..ca2aed8b 100644 --- a/include/session/config/convo_info_volatile.h +++ b/include/session/config/convo_info_volatile.h @@ -4,7 +4,7 @@ extern "C" { #endif -#include "base.h" +#include "../state.h" #include "profile_pic.h" typedef struct convo_info_volatile_1to1 { @@ -38,111 +38,59 @@ typedef struct convo_info_volatile_legacy_group { bool unread; // true if marked unread } convo_info_volatile_legacy_group; -/// API: convo_info_volatile/convo_info_volatile_init -/// -/// Constructs a conversations config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT convo_info_volatile_init( -/// [out] config_object** conf, -/// [in] unsigned char* ed25519_secretkey, -/// [in, optional] unsigned char* dump, -/// [in, optional] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `ed25519_secretkey` -- [out] 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. -/// -/// - `dump` -- [in, optional] if non-NULL this restores the state from the dumped byte string -/// produced by a past instantiation's call to `dump()`. To construct a new, empty object this -/// should be NULL. -/// -/// - `dumplen` -- [in, optional] the length of `dump` when restoring from a dump, or 0 when `dump` -/// is 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: -/// - `int` --Returns 0 on success; returns a non-zero error code and write the exception message as -/// a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int convo_info_volatile_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: convo_info_volatile/convo_info_volatile_get_1to1 +/// API: convo_info_volatile/state_get_convo_info_volatile_1to1 /// /// Fills `convo` with the conversation info given a 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_1to1( -/// [in] config_object* conf, -/// [out] convo_info_volatile_1to1* convo, -/// [in] const char* session_id -/// ); -/// ``` +/// the error buffer 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, the error buffer will not be set). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the costatenfig object /// - `convo` -- [out] Pointer to conversation info /// - `session_id` -- [in] Null terminated hex string of the session_id +/// - `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 if the conversation exists -LIBSESSION_EXPORT bool convo_info_volatile_get_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_1to1 +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_1to1 /// -/// Same as the above convo_info_volatile_get_1to1 except that when the conversation does not exist, -/// this sets all the convo fields to defaults and loads it with the given session_id. +/// Same as the above state_get_convo_info_volatile_1to1 except that when the conversation does not +/// exist, this sets all the convo fields to defaults and loads it with the given session_id. /// /// Returns true as long as it is given a valid session_id. A false return is considered an error, -/// and means the session_id was not a valid session_id. In such a case `conf->last_error` will be +/// and means the session_id was not a valid session_id. In such a case the error buffer 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_1to1* convo, -/// [in] const char* session_id -/// ); -/// ``` +/// setting fields in the convo, and then giving it to state_set_convo_info_volatile(). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to conversation info /// - `session_id` -- [in] Null terminated hex string of the session_id +/// - `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 if the conversation exists -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_community +/// API: convo_info_volatile/state_get_convo_info_volatile_community /// /// community versions of the 1-to-1 functions: /// @@ -151,31 +99,25 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_1to1( /// /// Error handling works the same as the 1-to-1 version. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_community( -/// [in] config_object* conf, -/// [out] convo_info_volatile_community* comm, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `comm` -- [out] Pointer to community info structure /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string +/// - `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 if the community exists -LIBSESSION_EXPORT bool convo_info_volatile_get_community( - config_object* conf, +LIBSESSION_EXPORT bool state_get_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* comm, const char* base_url, - const char* room) __attribute__((warn_unused_result)); + const char* room, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_community +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_community /// /// Gets a community convo info, but if the community does not exist will set all the fileds to /// defaults and load it. `base_url` and `room` are null-terminated c strings; pubkey is 32 bytes. @@ -193,404 +135,278 @@ LIBSESSION_EXPORT bool convo_info_volatile_get_community( /// /// Error handling works the same as the 1-to-1 version. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_or_construct_community( -/// [in] config_object* conf, -/// [out] convo_info_volatile_community* comm, -/// [in] const char* base_url, -/// [in] const char* room, -/// [in] unsigned const char* pubkey -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to community info structure /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string /// - `pubkey` -- [in] 32 byte binary data of the pubkey +/// - `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 if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_community( - config_object* conf, +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* convo, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_group +/// API: convo_info_volatile/state_get_convo_info_volatile_group /// /// Fills `convo` with the conversation info given a group 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. On error, false is returned and the error -/// is set in conf->last_error (on non-error, last_error is cleared). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_group* convo, -/// [in] const char* id -/// ); -/// ``` +/// then `convo` is left unchanged and false is returned. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to group /// - `id` -- [in] Null terminated hex string (66 chars, beginning with 03) specifying the ID of the /// group +/// - `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 if the group exists -LIBSESSION_EXPORT bool convo_info_volatile_get_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) +LIBSESSION_EXPORT bool state_get_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_group +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_group /// /// Same as the above except that when the conversation does not exist, this sets all the convo /// fields to defaults and loads it with the given id. /// /// Returns true as long as it is given a valid group id (i.e. 66 hex chars beginning with "03"). A /// false return is considered an error, and means the id was not a valid session id; an error -/// string will be set in `conf->last_error`. +/// string will be set in the error buffer. /// /// 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_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_group* convo, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to group /// - `id` -- [in] Null terminated hex string specifying the ID of the group +/// - `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 if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_legacy_group +/// API: convo_info_volatile/state_get_convo_info_volatile_legacy_group /// /// Fills `convo` with the conversation info given a legacy group 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. On error, false is returned and the error -/// is set in conf->last_error (on non-error, last_error is cleared). -/// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_get_legacy_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_legacy_group* convo, -/// [in] const char* id -/// ); -/// ``` +/// is set in the error buffer. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to legacy group /// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group +/// - `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 if the legacy group exists -LIBSESSION_EXPORT bool convo_info_volatile_get_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_get_or_construct_legacy_group +/// API: convo_info_volatile/state_get_or_construct_convo_info_volatile_legacy_group /// /// Same as the above except that when the conversation does not exist, this sets all the convo /// fields to defaults and loads it with the given id. /// /// Returns true as long as it is given a valid legacy group id (i.e. same format as a session id). /// A false return is considered an error, and means the id was not a valid session id; an error -/// string will be set in `conf->last_error`. +/// string will be set in the error buffer. /// /// 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_legacy_group( -/// [in] config_object* conf, -/// [out] convo_info_volatile_legacy_group* convo, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `convo` -- [out] Pointer to legacy group /// - `id` -- [in] Null terminated hex string specifying the ID of the legacy group +/// - `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 if the call succeeds -LIBSESSION_EXPORT bool convo_info_volatile_get_or_construct_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) __attribute__((warn_unused_result)); -/// API: convo_info_volatile/convo_info_volatile_set_1to1 +/// API: convo_info_volatile/state_set_convo_info_volatile_1to1 /// /// Adds or updates a conversation from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_1to1( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_1to1* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to conversation info structure -LIBSESSION_EXPORT void convo_info_volatile_set_1to1( - config_object* conf, const convo_info_volatile_1to1* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_1to1( + mutable_user_state_object* state, const convo_info_volatile_1to1* convo); -/// API: convo_info_volatile/convo_info_volatile_set_community +/// API: convo_info_volatile/state_set_convo_info_volatile_community /// /// Adds or updates a community from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_community( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_community* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to community info structure -LIBSESSION_EXPORT void convo_info_volatile_set_community( - config_object* conf, const convo_info_volatile_community* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_community( + mutable_user_state_object* state, const convo_info_volatile_community* convo); -/// API: convo_info_volatile/convo_info_volatile_set_group +/// API: convo_info_volatile/state_set_convo_info_volatile_group /// /// Adds or updates a group from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_group( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_group* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_group( - config_object* conf, const convo_info_volatile_group* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_group( + mutable_user_state_object* state, const convo_info_volatile_group* convo); -/// API: convo_info_volatile/convo_info_volatile_set_legacy_group +/// API: convo_info_volatile/state_set_convo_info_volatile_legacy_group /// /// Adds or updates a legacy group from the given convo info /// -/// Declaration: -/// ```cpp -/// VOID convo_info_volatile_set_legacy_group( -/// [in] config_object* conf, -/// [in] const convo_info_volatile_legacy_group* convo -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to legacy group info structure -LIBSESSION_EXPORT void convo_info_volatile_set_legacy_group( - config_object* conf, const convo_info_volatile_legacy_group* convo); +LIBSESSION_EXPORT void state_set_convo_info_volatile_legacy_group( + mutable_user_state_object* state, const convo_info_volatile_legacy_group* convo); -/// API: convo_info_volatile/convo_info_volatile_erase_1to1 +/// API: convo_info_volatile/state_erase_convo_info_volatile_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_1to1( -/// [in] config_object* conf, -/// [in] const char* session_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `convo` -- [in] Pointer to community info structure /// /// Outputs: /// - `bool` - Returns true if conversation was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_1to1( + mutable_user_state_object* state, const char* session_id); -/// API: convo_info_volatile/convo_info_volatile_erase_community +/// API: convo_info_volatile/state_erase_convo_info_volatile_community /// /// Erases a community. Returns true if the community was found /// and removed, false if the community was not present. You must not call this during /// iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_community( -/// [in] config_object* conf, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `base_url` -- [in] Null terminated string /// - `room` -- [in] Null terminated string /// /// Outputs: /// - `bool` - Returns true if community was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_community( - config_object* conf, const char* base_url, const char* room); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_community( + mutable_user_state_object* state, const char* base_url, const char* room); -/// API: convo_info_volatile/convo_info_volatile_erase_group +/// API: convo_info_volatile/state_erase_convo_info_volatile_group /// /// Erases a group. Returns true if the group was found and removed, false if the group was not /// present. You must not call this during iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `group_id` -- [in] Null terminated hex string /// /// Outputs: /// - `bool` - Returns true if group was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_group( + mutable_user_state_object* state, const char* group_id); -/// API: convo_info_volatile/convo_info_volatile_erase_legacy_group +/// API: convo_info_volatile/state_erase_convo_info_volatile_legacy_group /// /// Erases a legacy group. Returns true if the group was found /// and removed, false if the group was not present. You must not call this during /// iteration. /// -/// Declaration: -/// ```cpp -/// BOOL convo_info_volatile_erase_legacy_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `group_id` -- [in] Null terminated hex string /// /// Outputs: /// - `bool` - Returns true if group was found and removed -LIBSESSION_EXPORT bool convo_info_volatile_erase_legacy_group( - config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_convo_info_volatile_legacy_group( + mutable_user_state_object* state, const char* group_id); -/// API: convo_info_volatile/convo_info_volatile_size +/// API: convo_info_volatile/state_size_convo_info_volatile /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of conversations -LIBSESSION_EXPORT size_t convo_info_volatile_size(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_1to1 +/// API: convo_info_volatile/state_convo_info_volatile_1to1 /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_1to1( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of conversations -LIBSESSION_EXPORT size_t convo_info_volatile_size_1to1(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_1to1(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_communities +/// API: convo_info_volatile/state_size_convo_info_volatile_communities /// /// Returns the number of communitites. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of communities -LIBSESSION_EXPORT size_t convo_info_volatile_size_communities(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_communities(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_groups +/// API: convo_info_volatile/state_size_convo_info_volatile_groups /// /// Returns the number of groups. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of groups -LIBSESSION_EXPORT size_t convo_info_volatile_size_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_groups(const state_object* state); -/// API: convo_info_volatile/convo_info_volatile_size_legacy_groups +/// API: convo_info_volatile/state_size_convo_info_volatile_legacy_groups /// /// Returns the number of legacy groups. /// -/// Declaration: -/// ```cpp -/// SIZE_T convo_info_volatile_size_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `size_t` -- number of legacy groups -LIBSESSION_EXPORT size_t convo_info_volatile_size_legacy_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_convo_info_volatile_legacy_groups(const state_object* state); typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; @@ -622,20 +438,13 @@ typedef struct convo_info_volatile_iterator convo_info_volatile_iterator; /// It is NOT permitted to add/modify/remove records while iterating; instead you must use two /// loops: a first one to identify changes, and a second to apply them. /// -/// Declaration: -/// ```cpp -/// CONVO_INFO_VOLATILE_ITERATOR* convo_info_volatile_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_1to1 /// @@ -645,20 +454,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// 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_1to1( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_communities /// @@ -668,20 +470,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// 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_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_groups /// @@ -691,20 +486,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// 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_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_new_legacy_groups /// @@ -714,20 +502,13 @@ LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new /// 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_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `convo_info_volatile_iterator*` -- Iterator LIBSESSION_EXPORT convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( - const config_object* conf); + const state_object* state); /// API: convo_info_volatile/convo_info_volatile_iterator_free /// diff --git a/include/session/config/groups/info.h b/include/session/config/groups/info.h index 32da3ae9..f2631d36 100644 --- a/include/session/config/groups/info.h +++ b/include/session/config/groups/info.h @@ -4,233 +4,227 @@ extern "C" { #endif -#include "../base.h" +#include "../../state.h" #include "../profile_pic.h" #include "../util.h" LIBSESSION_EXPORT extern const size_t GROUP_INFO_NAME_MAX_LENGTH; LIBSESSION_EXPORT extern const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH; -/// API: groups/groups_info_init -/// -/// Constructs a group info config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Inputs: -/// - `conf` -- [out] Pointer to the config object -/// - `ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is 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: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_info_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups_info/groups_info_get_name +/// API: groups_info/state_get_group_name /// /// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at /// all. Should be copied right away as the pointer may not remain valid beyond other API calls. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `name` -- [out] the pointer to a buffer in which we will write the null-terminated name +/// string. This must be a +/// buffer of at least 'GROUP_INFO_NAME_MAX_LENGTH' bytes. /// /// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_EXPORT const char* groups_info_get_name(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group name +LIBSESSION_EXPORT bool state_get_group_name( + const state_object* state, const char* group_id, char* name); -/// API: groups_info/groups_info_set_name +/// API: groups_info/state_set_group_name /// /// Sets the group's name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). +/// error (and sets the state_object's error string). /// /// If the given name is longer than GROUP_INFO_NAME_MAX_LENGTH (100) bytes then it will be /// truncated. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_name(config_object* conf, const char* name); +LIBSESSION_EXPORT void state_set_group_name(mutable_group_state_object* state, const char* name); -/// API: groups_info/groups_info_get_description +/// API: groups_info/state_get_group_description /// /// Returns a pointer to the currently-set description (null-terminated), or NULL if there is no /// description at all. Should be copied right away as the pointer may not remain valid beyond /// other API calls. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `description` -- [out] the pointer to a buffer in which we will write the null-terminated +/// description string. This must be a +/// buffer of at least 'GROUP_INFO_DESCRIPTION_MAX_LENGTH' bytes. /// /// Outputs: -/// - `char*` -- Pointer to the currently-set description as a null-terminated string, or NULL if -/// there is no description -LIBSESSION_EXPORT const char* groups_info_get_description(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group description +LIBSESSION_EXPORT bool state_get_group_description( + const state_object* state, const char* group_id, char* description); -/// API: groups_info/groups_info_set_description +/// API: groups_info/state_set_group_description /// /// Sets the group's description to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). +/// error (and sets the state_object's error string). /// /// If the given description is longer than GROUP_INFO_DESCRIPTION_MAX_LENGTH (2000) bytes then it /// will be truncated. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `description` -- [in] Pointer to the description as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_description(config_object* conf, const char* description); +LIBSESSION_EXPORT void state_set_group_description( + mutable_group_state_object* state, const char* description); -/// API: groups_info/groups_info_get_pic +/// API: groups_info/state_get_group_pic /// /// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile /// pic is not currently set, and otherwise should be copied right away (they will not be valid /// beyond other API calls on this config object). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `description` -- [out] the pointer that will be set to the current profile pic (despite the +/// "user_profile" in +/// the struct name, this is the group's profile pic). /// /// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic (despite the "user_profile" in -/// the struct name, this is the group's profile pic). -LIBSESSION_EXPORT user_profile_pic groups_info_get_pic(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group profile pic +LIBSESSION_EXPORT bool state_get_group_pic( + const state_object* state, const char* group_id, user_profile_pic* pic); -/// API: groups_info/groups_info_set_pic +/// API: groups_info/state_set_group_pic /// /// Sets a user profile /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int groups_info_set_pic(config_object* conf, user_profile_pic pic); +LIBSESSION_EXPORT void state_set_group_pic(mutable_group_state_object* state, user_profile_pic pic); -/// API: groups_info/groups_info_get_expiry_timer +/// API: groups_info/state_get_group_expiry_timer /// /// Gets the group's message expiry timer (seconds). Returns 0 if not set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `timer` -- [out] Pointer that will be set to the expiry timer in seconds. /// /// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_EXPORT int groups_info_get_expiry_timer(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group expiry timer +LIBSESSION_EXPORT bool state_get_group_expiry_timer( + const state_object* state, const char* group_id, int* timer); -/// API: groups_info/groups_info_set_expiry_timer +/// API: groups_info/state_set_group_expiry_timer /// /// Sets the group's message expiry timer (seconds). Setting 0 (or negative) will clear the current /// timer. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void groups_info_set_expiry_timer(config_object* conf, int expiry); +LIBSESSION_EXPORT void state_set_group_expiry_timer(mutable_group_state_object* state, int expiry); -/// API: groups_info/groups_info_get_created +/// API: groups_info/state_get_group_created /// /// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `created` -- [out] Pointer that will be set to the unix timestamp when the group was created +/// (if set by an admin). /// /// Outputs: -/// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). -LIBSESSION_EXPORT int64_t groups_info_get_created(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group created +/// timestamp +LIBSESSION_EXPORT bool state_get_group_created( + const state_object* state, const char* group_id, int64_t* created); -/// API: groups_info/groups_info_set_created +/// API: groups_info/state_set_group_created /// /// Sets the creation time (unix timestamp, in seconds) when the group was created. Setting 0 /// clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_created(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void state_set_group_created(mutable_group_state_object* state, int64_t ts); -/// API: groups_info/groups_info_get_delete_before +/// API: groups_info/state_get_group_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds); clients should delete all messages /// from the group with timestamps earlier than this value, if set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which messages +/// should be deleted. Returns 0 if not set. /// /// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_EXPORT int64_t groups_info_get_delete_before(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted +/// before value +LIBSESSION_EXPORT bool state_get_group_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before); -/// API: groups_info/groups_info_set_delete_before +/// API: groups_info/state_set_group_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) before which messages should be /// deleted. Setting 0 clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_delete_before(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void state_set_group_delete_before(mutable_group_state_object* state, int64_t ts); -/// API: groups_info/groups_info_get_attach_delete_before +/// API: groups_info/state_get_group_attach_delete_before /// /// Returns the delete-before timestamp (unix time, in seconds) for attachments; clients should drop /// all attachments from messages from the group with timestamps earlier than this value, if set. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `delete_before` -- [out] Pointer that will be set to the unix timestamp before which message +/// attachments should be deleted. Returns 0 if not set. /// /// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_EXPORT int64_t groups_info_get_attach_delete_before(const config_object* conf); +/// - `bool` -- Flag indicating whether it was able to successfully retrieve the group deleted +/// before value +LIBSESSION_EXPORT bool state_get_group_attach_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before); -/// API: groups_info/groups_info_set_attach_delete_before +/// API: groups_info/state_set_group_attach_delete_before /// /// Sets the delete-before time (unix timestamp, in seconds) for attachments; attachments should be /// dropped from messages older than this value. Setting 0 clears the value. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_EXPORT void groups_info_set_attach_delete_before(config_object* conf, int64_t ts); +LIBSESSION_EXPORT void state_set_group_attach_delete_before( + mutable_group_state_object* state, int64_t ts); -/// API: groups_info/groups_info_is_destroyed(const config_object* conf); +/// API: groups_info/state_group_is_destroyed /// /// Returns true if this group has been marked destroyed by an admin, which indicates to a receiving /// client that they should destroy it locally. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `true` if the group has been nuked, `false` otherwise. -LIBSESSION_EXPORT bool groups_info_is_destroyed(const config_object* conf); +LIBSESSION_EXPORT bool state_group_is_destroyed(const state_object* state, const char* group_id); -/// API: groups_info/groups_info_destroy_group(const config_object* conf); +/// API: groups_info/state_destroy_group /// /// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object -LIBSESSION_EXPORT void groups_info_destroy_group(config_object* conf); +/// - `state` -- [in] Pointer to the mutable state object +LIBSESSION_EXPORT void state_destroy_group(mutable_group_state_object* state); #ifdef __cplusplus } // extern "C" diff --git a/include/session/config/groups/keys.h b/include/session/config/groups/keys.h index 9dab2ac7..d8debc3a 100644 --- a/include/session/config/groups/keys.h +++ b/include/session/config/groups/keys.h @@ -4,84 +4,24 @@ extern "C" { #endif -#include "../base.h" +#include "../../state.h" #include "../util.h" -// This is an opaque type analagous to `config_object` but specific to the groups keys object. -// -// It is constructed via groups_keys_init and destructed via groups_keys_free. -typedef struct config_group_keys { - // Internal opaque object pointer; calling code should leave this alone. - void* internals; - - // When an error occurs in the C API this string will be set to the specific error message. May - // be empty. - const char* last_error; - - // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. - char _error_buf[256]; - -} config_group_keys; - -/// API: groups/groups_keys_init -/// -/// Constructs a group keys management config object and sets a pointer to it in `conf`. -/// -/// Note that this is *not* a regular `config_object` and thus does not use the usual -/// `config_free()` and similar methods from `session/config/base.h`; instead it must be managed by -/// the functions declared in the header. -/// -/// Inputs: -/// - `conf` -- [out] Pointer-pointer to a `config_group_keys` pointer (i.e. double pointer); the -/// pointer will be set to a new config_group_keys object on success. -/// -/// Intended use: -/// -/// ```C -/// config_group_keys* keys; -/// int rc = groups_keys_init(&keys, ...); -/// ``` -/// - `user_ed25519_secretkey` -- [in] 64-byte pointer to the **user**'s (not group's) secret -/// ed25519 key. (Used to be able to decrypt keys encrypted individually for us). -/// - `group_ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `group_ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `group_info_conf` -- the group info config instance (keys will be added) -/// - `group_members_conf` -- the group members config instance (keys will be added) -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is 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: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_keys_init( - config_group_keys** conf, - const unsigned char* user_ed25519_secretkey, - const unsigned char* group_ed25519_pubkey, - const unsigned char* group_ed25519_secretkey, - config_object* group_info_conf, - config_object* group_members_conf, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_size +/// API: groups/state_size_group_keys /// /// Returns the number of decryption keys stored in this Keys object. Mainly for /// debugging/information purposes. /// /// Inputs: -/// - `conf` -- keys config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: -/// - `size_t` number of keys -LIBSESSION_EXPORT size_t groups_keys_size(const config_group_keys* conf); +/// - `size_t` -- number of members in the group (will be 0 if the group doesn't exist or the +/// 'pubkey_hex' is invalid) +LIBSESSION_EXPORT size_t state_size_group_keys(const state_object* state, const char* group_id); -/// API: groups/groups_keys_get_key +/// API: groups/state_get_group_key /// /// Accesses the Nth encryption key, ordered from most-to-least recent starting from index 0. /// Calling this with 0 thus returns the most-current key (which is also the current _en_cryption @@ -101,141 +41,29 @@ LIBSESSION_EXPORT size_t groups_keys_size(const config_group_keys* conf); /// freed. /// /// Inputs: -/// - `conf` -- keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `N` -- the index of the key to obtain /// /// Outputs: -/// - `const unsigned char*` -- pointer to the 32-byte key, or nullptr if there -LIBSESSION_EXPORT const unsigned char* groups_keys_get_key(const config_group_keys* conf, size_t N); - -/// API: groups/groups_keys_is_admin +/// - `const unsigned char*` -- pointer to the 32-byte key, or nullptr if thereis no group or key +LIBSESSION_EXPORT const unsigned char* state_get_group_key( + const state_object* state, const char* group_id, size_t N); +// +/// API: groups/state_is_group_admin /// /// Returns true if this object has the group private keys, i.e. the user is an all-powerful /// wiz^H^H^Hadmin of the group. /// /// Inputs: -/// - `conf` -- the groups config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `true` if we have admin keys, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_is_admin(const config_group_keys* conf); - -/// API: groups/groups_keys_load_admin_key -/// -/// Loads the admin keys, effectively upgrading this keys object from a member to an admin. -/// -/// This does nothing if the keys object already has admin keys. -/// -/// Inputs: -/// - `conf` -- the groups keys config object -/// - `secret` -- pointer to the 32-byte group seed. (This a 64-byte libsodium "secret key" begins -/// with the seed, this can also be a given a pointer to such a value). -/// - `group_info_conf` -- the group info config instance (the key will be added) -/// - `group_members_conf` -- the group members config instance (the key will be added) -/// -/// Outputs: -/// - `true` if the object has been upgraded to admin status, or was already admin status; `false` -/// if the given seed value does not match the group's public key. If this returns `true` then -/// after the call a call to `groups_keys_is_admin` would also return `true`. -LIBSESSION_EXPORT bool groups_keys_load_admin_key( - config_group_keys* conf, - const unsigned char* secret, - config_object* group_info_conf, - config_object* group_members_conf); - -/// API: groups/groups_keys_rekey -/// -/// Generates a new encryption key for the group and returns an encrypted key message to be pushed -/// to the swarm containing the key, encrypted for the members of the group. -/// -/// The returned binary key message to be pushed is written into a newly-allocated buffer. A -/// pointer to this buffer is set in the pointer-pointer `out` argument, and its length is set in -/// the `outlen` pointer. -/// -/// See Keys::rekey in the C++ API for more details about intended use. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `info` -- [in] Pointer to group Info object -/// - `members` -- [in] Pointer to group Members object -/// - `out` -- [out] Will be set to a pointer to the message to be pushed (only if the function -/// returns true). This value must be used immediately (it is not guaranteed to remain valid -/// beyond other calls to the config object), and must not be freed (i.e. ownership remains with -/// the keys config object). -/// - `outlen` -- [out] Length of the output value. Only set when the function returns true. -/// -/// Output: -/// - `bool` -- Returns true on success, false on failure. -LIBSESSION_EXPORT bool groups_keys_rekey( - config_group_keys* conf, - config_object* info, - config_object* members, - const unsigned char** out, - size_t* outlen) __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_pending_config -/// -/// If a `rekey()` is currently in progress (and not yet confirmed, or possibly lost), this returns -/// the config message that should be pushed. As with the result of `rekey()` the pointer ownership -/// remains with the keys config object, and the value should be used/copied immediately. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `out` -- [out] Pointer-pointer that will be updated to point at the config data. Only set if -/// this function returns true! -/// - `outlen` -- [out] Pointer to the config data size (only set if the function returns true). -/// -/// Outputs: -/// - `bool` -- true if `out` and `outlen` have been updated to point to a pending config message; -/// false if there is no pending config message. -LIBSESSION_EXPORT bool groups_keys_pending_config( - const config_group_keys* conf, const unsigned char** out, size_t* outlen) - __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_load_message -/// -/// Loads a key config message downloaded from the swarm, and loads the key into the info/member -/// configs. -/// -/// Such messages should be processed via this method *before* attempting to load config messages -/// downloaded from an info/members namespace. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `msg_hash` -- [in] Null-terminated C string containing the message hash -/// - `data` -- [in] Pointer to the incoming key config message -/// - `datalen` -- [in] length of `data` -/// - `timestamp_ms` -- [in] the timestamp (from the swarm) of the message -/// - `info` -- [in] the info config object to update with newly discovered keys -/// - `members` -- [in] the members config object to update with newly discovered keys -/// -/// Outputs: -/// Returns `true` if the message was parsed successfully (whether or not any new keys were -/// decrypted or loaded). Returns `false` on failure to parse (and sets `conf->last_error`). -LIBSESSION_EXPORT bool groups_keys_load_message( - config_group_keys* conf, - const char* msg_hash, - const unsigned char* data, - size_t datalen, - int64_t timestamp_ms, - config_object* info, - config_object* members) __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_is_group_admin(const state_object* state, const char* group_id); -/// API: groups/groups_keys_current_hashes -/// -/// Returns the hashes of currently active keys messages, that is, messages that have a decryption -/// key that new devices or clients might require; these are the messages that should have their -/// expiries renewed periodically. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the keys config object -/// -/// Outputs: -/// - `config_string_list*` -- pointer to an array of message hashes. The returned pointer belongs -/// to the caller and must be free()d when done. -LIBSESSION_EXPORT config_string_list* groups_keys_current_hashes(const config_group_keys* conf); - -/// API: groups/groups_keys_needs_rekey +/// API: groups/state_group_needs_rekey /// /// Checks whether a rekey is required (for instance, because of key generation conflict). Note /// that this is *not* a check for when members changed (such rekeys are up to the caller to @@ -244,96 +72,52 @@ LIBSESSION_EXPORT config_string_list* groups_keys_current_hashes(const config_gr /// See the C++ Keys::needs_rekey and Keys::rekey descriptions for more details. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `bool` -- `true` if `rekey()` needs to be called, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_needs_rekey(const config_group_keys* conf) - __attribute__((warn_unused_result)); - -/// API: groups/groups_keys_needs_dump -/// -/// Checks whether a groups_keys_dump needs to be called to save state. This is analagous to -/// config_dump, but specific for the group keys object. The value becomes false as soon as -/// `groups_keys_dump` is called, and remains false until the object's state is mutated (e.g. by -/// rekeying or loading new config messages). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `bool` -- `true` if a dump is needed, `false` otherwise. -LIBSESSION_EXPORT bool groups_keys_needs_dump(const config_group_keys* conf) +LIBSESSION_EXPORT bool state_group_needs_rekey(const state_object* state, const char* group_id) __attribute__((warn_unused_result)); -/// API: groups/groups_keys_dump +/// API: groups/state_rekey_group /// -/// Produces a dump of the keys object state to be stored by the application to later restore the -/// object by passing the dump into the constructor. This is analagous to config_dump, but specific -/// for the group keys object. +/// Generates a new encryption key for the group containing the key, encrypted for the members of +/// the group. This function should be used after modify group members when mutating a group to +/// ensure the updated keys include the changes. /// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `out` -- [out] Pointer-pointer to a data buffer; this will be set to a newly malloc'd pointer -/// containing the dump data. The caller is responsible for freeing the data when done! -/// - `outlen` -- [out] Pointer to a size_t where the length of `out` will be stored. -LIBSESSION_EXPORT void groups_keys_dump( - config_group_keys* conf, unsigned char** out, size_t* outlen); - -/// API: groups/groups_keys_key_supplement -/// -/// Generates a supplemental key message for one or more session IDs. This is used to distribute -/// existing active keys to a new member so that that member can access existing keys, configs, and -/// messages. Only admins can call this. -/// -/// The recommended order of operations for adding such a member is: -/// - add the member to Members -/// - generate the key supplement -/// - push new members & key supplement (ideally in a batch) -/// - send invite details, auth signature, etc. to the new user -/// -/// To add a member *without* giving them access to old messages you would use groups_keys_rekey() -/// instead of this method. +/// See Keys::rekey in the C++ API for more details about intended use. /// /// Inputs: -/// - `conf` -- pointer to the keys config object -/// - `sids` -- array of session IDs of the members to generate a supplemental key for; each element -/// must be an ordinary (null-terminated) C string containing the 66-character session id. -/// - `sids_len` -- length of the `sids` array -/// - `message` -- pointer-pointer that will be set to a newly allocated buffer containing the -/// message that should be sent to the swarm. The caller must free() the pointer when finished to -/// not leak the message memory (but only if the function returns true). -/// - `message_len` -- pointer to a `size_t` that will be set to the length of the `message` buffer. +/// - `state` -- Pointer to the mutable state object /// -/// Oututs: -/// - `true` and sets `*message` and `*message_len` on success; returns `false` and does not set -/// them on failure. -LIBSESSION_EXPORT bool groups_keys_key_supplement( - config_group_keys* conf, - const char** sids, - size_t sids_len, - unsigned char** message, - size_t* message_len); +/// Output: +/// - `bool` -- Returns true on success, false on failure. +LIBSESSION_EXPORT bool state_rekey_group(mutable_group_state_object* state) + __attribute__((warn_unused_result)); -/// API: groups/groups_keys_current_generation +/// API: groups/state_get_current_group_generation /// /// Returns the current generation number for the latest keys message. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Oututs: -/// - `int` -- latest keys generation number -LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); +/// - `int` -- latest keys generation number, returns 0 if there is no group or the key is invalid +LIBSESSION_EXPORT int state_get_current_group_generation( + const state_object* state, const char* group_id); -/// API: groups/groups_keys_swarm_make_subaccount +/// API: groups/state_make_group_swarm_subaccount /// /// Constructs a swarm subaccount signing value that a member can use to access messages in the /// swarm. The member will have read and write access, but not delete access. Requires group /// admins keys. /// /// Inputs: -/// - `conf` -- the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `sign_value` -- [out] pointer to a 100 byte (or larger) buffer where the 100 byte signing /// value will be written. This is the value that should be sent to a member to allow @@ -342,17 +126,21 @@ LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf); /// Outputs: /// - `true` -- if making the subaccount succeeds, false if it fails (e.g. because of an invalid /// session id, or not being an admin). If a failure occurs, sign_value will not be written to. -LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount( - config_group_keys* conf, const char* session_id, unsigned char* sign_value); +LIBSESSION_EXPORT bool state_make_group_swarm_subaccount( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* sign_value, + char* error); -/// API: groups/groups_keys_swarm_make_subaccount_flags +/// API: groups/state_make_group_swarm_subaccount_flags /// -/// Same as groups_keys_swarm_make_subaccount, but lets you specify whether the write/del flags are +/// Same as state_make_group_swarm_subaccount, but lets you specify whether the write/del flags are /// present. /// -/// /// Inputs: -/// - `conf` -- the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the member session id (hex c string) /// - `write` -- if true then the member shall be allowed to submit messages into the group account /// of the swarm and extend (but not shorten) the expiry of messages in the group account. If @@ -362,17 +150,22 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount( /// without having the full admin group keys. Typically this is false. /// - `sign_value` -- pointer to a buffer with at least 100 bytes where the 100 byte signing value /// will be written. +/// - `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` - same as groups_keys_swarm_make_subaccount -LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( - config_group_keys* conf, +/// - `bool` - same as state_make_group_swarm_subaccount +LIBSESSION_EXPORT bool state_make_group_swarm_subaccount_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* sign_value); + unsigned char* sign_value, + char* error); -/// API: groups/groups_keys_swarm_verify_subaccount +/// API: groups/verify_group_swarm_subaccount /// /// Verifies that a received subaccount signing value (allegedly produced by /// groups_keys_swarm_make_subaccount) is a valid subaccount signing value for the given group @@ -384,9 +177,12 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( /// Inputs: /// - note that this function does *not* take a config object as it is intended for use to validate /// an invitation before constructing the keys config objects. -/// - `groupid` -- the group id/pubkey, in hex, beginning with "03". +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_ed25519_secretkey` -- the user's Session ID secret key (64 bytes). /// - `signing_value` -- the 100-byte subaccount signing value to validate +/// - `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. /// /// The key will require read and write access to be acceptable. (See the _flags version if you /// need something else). @@ -395,36 +191,36 @@ LIBSESSION_EXPORT bool groups_keys_swarm_make_subaccount_flags( /// - `true` if `signing_value` is a valid subaccount signing value for `groupid` with (at least) /// read and write permissions, `false` if the signing value does not validate or does not meet /// the requirements. -LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount( +LIBSESSION_EXPORT bool verify_group_swarm_subaccount( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value); -/// API: groups/groups_keys_swarm_verify_subaccount_flags +/// API: groups/verify_group_swarm_subaccount_flags /// -/// Same as groups_keys_swarm_verify_subaccount, except that you can specify whether you want to +/// Same as verify_group_swarm_subaccount, except that you can specify whether you want to /// require the write and or delete flags. /// /// Inputs: -/// - same as groups_keys_swarm_verify_subaccount +/// - same as verify_group_swarm_subaccount /// - `write` -- if true, require that the signing_value has write permission (i.e. that the /// user will be allowed to post messages). /// - `del` -- if true, required that the signing_value has delete permissions (i.e. that the /// user will be allowed to remove storage messages from the group's swarm). Note that this /// permission is about forcible swarm message deletion, and has no effect on an ability to /// submit a deletion meta-message to the group (which only requires writing a message). -LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( +LIBSESSION_EXPORT bool verify_group_swarm_subaccount_flags( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value, bool write, bool del); -/// API: groups/groups_keys_swarm_subaccount_sign +/// API: groups/state_sign_group_swarm_subaccount /// /// This helper function generates the required signature for swarm subaccount authentication, /// given the user's keys and swarm auth keys (as provided by an admin, produced via -/// `groups_keys_swarm_make_subaccount`). +/// `state_make_group_swarm_subaccount`). /// /// Storage server subaccount authentication requires passing the three values in the returned /// struct in the storage server request. @@ -433,7 +229,8 @@ LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( /// also a `_binary` version that writes raw values. /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `msg` -- the binary data that needs to be signed (which depends on the storage server request /// being made; for example, "retrieve9991234567890123" for a retrieve request to namespace 999 /// made at unix time 1234567890.123; see storage server RPC documentation for details). @@ -451,49 +248,56 @@ LIBSESSION_EXPORT bool groups_keys_swarm_verify_subaccount_flags( /// 88-character request signature will be written, base64 encoded. This is passes as the /// `signature` value, alongside `subaccount`/`subaccoung_sig` to perform subaccount signature /// authentication. +/// - `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: /// - true if the values were written, false if an error occured (e.g. from an invalid signing_value /// or cryptography error). -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_sign_group_swarm_subaccount( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, char* subaccount, char* subaccount_sig, - char* signature); + char* signature, + char* error); -/// API: groups/groups_keys_swarm_subaccount_sign_binary +/// API: groups/state_sign_group_swarm_subaccount_binary /// -/// Does exactly the same as groups_keys_swarm_subaccount_sign except that the subaccount, +/// Does exactly the same as state_sign_group_swarm_subaccount except that the subaccount, /// subaccount_sig, and signature values are written in binary (without null termination) of exactly /// 36, 64, and 64 bytes, respectively. /// /// Inputs: -/// - see groups_keys_swarm_subaccount_sign +/// - see state_sign_group_swarm_subaccount /// - `subaccount`, `subaccount_sig`, and `signature` are binary output buffers of size 36, 64, and /// 64, respectively. /// /// Outputs: /// See groups_keys_swarm_subaccount. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign_binary( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_sign_group_swarm_subaccount_binary( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, unsigned char* subaccount, unsigned char* subaccount_sig, - unsigned char* signature); + unsigned char* signature, + char* error); -/// API: groups/groups_keys_swarm_subaccount_token +/// API: groups/state_get_group_swarm_subaccount_token /// /// Constructs the subaccount token for a session id. The main use of this is to submit a swarm /// token revocation; for issuing subaccount tokens you want to use -/// `groups_keys_swarm_make_subaccount` instead. This will produce the same subaccount token that -/// `groups_keys_swarm_make_subaccount` implicitly creates that can be passed to a swarm to add a +/// `state_make_group_swarm_subaccount` instead. This will produce the same subaccount token that +/// `state_make_group_swarm_subaccount` implicitly creates that can be passed to a swarm to add a /// revocation for that subaccount. /// /// This is recommended to be used when removing a non-admin member to prevent their access. @@ -502,36 +306,50 @@ LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_sign_binary( /// gains access to messages, they cannot read them). /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `token` -- [out] a 36-byte buffer into which to write the subaccount token. +/// - `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: /// - true if the call succeeded, false if an error occured. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token( - config_group_keys* conf, const char* session_id, unsigned char* token); +LIBSESSION_EXPORT bool state_get_group_swarm_subaccount_token( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* token, + char* error); -/// API: groups/groups_keys_swarm_subaccount_token_flags +/// API: groups/state_get_group_swarm_subaccount_token_flags /// -/// Same as `groups_keys_swarm_subaccount_token`, but takes `write` and `del` flags for creating a -/// token matching a user with non-standard permissions. +/// Same as `state_get_group_swarm_subaccount_token`, but takes `write` and `del` flags for creating +/// a token matching a user with non-standard permissions. /// /// Inputs: -/// - `conf` -- the keys config object +/// - `state` -- Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `session_id` -- the session ID of the member (in hex) /// - `write`, `del` -- see groups_keys_swarm_make_subaccount_flags /// - `token` -- [out] a 36-byte buffer into which to write the subaccount token. +/// - `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: /// - true if the call succeeded, false if an error occured. -LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( - config_group_keys* conf, +LIBSESSION_EXPORT bool state_get_group_swarm_subaccount_token_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* token); + unsigned char* token, + char* error); -/// API: groups/groups_keys_encrypt_message +/// API: groups/state_encrypt_group_message /// /// Encrypts a message using the most recent group encryption key of this object. /// @@ -544,21 +362,23 @@ LIBSESSION_EXPORT bool groups_keys_swarm_subaccount_token_flags( /// and should not be read or free()d. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". /// - `plaintext_in` -- [in] Pointer to a data buffer containing the unencrypted data. /// - `plaintext_len` -- [in] Length of `plaintext_in` /// - `ciphertext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the /// encrypted data written to it, and then the pointer to that buffer is stored here. This /// buffer must be `free()`d by the caller when done with it! /// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. -LIBSESSION_EXPORT void groups_keys_encrypt_message( - const config_group_keys* conf, +LIBSESSION_EXPORT void state_encrypt_group_message( + const state_object* state, + const char* group_id, const unsigned char* plaintext_in, size_t plaintext_len, unsigned char** ciphertext_out, size_t* ciphertext_len); -/// API: groups/groups_keys_decrypt_message +/// API: groups/state_decrypt_group_message /// /// Attempts to decrypt a message using all of the known active encryption keys of this object. The /// message will be de-padded, decompressed (if compressed), and have its signature verified after @@ -568,7 +388,8 @@ LIBSESSION_EXPORT void groups_keys_encrypt_message( /// reason the decryption failed (intended for logging, not for end-user display). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". /// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data (as was /// produced by `groups_keys_encrypt_message`). /// - `ciphertext_len` -- [in] Length of `ciphertext_in` @@ -581,19 +402,24 @@ LIBSESSION_EXPORT void groups_keys_encrypt_message( /// false, in which case the buffer pointer will not be set. /// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. /// Not touched if the function returns false. +/// - `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` -- True if the message was successfully decrypted, false if decryption (or parsing or /// decompression) failed with all of our known keys. If (and only if) true is returned then -/// `plaintext_out` must be freed when done with it. If false is returned then `conf.last_error` -/// will contain a diagnostic message describing why the decryption failed. -LIBSESSION_EXPORT bool groups_keys_decrypt_message( - config_group_keys* conf, +/// `plaintext_out` must be freed when done with it. If false is returned then `error` will +/// contain a diagnostic message describing why the decryption failed. +LIBSESSION_EXPORT bool state_decrypt_group_message( + const state_object* state, + const char* group_id, const unsigned char* cipherext_in, size_t cipherext_len, char* session_id_out, unsigned char** plaintext_out, - size_t* plaintext_len); + size_t* plaintext_len, + char* error); #ifdef __cplusplus } // extern "C" diff --git a/include/session/config/groups/keys.hpp b/include/session/config/groups/keys.hpp index f7dd59ff..3dc593d7 100644 --- a/include/session/config/groups/keys.hpp +++ b/include/session/config/groups/keys.hpp @@ -73,7 +73,6 @@ using namespace std::literals; /// key="SessionGroupKeyGen"), where S = H(group_seed, key="SessionGroupKeySeed"). class Keys final : public ConfigSig { - Ed25519Secret user_ed25519_sk; struct key_info { @@ -206,6 +205,16 @@ class Keys final : public ConfigSig { /// - `const char*` - Will return "groups::Keys" const char* encryption_domain() const { return "groups::Keys"; } + /// API: groups/Keys::default_ttl + /// + /// The default duration the config message should last for before it expires. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::chrono::milliseconds` -- Duration the mesage should last for in milliseconds. + virtual std::chrono::milliseconds default_ttl() const { return std::chrono::hours(30 * 24); } + /// API: groups/Keys::group_keys /// /// Returns all the unexpired decryption keys that we know about. Keys are returned ordered @@ -345,6 +354,20 @@ class Keys final : public ConfigSig { return key_supplement(std::vector{{std::move(sid)}}); } + /// API: groups/Keys::prepare_supplement_payload + /// + /// Generates a payload to send the supplemental keys message to the swarm. + /// + /// Inputs: + /// - `supplement_msg` -- the message generated by `key_supplement`. + /// - `timestamp` -- the current timestamp offset by the latest service node network offset. + /// + /// Outputs: + /// - `std::pair` containing the swarm public key and payload that should + /// be sent to the swarm to store the supplemental keys message. + std::pair prepare_supplement_payload( + ustring supplement_msg, std::chrono::milliseconds timestamp) const; + /// API: groups/current_generation /// /// Returns the current generation number for the latest keys message. diff --git a/include/session/config/groups/members.h b/include/session/config/groups/members.h index a4624b7d..393d2851 100644 --- a/include/session/config/groups/members.h +++ b/include/session/config/groups/members.h @@ -4,14 +4,14 @@ extern "C" { #endif -#include "../base.h" +#include "../../state.h" #include "../profile_pic.h" #include "../util.h" enum groups_members_invite_status { INVITE_SENT = 1, INVITE_FAILED = 2 }; enum groups_members_remove_status { REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2 }; -typedef struct config_group_member { +typedef struct state_group_member { char session_id[67]; // in hex; 66 hex chars + null terminator. // These two will be 0-length strings when unset: @@ -25,88 +25,73 @@ typedef struct config_group_member { // member and their messages bool supplement; -} config_group_member; +} state_group_member; -/// API: groups/groups_members_init -/// -/// Constructs a group members config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Inputs: -/// - `conf` -- [out] Pointer to the config object -/// - `ed25519_pubkey` -- [in] 32-byte pointer to the group's public key -/// - `ed25519_secretkey` -- [in] optional 64-byte pointer to the group's secret key -/// (libsodium-style 64 byte value). Pass as NULL for a non-admin member. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty object this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is 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: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int groups_members_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: groups/groups_members_get +/// API: groups/state_get_group_member /// /// Fills `member` with the member info given a session ID (specified as a null-terminated hex /// string), if the member exists, and returns true. If the member does not exist then `member` /// is left unchanged and false is returned. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) /// - `member` -- [out] the member info data /// - `session_id` -- [in] null terminated hex string +/// - `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. /// /// Output: /// - `bool` -- Returns true if member exists -LIBSESSION_EXPORT bool groups_members_get( - config_object* conf, config_group_member* member, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_group_member( + const state_object* state, + const char* pubkey_hex, + state_group_member* member, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: groups/groups_members_get_or_construct +/// API: groups/state_get_or_construct_group_member /// -/// Same as the above `groups_members_get()` except that when the member does not exist, this sets -/// all the member fields to defaults and loads it with the given session_id. +/// Same as the above `state_get_group_members()` except that when the member does not exist, this +/// sets all the member fields to defaults and loads it with the given session_id. /// /// Returns true as long as it is given a valid session_id. A false return is considered an error, /// and means the session_id was not a valid session_id. /// /// This is the method that should usually be used to create or update a member, followed by -/// setting fields in the member, and then giving it to groups_members_set(). +/// setting fields in the member, and then giving it to state_set_group_member(). +/// - `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. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// - `member` -- [out] the member info data /// - `session_id` -- [in] null terminated hex string /// /// Output: /// - `bool` -- Returns true if the call succeeds, false if an error occurs (e.g. because of an /// invalid session_id). -LIBSESSION_EXPORT bool groups_members_get_or_construct( - config_object* conf, config_group_member* member, const char* session_id) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_or_construct_group_member( + const state_object* state, + const char* group_id, + state_group_member* member, + const char* session_id, + char* error) __attribute__((warn_unused_result)); -/// API: groups/groups_members_set +/// API: groups/state_set_group_member /// /// Adds or updates a member from the given member info struct. /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `member` -- [in] Pointer containing the member info data -LIBSESSION_EXPORT void groups_members_set(config_object* conf, const config_group_member* member); +LIBSESSION_EXPORT void state_set_group_member( + mutable_group_state_object* state, const state_group_member* member); -/// API: groups/groups_members_erase +/// API: groups/state_erase_group_member /// /// Erases a member from the member list. session_id is in hex. Returns true if the member was /// found and removed, false if the member was not present. You must not call this during @@ -116,23 +101,26 @@ LIBSESSION_EXPORT void groups_members_set(config_object* conf, const config_grou /// group). /// /// Inputs: -/// - `conf` -- [in, out] Pointer to the config object +/// - `state` -- [in, out] Pointer to the mutable state object /// - `session_id` -- [in] Text containing null terminated hex string /// /// Outputs: /// - `bool` -- True if erasing was successful -LIBSESSION_EXPORT bool groups_members_erase(config_object* conf, const char* session_id); +LIBSESSION_EXPORT bool state_erase_group_member( + mutable_group_state_object* state, const char* session_id); -/// API: groups/groups_members_size +/// API: groups/state_size_group_members /// /// Returns the number of group members. /// /// Inputs: -/// - `conf` -- input - Pointer to the config object +/// - `state` -- [in] - Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: -/// - `size_t` -- number of contacts -LIBSESSION_EXPORT size_t groups_members_size(const config_object* conf); +/// - `size_t` -- number of members in the group (will be 0 if the group doesn't exist or the +/// 'group_id' is invalid) +LIBSESSION_EXPORT size_t state_size_group_members(const state_object* state, const char* group_id); typedef struct groups_members_iterator { void* _internals; @@ -145,7 +133,7 @@ typedef struct groups_members_iterator { /// Functions for iterating through the entire member list, in sorted order. Intended use is: /// /// group_member m; -/// groups_members_iterator *it = groups_members_iterator_new(group); +/// groups_members_iterator *it = groups_members_iterator_new(state, group_id); /// for (; !groups_members_iterator_done(it, &c); groups_members_iterator_advance(it)) { /// // c.session_id, c.name, etc. are loaded /// } @@ -154,11 +142,13 @@ typedef struct groups_members_iterator { /// It is NOT permitted to add/remove/modify members while iterating. /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". /// /// Outputs: /// - `groups_members_iterator*` -- pointer to the new iterator -LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new(const config_object* conf); +LIBSESSION_EXPORT groups_members_iterator* groups_members_iterator_new( + const state_object* state, const char* group_id); /// API: groups/groups_members_iterator_free /// @@ -175,12 +165,12 @@ LIBSESSION_EXPORT void groups_members_iterator_free(groups_members_iterator* it) /// /// Inputs: /// - `it` -- [in] Pointer to the groups_members_iterator -/// - `m` -- [out] Pointer to the config_group_member, will be populated if false is returned +/// - `m` -- [out] Pointer to the state_group_member, will be populated if false is returned /// /// Outputs: /// - `bool` -- True if iteration has reached the end LIBSESSION_EXPORT bool groups_members_iterator_done( - groups_members_iterator* it, config_group_member* m); + groups_members_iterator* it, state_group_member* m); /// API: groups/groups_members_iterator_advance /// diff --git a/include/session/config/groups/members.hpp b/include/session/config/groups/members.hpp index 9a6dd4c5..acd5d2d8 100644 --- a/include/session/config/groups/members.hpp +++ b/include/session/config/groups/members.hpp @@ -8,7 +8,7 @@ #include "../namespaces.hpp" #include "../profile_pic.hpp" -struct config_group_member; +struct state_group_member; namespace session::config::groups { @@ -49,7 +49,7 @@ struct member { explicit member(std::string sid); // Internal ctor/method for C API implementations: - explicit member(const config_group_member& c); // From c struct + explicit member(const state_group_member& c); // From c struct /// API: groups/member::session_id /// @@ -246,7 +246,7 @@ struct member { /// /// Inputs: /// - `m` -- Reference to C struct to fill with group member info. - void into(config_group_member& m) const; + void into(state_group_member& m) const; /// API: groups/member::set_name /// diff --git a/include/session/config/namespaces.h b/include/session/config/namespaces.h new file mode 100644 index 00000000..e0f98be6 --- /dev/null +++ b/include/session/config/namespaces.h @@ -0,0 +1,16 @@ +#pragma once + +typedef enum NAMESPACE { + NAMESPACE_USER_PROFILE = 2, + NAMESPACE_CONTACTS = 3, + NAMESPACE_CONVO_INFO_VOLATILE = 4, + NAMESPACE_USER_GROUPS = 5, + + // Messages sent to a closed group: + NAMESPACE_GROUP_MESSAGES = 5, + // Groups config namespaces (i.e. for shared config of the group itself, not one user's group + // settings) + NAMESPACE_GROUP_KEYS = 12, + NAMESPACE_GROUP_INFO = 13, + NAMESPACE_GROUP_MEMBERS = 14, +} NAMESPACE; diff --git a/include/session/config/namespaces.hpp b/include/session/config/namespaces.hpp index c5c29ec5..ac969df3 100644 --- a/include/session/config/namespaces.hpp +++ b/include/session/config/namespaces.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace session::config { @@ -17,6 +18,69 @@ enum class Namespace : std::int16_t { GroupKeys = 12, GroupInfo = 13, GroupMembers = 14, + + // Messages sent to an updated group which should be able to be retrieved by revoked members are + // stored in this namespace + RevokedRetrievableGroupMessages = -11, }; +namespace { + std::string namespace_name(const Namespace& n) { + switch (n) { + case Namespace::UserProfile: return "USER_PROFILE"; + case Namespace::Contacts: return "CONTACTS"; + case Namespace::ConvoInfoVolatile: return "CONVO_INFO_VOLATILE"; + case Namespace::UserGroups: return "USER_GROUPS"; + + case Namespace::GroupMessages: return "GroupMessages"; + case Namespace::GroupKeys: return "GROUP_KEYS"; + case Namespace::GroupInfo: return "GROUP_INFO"; + case Namespace::GroupMembers: return "GROUP_MEMBERS"; + + case Namespace::RevokedRetrievableGroupMessages: + return "RevokedRetrievableGroupMessages"; + } + + return "Invalid"; + } + + /// Returns a number indicating the order that the config dumps should be loaded in, we need to + /// load the `UserGroups` config before any group configs (due to how the configs are stored) + /// and the `GroupKeys` config _after_ the `GroupInfo` and `GroupMembers` configs as it requires + /// those to be passed as arguments + int namespace_load_order(const Namespace& n) { + if (n == Namespace::GroupInfo || n == Namespace::GroupMembers) + return 1; + if (n == Namespace::GroupKeys) + return 2; + return 0; + } + + /// Returns a number indicating the order that messages from the specified namespace should be + /// merged in (lower numbers shold be merged first), + /// by merging in a specific order we can prevent certain edge-cases where data/logic between + /// different configs could be dependant on each other (eg. there could be `ConvoInfoVolatile` + /// data related to a new conversation which hasn't been created yet because it's associated + /// `Contacts`/`UserGroups` message hasn't been processed; or a `GroupInfo` which was encrypted + /// with a key included in the `GroupKeys` within the same poll) + int namespace_merge_order(const Namespace& n) { + if (n == Namespace::UserProfile || n == Namespace::Contacts || n == Namespace::GroupKeys) + return 0; + if (n == Namespace::UserGroups || n == Namespace::GroupInfo || n == Namespace::GroupMembers) + return 1; + if (n == Namespace::ConvoInfoVolatile) + return 2; + return 3; + }; + + /// Returns a number indicating the order that the config messages should be sent in, we need to + /// send the `GroupKeys` config _before_ the `GroupInfo` and `GroupMembers` configs as they both + /// get encrypted with the latest key and we want to avoid weird edge-cases + int namespace_send_order(const Namespace& n) { + if (n == Namespace::GroupKeys) + return 0; + return 1; + } +} // namespace + } // namespace session::config diff --git a/include/session/config/user_groups.h b/include/session/config/user_groups.h index b17bf2f7..30664d7e 100644 --- a/include/session/config/user_groups.h +++ b/include/session/config/user_groups.h @@ -4,7 +4,7 @@ extern "C" { #endif -#include "base.h" +#include "../state.h" #include "notify.h" #include "util.h" @@ -88,68 +88,43 @@ typedef struct ugroups_community_info { } ugroups_community_info; -/// API: user_groups/user_groups_init -/// -/// Initializes the user groups object -/// -/// Declaration: -/// ```cpp -/// INT user_groups_init( -/// [out] config_object** conf, -/// [in] unsigned char* ed25519_secretkey, -/// [in, optional] unsigned char* dump, -/// [in, optional] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] pointer to config_object object -/// - `ed25519_secretkey` -- [in] pointer to secret key -/// - `dump` -- [in, optional] text of dump -/// - `dumplen` -- [in, optional] size of the text passed in as dump -/// - `error` -- [out] of the error if failed -/// -/// Outputs: -/// - `int` -- Whether the function succeeded or not -LIBSESSION_EXPORT int user_groups_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) __attribute__((warn_unused_result)); - -/// API: user_groups/user_groups_get_group +/// API: user_groups/state_get_ugroups_group /// /// Gets (non-legacy) group info into `group`, if the group was found. `group_id` is a /// null-terminated C string containing the 66 character group id in hex (beginning with "03"). /// /// Inputs: -/// `conf` -- pointer to the group config object +/// `state` -- pointer to the state object /// `group` -- [out] `ugroups_group_info` struct into which to store the group info. /// `group_id` -- C string containing the hex group id (starting with "03") +/// - `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: /// Returns `true` and populates `group` if the group was found; returns false otherwise. -LIBSESSION_EXPORT bool user_groups_get_group( - config_object* conf, ugroups_group_info* group, const char* group_id); +LIBSESSION_EXPORT bool state_get_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error); -/// API: user_groups/user_groups_get_or_construct_group +/// API: user_groups/state_get_or_construct_ugroups_group /// /// Gets (non-legacy) group info into `group`, if the group was found. Otherwise initialize `group` /// to default values (and set its `.id` appropriately). /// /// Inputs: -/// `conf` -- pointer to the group config object +/// `state` -- pointer to the mutable state object /// `group` -- [out] `ugroups_group_info` struct into which to store the group info. /// `group_id` -- C string containing the hex group id (starting with "03") +/// - `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: -/// Returns `true` on success, `false` upon error (such as when given an invalid group id). -LIBSESSION_EXPORT bool user_groups_get_or_construct_group( - config_object* conf, ugroups_group_info* group, const char* group_id); +/// - `bool` -- `true` on success, `false` upon error (such as when given an invalid group id). +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error); -/// API: user_groups/user_groups_get_community +/// API: user_groups/state_get_ugroups_community /// /// Gets community conversation info into `comm`, if the community info was found. `base_url` and /// `room` are null-terminated c strings. base_url will be normalized/lower-cased; room is @@ -157,32 +132,28 @@ LIBSESSION_EXPORT bool user_groups_get_or_construct_group( /// different room capitalization than the one provided to the call. /// /// Returns true if the community was found and `comm` populated; false otherwise. A false return -/// can either be because it didn't exist (`conf->last_error` will be NULL) or because of some error -/// (`last_error` will be set to an error string). -/// -/// Declaration: -/// ```cpp -/// BOOL user_groups_get_community( -/// [in] config_object* conf, -/// [out] ugroups_community_info* comm, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` +/// can either be because it didn't exist (the error buffer will be NULL) or because of some error +/// (the error buffer will be set to an error string). /// /// Inputs: -/// - `conf` -- [in] pointer to config_object object +/// - `state` -- [in] pointer to state object /// - `comm` -- [out] pointer to ugroups_community_info object /// - `base_url` -- [in] text of the url /// - `room` -- [in] text of the room +/// - `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` -- Whether the function succeeded or not -LIBSESSION_EXPORT bool user_groups_get_community( - config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) - __attribute__((warn_unused_result)); +LIBSESSION_EXPORT bool state_get_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + char* error) __attribute__((warn_unused_result)); -/// API: user_groups/user_groups_get_or_construct_community +/// API: user_groups/state_get_or_construct_ugroups_community /// /// Like the above, but if the community was not found, this constructs one that can be inserted. /// `base_url` will be normalized in the returned object. `room` is a case-insensitive lookup key @@ -195,62 +166,48 @@ LIBSESSION_EXPORT bool user_groups_get_community( /// Note that this is all different from convo_info_volatile, which always forces the room token to /// lower-case (because it does not preserve the case). /// -/// Returns false (and sets `conf->last_error`) on error. -/// -/// Declaration: -/// ```cpp -/// BOOL user_groups_get_or_construct_community( -/// [in] config_object* conf, -/// [out] ugroups_community_info* comm, -/// [in] const char* base_url, -/// [in] const char* room, -/// [in] unsigned const char* pubkey -/// ); -/// ``` +/// Returns false (and sets the error buffer) on error. /// /// Inputs: -/// - `conf` -- [in] pointer to config_object object +/// - `state` -- [in] pointer to mutable state object /// - `comm` -- [out] pointer to ugroups_community_info object /// - `base_url` -- [in] text of the url /// - `room` -- [in] text of the room /// - `pubkey` -- [in] binary of pubkey -/// -/// Outputs: -/// - `bool` -- Whether the function succeeded or not -LIBSESSION_EXPORT bool user_groups_get_or_construct_community( - config_object* conf, +/// - `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. +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_community( + const state_object* state, ugroups_community_info* comm, const char* base_url, const char* room, - unsigned const char* pubkey) __attribute__((warn_unused_result)); + unsigned const char* pubkey, + char* error); -/// API: user_groups/user_groups_get_legacy_group +/// API: user_groups/state_get_ugroups_legacy_group /// /// Returns a ugroups_legacy_group_info pointer containing the conversation info for a given legacy /// group ID (specified as a null-terminated hex string), if the conversation exists. If the -/// conversation does not exist, returns NULL. Sets conf->last_error on error. +/// conversation does not exist, returns NULL. Sets the error buffer on error. /// /// The returned pointer *must* be freed either by calling `ugroups_legacy_group_free()` when done /// with it, or by passing it to `user_groups_set_free_legacy_group()`. /// -/// Declaration: -/// ```cpp -/// UGROUPS_LEGACY_GROUP_INFO* user_groups_get_legacy_group( -/// [in] config_object* conf, -/// [in] const char* id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object +/// - `legacy_group_info` -- [out] Pointer containing conversation info /// - `id` -- [in] Null terminated hex string -/// -/// Outputs: -/// - `ugroupts_legacy_group_info*` -- Pointer containing conversation info -LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_legacy_group( - config_object* conf, const char* id) __attribute__((warn_unused_result)); +/// - `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. +LIBSESSION_EXPORT bool state_get_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error); -/// API: user_groups/user_groups_get_or_construct_legacy_group +/// API: user_groups/state_get_or_construct_ugroups_legacy_group /// /// Same as the above `get_legacy_group()`except that when the conversation does not exist, this /// sets all the group fields to defaults and loads it with the given id. @@ -265,177 +222,111 @@ LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_legacy_group( /// This is the method that should usually be used to create or update a conversation, followed by /// setting fields in the group, and then giving it to user_groups_set(). /// -/// On error, this returns NULL and sets `conf->last_error`. -/// -/// Declaration: -/// ```cpp -/// UGROUPS_LEGACY_GROUP_INFO* user_groups_get_or_construct_legacy_group( -/// [in] config_object* conf, -/// [in] const char* id -/// ); -/// ``` +/// On error, this returns NULL and sets the error buffer. /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object +/// - `legacy_group_info` -- [out] Pointer containing conversation info /// - `id` -- [in] Null terminated hex string -/// -/// Outputs: -/// - `ugroupts_legacy_group_info*` -- Pointer containing conversation info -LIBSESSION_EXPORT ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( - config_object* conf, const char* id) __attribute__((warn_unused_result)); +/// - `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. +LIBSESSION_EXPORT bool state_get_or_construct_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error); -/// API: user_groups/ugroups_legacy_group_free -/// -/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by -/// get_legacy_group/get_or_construct_legacy_group). -/// -/// Declaration: -/// ```cpp -/// VOID ugroups_legacy_group_free( -/// [in] ugroups_community_info* group -/// ); -/// ``` -/// -/// Inputs: -/// - `group` -- [in] Pointer to ugroups_legacy_group_info -LIBSESSION_EXPORT void ugroups_legacy_group_free(ugroups_legacy_group_info* group); - -/// API: user_groups/user_groups_set_community +/// API: user_groups/state_set_ugroups_community /// /// Adds or updates a community conversation from the given group info /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_community( -/// [in] config_object* conf, -/// [in] const ugroups_community_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a community group info object -LIBSESSION_EXPORT void user_groups_set_community( - config_object* conf, const ugroups_community_info* group); +LIBSESSION_EXPORT void state_set_ugroups_community( + mutable_user_state_object* state, const ugroups_community_info* group); -/// API: user_groups/user_groups_set_group +/// API: user_groups/state_set_ugroups_group /// /// Adds or updates a (non-legacy) group conversation from the given group info /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a group info object -LIBSESSION_EXPORT void user_groups_set_group(config_object* conf, const ugroups_group_info* group); +LIBSESSION_EXPORT void state_set_ugroups_group( + mutable_user_state_object* state, const ugroups_group_info* group); -/// API: user_groups/user_groups_set_legacy_group +/// API: user_groups/state_set_ugroups_legacy_group /// /// Adds or updates a legacy group conversation from the into. This version of the method should /// only be used when you explicitly want the `group` to remain valid; if the set is the last thing /// you need to do with it (which is common) it is more efficient to call the freeing version, /// below. /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_legacy_group( -/// [in] config_object* conf, -/// [in] const ugroups_legacy_group_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_legacy_group( - config_object* conf, const ugroups_legacy_group_info* group); +LIBSESSION_EXPORT void state_set_ugroups_legacy_group( + mutable_user_state_object* state, const ugroups_legacy_group_info* group); -/// API: user_groups/user_groups_set_free_legacy_group +/// API: user_groups/state_set_free_ugroups_legacy_group /// /// Same as above `user_groups_set_free_legacy_group()`, except that this also frees the pointer for /// you, which is commonly what is wanted when updating fields. This is equivalent to, but more /// efficient than, setting and then freeing. /// -/// Declaration: -/// ```cpp -/// VOID user_groups_set_free_legacy_group( -/// [in] config_object* conf, -/// [in] const ugroups_legacy_group_info* group -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to mutable state object /// - `group` -- [in] Pointer to a legacy group info object -LIBSESSION_EXPORT void user_groups_set_free_legacy_group( - config_object* conf, ugroups_legacy_group_info* group); +LIBSESSION_EXPORT void state_set_free_ugroups_legacy_group( + mutable_user_state_object* state, ugroups_legacy_group_info* group); -/// API: user_groups/user_groups_erase_community +/// API: user_groups/state_erase_ugroups_community /// /// 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 user_groups_erase_community( -/// [in] config_object* conf, -/// [in] const char* base_url, -/// [in] const char* room -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `base_url` -- [in] null terminated string of the base url /// - `room` -- [in] null terminated string of the room /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_community( - config_object* conf, const char* base_url, const char* room); +LIBSESSION_EXPORT bool state_erase_ugroups_community( + mutable_user_state_object* state, const char* base_url, const char* room); -/// API: user_groups/user_groups_erase_group +/// API: user_groups/state_erase_ugroups_group /// /// Erases a group 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 user_groups_erase_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `group_id` -- [in] null terminated string of the hex group id (starting with "03") /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_ugroups_group( + mutable_user_state_object* state, const char* group_id); -/// API: user_groups/user_groups_erase_legacy_group +/// API: user_groups/state_erase_ugroups_legacy_group /// /// 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 user_groups_erase_legacy_group( -/// [in] config_object* conf, -/// [in] const char* group_id -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// - `group_id` -- [in] null terminated string of the base url /// /// Outputs: /// - `bool` -- Returns True if conversation was found and removed -LIBSESSION_EXPORT bool user_groups_erase_legacy_group(config_object* conf, const char* group_id); +LIBSESSION_EXPORT bool state_erase_ugroups_legacy_group( + mutable_user_state_object* state, const char* group_id); /// API: user_groups/ugroups_group_set_kicked /// @@ -450,6 +341,8 @@ LIBSESSION_EXPORT void ugroups_group_set_kicked(ugroups_group_info* group); /// API: user_groups/ugroups_group_is_kicked /// /// Returns true if we have been kicked (i.e. our secret key and auth data are empty). +/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by +/// get_legacy_group/get_or_construct_legacy_group). /// /// Inputs: /// - `group` -- [in] pointer to the group info to query @@ -458,6 +351,15 @@ LIBSESSION_EXPORT bool ugroups_group_is_kicked(const ugroups_group_info* group); typedef struct ugroups_legacy_members_iterator ugroups_legacy_members_iterator; +/// API: user_groups/ugroups_legacy_group_free +/// +/// Properly frees memory associated with a ugroups_legacy_group_info pointer (as returned by +/// get_legacy_group/get_or_construct_legacy_group). +/// +/// Inputs: +/// - `group` -- [in] Pointer to ugroups_legacy_group_info +LIBSESSION_EXPORT void ugroups_legacy_group_free(ugroups_legacy_group_info* group); + /// API: user_groups/ugroups_legacy_members_begin /// /// Group member iteration; this lets you walk through the full group member list. Example usage: @@ -632,81 +534,53 @@ LIBSESSION_EXPORT bool ugroups_legacy_member_remove( LIBSESSION_EXPORT size_t ugroups_legacy_members_count( const ugroups_legacy_group_info* group, size_t* members, size_t* admins); -/// API: user_groups/user_groups_size +/// API: user_groups/state_size_ugroups /// /// Returns the number of conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups(const state_object* state); -/// API: user_groups/user_groups_size_communities +/// API: user_groups/state_size_ugroups_communities /// /// Returns the number of community conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_communities(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_communities(const state_object* state); -/// API: user_groups/user_groups_size_groups +/// API: user_groups/state_size_ugroups_groups /// /// Returns the number of (non-legacy) group conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_groups(const state_object* state); -/// API: user_groups/user_groups_size_legacy_groups +/// API: user_groups/state_size_ugroups_legacy_groups /// /// Returns the number of legacy group conversations. /// -/// Declaration: -/// ```cpp -/// SIZE_T user_groups_size_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `size_t` -- Returns the number of conversations -LIBSESSION_EXPORT size_t user_groups_size_legacy_groups(const config_object* conf); +LIBSESSION_EXPORT size_t state_size_ugroups_legacy_groups(const state_object* state); typedef struct user_groups_iterator user_groups_iterator; -/// API: user_groups/user_groups_iterator_new +/// API: user_groups/state_iterator_new_user_groups /// /// Starts a new iterator that iterates over all conversations. /// @@ -715,7 +589,7 @@ typedef struct user_groups_iterator user_groups_iterator; /// ugroups_community_info c2; /// ugroups_legacy_group_info c3; /// ugroups_group_info c4; -/// user_groups_iterator *it = user_groups_iterator_new(my_groups); +/// user_groups_iterator *it = state_iterator_new_user_groups(my_groups); /// for (; !user_groups_iterator_done(it); user_groups_iterator_advance(it)) { /// if (user_groups_it_is_community(it, &c2)) { /// // use c2.whatever @@ -730,79 +604,58 @@ typedef struct user_groups_iterator user_groups_iterator; /// /// It is NOT permitted to add/remove/modify records while iterating. /// -/// Declaration: -/// ```cpp -/// USER_GROUPS_ITERATOR* user_groups_iterator_new( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator -LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new(const config_object* conf); +LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new(const state_object* state); -/// API: user_groups/user_groups_iterator_new_communities +/// API: user_groups/state_iterator_new_user_groups_communities /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation. You still need to use `user_groups_it_is_community` (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 -/// USER_GROUPS_ITERATOR* user_groups_iterator_new_communities( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_communities( - const config_object* conf); + const state_object* state); -/// API: user_groups/user_groups_iterator_new_legacy_groups +/// API: user_groups/state_iterator_new_user_groups_legacy_groups /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation. You still need to use `user_groups_it_is_community` (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 -/// USER_GROUPS_ITERATOR* user_groups_iterator_new_legacy_groups( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_legacy_groups( - const config_object* conf); + const state_object* state); -/// API: user_groups/user_groups_iterator_new_groups +/// API: user_groups/state_iterator_new_user_groups_groups /// -/// The same as `user_groups_iterator_new` except that this iterates *only* over one type of +/// The same as `state_iterator_new_user_groups` except that this iterates *only* over one type of /// conversation: non-legacy groups. You still need to use `user_groups_it_is_group` to load the /// data in each pass of the loop. (You can, however, safely ignore the bool return value of the /// `it_is_group` function: it will always be true for iterations for this iterator). /// /// Inputs: -/// - `conf` -- [in] Pointer to config_object object +/// - `state` -- [in] Pointer to state object /// /// Outputs: /// - `user_groups_iterator*` -- The Iterator -LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_groups(const config_object* conf); +LIBSESSION_EXPORT user_groups_iterator* user_groups_iterator_new_groups(const state_object* state); /// API: user_groups/user_groups_iterator_free /// diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 651f46af..d86505eb 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -4,250 +4,122 @@ extern "C" { #endif -#include "base.h" +#include "../state.h" #include "profile_pic.h" -/// API: user_profile/user_profile_init -/// -/// Constructs a user profile config object and sets a pointer to it in `conf`. -/// -/// When done with the object the `config_object` must be destroyed by passing the pointer to -/// config_free() (in `session/config/base.h`). -/// -/// Declaration: -/// ```cpp -/// INT user_profile_init( -/// [out] config_object** conf, -/// [in] const unsigned char* ed25519_secretkey, -/// [in] const unsigned char* dump, -/// [in] size_t dumplen, -/// [out] char* error -/// ); -/// ``` -/// -/// Inputs: -/// - `conf` -- [in] 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. -/// - `dump` -- [in] if non-NULL this restores the state from the dumped byte string produced by a -/// past instantiation's call to `dump()`. To construct a new, empty profile this should be NULL. -/// - `dumplen` -- [in] the length of `dump` when restoring from a dump, or 0 when `dump` is 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: -/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message -/// as a C-string into `error` (if not NULL) on failure. -LIBSESSION_EXPORT int user_profile_init( - config_object** conf, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) -#if defined(__GNUC__) || defined(__clang__) - __attribute__((warn_unused_result)) -#endif - ; - -/// API: user_profile/user_profile_get_name +/// API: state/state_get_profile_name /// /// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at /// all. Should be copied right away as the pointer may not remain valid beyond other API calls. /// -/// Declaration: -/// ```cpp -/// CONST CHAR* user_profile_get_name( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is /// no name -LIBSESSION_EXPORT const char* user_profile_get_name(const config_object* conf); +LIBSESSION_EXPORT const char* state_get_profile_name(const state_object* state); -/// API: user_profile/user_profile_set_name +/// API: state/state_set_profile_name /// /// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Declaration: -/// ```cpp -/// INT user_profile_set_name( -/// [in] config_object* conf, -/// [in] const char* name -/// ); -/// ``` +/// error (and sets the state_object's error string). /// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int user_profile_set_name(config_object* conf, const char* name); +LIBSESSION_EXPORT void state_set_profile_name(mutable_user_state_object* state, const char* name); -/// API: user_profile/user_profile_get_pic +/// API: state/state_get_profile_pic /// /// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile /// pic is not currently set, and otherwise should be copied right away (they will not be valid /// beyond other API calls on this config object). /// -/// Declaration: -/// ```cpp -/// USER_PROFILE_PIC user_profile_get_pic( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `user_profile_pic` -- Pointer to the currently-set profile pic -LIBSESSION_EXPORT user_profile_pic user_profile_get_pic(const config_object* conf); +LIBSESSION_EXPORT user_profile_pic state_get_profile_pic(const state_object* state); -/// API: user_profile/user_profile_set_pic +/// API: state/state_set_profile_pic /// /// Sets a user profile /// -/// Declaration: -/// ```cpp -/// INT user_profile_set_pic( -/// [in] config_object* conf, -/// [in] user_profile_pic pic -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_EXPORT int user_profile_set_pic(config_object* conf, user_profile_pic pic); +LIBSESSION_EXPORT void state_set_profile_pic( + mutable_user_state_object* state, user_profile_pic pic); -/// API: user_profile/user_profile_get_nts_priority +/// API: state/state_get_profile_nts_priority /// /// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > /// 0 for pinned (with higher value = higher priority). /// -/// Declaration: -/// ```cpp -/// INT user_profile_get_nts_priority( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `int` -- Returns the priority level -LIBSESSION_EXPORT int user_profile_get_nts_priority(const config_object* conf); +LIBSESSION_EXPORT int state_get_profile_nts_priority(const state_object* state); -/// API: user_profile/user_profile_set_nts_priority +/// API: state/state_set_profile_nts_priority /// /// Sets the current note-to-self priority level. Set to -1 for hidden; 0 for unpinned, and > 0 for /// higher priority in the conversation list. /// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_nts_priority( -/// [in] config_object* conf, -/// [in] int priority -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `priority` -- [in] Integer of the priority -/// -/// Outputs: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void user_profile_set_nts_priority(config_object* conf, int priority); +LIBSESSION_EXPORT void state_set_profile_nts_priority( + mutable_user_state_object* state, int priority); -/// API: user_profile/user_profile_get_nts_expiry +/// API: state/state_get_profile_nts_expiry /// /// Gets the Note-to-self message expiry timer (seconds). Returns 0 if not set. /// -/// Declaration: -/// ```cpp -/// INT user_profile_get_nts_expiry( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: /// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_EXPORT int user_profile_get_nts_expiry(const config_object* conf); +LIBSESSION_EXPORT int state_get_profile_nts_expiry(const state_object* state); -/// API: user_profile/user_profile_set_nts_expiry +/// API: state/state_set_profile_nts_expiry /// /// Sets the Note-to-self message expiry timer (seconds). Setting 0 (or negative) will clear the /// current timer. /// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_nts_expiry( -/// [in] config_object* conf, -/// [in] int expiry -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_EXPORT void user_profile_set_nts_expiry(config_object* conf, int expiry); +LIBSESSION_EXPORT void state_set_profile_nts_expiry(mutable_user_state_object* state, int expiry); -/// API: user_profile/user_profile_get_blinded_msgreqs +/// API: state/state_get_profile_blinded_msgreqs /// /// Returns true if blinded message requests should be retrieved (from SOGS servers), false if they /// should be ignored. /// -/// Declaration: -/// ```cpp -/// INT user_profile_get_blinded_msgreqs( -/// [in] const config_object* conf -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the state object /// /// Outputs: -/// - `int` -- Will be -1 if the config does not have the value explicitly set, 0 if the setting is +/// - `int` -- Will be -1 if the state does not have the value explicitly set, 0 if the setting is /// explicitly disabled, and 1 if the setting is explicitly enabled. -LIBSESSION_EXPORT int user_profile_get_blinded_msgreqs(const config_object* conf); +LIBSESSION_EXPORT int state_get_profile_blinded_msgreqs(const state_object* state); -/// API: user_profile/user_profile_set_blinded_msgreqs +/// API: state/state_set_profile_blinded_msgreqs /// /// Sets whether blinded message requests should be retrieved from SOGS servers. Set to 1 (or any /// positive value) to enable; 0 to disable; and -1 to clear the setting. /// -/// Declaration: -/// ```cpp -/// VOID user_profile_set_blinded_msgreqs( -/// [in] config_object* conf, -/// [in] int enabled -/// ); -/// ``` -/// /// Inputs: -/// - `conf` -- [in] Pointer to the config object +/// - `state` -- [in] Pointer to the mutable state object /// - `enabled` -- [in] true if they should be enabled, false if disabled -/// -/// Outputs: -/// - `void` -- Returns Nothing -LIBSESSION_EXPORT void user_profile_set_blinded_msgreqs(config_object* conf, int enabled); +LIBSESSION_EXPORT void state_set_profile_blinded_msgreqs( + mutable_user_state_object* state, int enabled); #ifdef __cplusplus } // extern "C" diff --git a/include/session/errors.h b/include/session/errors.h new file mode 100644 index 00000000..899fd0ec --- /dev/null +++ b/include/session/errors.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "export.h" + +LIBSESSION_EXPORT extern const char* SESSION_ERROR_READ_ONLY_CONFIG; + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/errors.hpp b/include/session/errors.hpp new file mode 100644 index 00000000..f7036cd2 --- /dev/null +++ b/include/session/errors.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace session { + +struct Error { + static constexpr const char* READ_ONLY_CONFIG = + "Unable to make changes to a read-only config object"; +}; + +} // namespace session diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 8d2d290e..03e80353 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -1,6 +1,8 @@ +#pragma once + #include -#include "session/onionreq/hop_encryption.hpp" +#include "hop_encryption.hpp" #include "session/types.hpp" namespace session::onionreq { diff --git a/include/session/state.h b/include/session/state.h new file mode 100644 index 00000000..16ba1e06 --- /dev/null +++ b/include/session/state.h @@ -0,0 +1,417 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "config.h" +#include "config/namespaces.h" +#include "config/profile_pic.h" +#include "export.h" + +typedef struct state_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; + + // When an error occurs in the C API this string will be set to the specific error message. May + // be empty. + const char* last_error; + + // Sometimes used as the backing buffer for `last_error`. Should not be touched externally. + char _error_buf[256]; +} state_object; + +typedef struct mutable_user_state_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} mutable_user_state_object; + +typedef struct mutable_group_state_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} mutable_group_state_object; + +typedef struct state_namespaced_dump { + NAMESPACE namespace_; + const char* pubkey_hex; + const unsigned char* data; + size_t datalen; +} state_namespaced_dump; + +typedef struct state_config_message { + NAMESPACE namespace_; + const char* hash; + uint64_t timestamp_ms; + const unsigned char* data; + size_t datalen; +} state_config_message; + +typedef struct state_send_response { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; +} state_send_response; + +typedef enum state_log_level { + LOG_LEVEL_DEBUG = 0, + LOG_LEVEL_INFO, + LOG_LEVEL_WARNING, + LOG_LEVEL_ERROR +} state_log_level; + +/// API: state/state_init +/// +/// Constructs a new state which generates it's own random ed25519 key pair. +/// +/// When done with the object the `state_object` must be destroyed by passing the pointer to +/// state_free(). +/// +/// Inputs: +/// - `state` -- [out] Pointer to the state 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. +/// - `dumps` -- [in] pointer to an array of `state_namespaced_dump` which should include all dumps +/// which should be loaded into the state. +/// - `count` -- [in] number of items in the `dumps` pointer. +/// - `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: +/// - `int` -- Returns 0 on success; returns a non-zero error code and write the exception message +/// as a C-string into `error` (if not NULL) on failure. +LIBSESSION_EXPORT bool state_init( + state_object** state, + const unsigned char* ed25519_secretkey, + state_namespaced_dump* dumps, + size_t count, + char* error) __attribute__((warn_unused_result)); + +/// API: state/state_free +/// +/// Frees a state object. +/// +/// Inputs: +/// - `conf` -- [in] Pointer to state_object object +LIBSESSION_EXPORT void state_free(state_object* state); + +/// API: state/state_load +/// +/// Loads a dump into the state. 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: +/// - `state` -- [in] Pointer to state_object object +/// - `namespace` -- the namespace where config messages for this dump are stored. +/// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `dump` -- pointer to the binary state data that was previously dumped by calling `dump()` or +/// from the `store` hook. +/// - `dumplen` -- length of `dump`. +LIBSESSION_EXPORT bool state_load( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex, + const unsigned char* dump, + size_t dumplen); + +/// API: state/state_set_logger +/// +/// Sets a logging function; takes the log function pointer and a context pointer (which can be NULL +/// if not needed). The given function pointer will be invoked with one of the above values, a +/// null-terminated c string containing the log message, and the void* context object given when +/// setting the logger (this is for caller-specific state data and won't be touched). +/// +/// The logging function must have signature: +/// +/// void log(state_log_level lvl, const char* msg, void* ctx); +/// +/// Can be called with callback set to NULL to clear an existing logger. +/// +/// The config object itself has no log level: the caller should filter by level as needed. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_set_logger( + state_object* state, void (*callback)(state_log_level, const char*, void*), void* ctx); + +/// API: state/state_set_send_callback +/// +/// Takes a function pointer and a context pointer (which can be NULL if not needed). The given +/// function pointer will be invoked whenever a config `needs_push`. The function pointer contains +/// it's own callback function pointer which should be called by the client when it receives a +/// network response to the original send request. +/// +/// Can be called with callback set to NULL to clear an existing hook. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `app_ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT bool state_set_send_callback( + state_object* state, + void (*callback)( + const char* pubkey, + const unsigned char* data, + size_t data_len, + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context), + void* app_ctx); + +/// API: state/state_set_store_callback +/// +/// Takes a function pointer and a context pointer (which can be NULL if not needed). The given +/// function pointer will be invoked whenever a config `needs_dump` as long as the state isn't +/// suppressing store events. +/// +/// The function must have signature: +/// +/// void callback(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*); +/// +/// Can be called with callback set to NULL to clear an existing hook. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `callback` -- [in] Callback function +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT bool state_set_store_callback( + state_object* state, + void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), + void* ctx); + +/// API: state/state_set_service_node_offset +/// +/// Updates the state service node offset. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `offset_ms` -- [in] the delta between the current device time and service node time in the +/// most recent API response +LIBSESSION_EXPORT void state_set_service_node_offset(state_object* state, int64_t offset_ms); + +/// API: state/state_network_offset +/// +/// Retrieves the state service node offset. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// +/// Outputs: +/// - `int64_t` -- the delta between the current device time and service node time in the +/// most recent API response +LIBSESSION_EXPORT int64_t state_network_offset(const state_object* state); + +/// API: state/state_has_pending_send +/// +/// Returns whether the state currently has local changes which are waiting to be sent. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state object +/// +/// Outputs: +/// - `bool` -- Flag indicating whether the state has local changes which are waiting to be sent. +LIBSESSION_EXPORT bool state_has_pending_send(const state_object* state); + +/// API: state/state_merge +/// +/// Takes an pointer to an array of `state_config_message`, sorts them and merges them into the +/// relevant configs. Allocates a new buffer and sets it in `successful_hashes`. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `configs` -- [in] Pointer to an array of `state_config_message` objects +/// - `count` -- [in] Number of objects in `configs` +/// - `successful_hashes` -- [out] Pointer to an array of message hashes that were successfully +/// merged +LIBSESSION_EXPORT bool state_merge( + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + session_string_list** successful_hashes); + +/// API: state/state_current_hashes +/// +/// The current config hashes; this can be empty if the current hashes are unknown or the current +/// state is not clean (i.e. a push is needed or pending). +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 +/// bytes). Required for group hashes. +/// - `current_hashes` -- [out] Pointer to an array of the current config hashes +LIBSESSION_EXPORT bool state_current_hashes( + state_object* state, const char* pubkey_hex_, session_string_list** current_hashes); + +/// API: state/state_current_seqno +/// +/// The current config seqno; this will return the updated seqno if there is a pending push. If +/// an invalid pubkey is provided when trying to retrieve for a group namespace then '-1' is +/// returned. +/// +/// Inputs: +/// - `state` -- [in] Pointer to state object +/// - `pubkey_hex` -- [in] optional pubkey to retrieve the hashes for (in hex, with prefix - 66 +/// bytes). Required for group namespaces. +/// - `namespace` -- [in] The namespace to retrieve the seqno for. +/// +/// Outputs: +/// - `seqno_t` -- The seqno for the config state associated with the given pubkey and namespace (or +/// -1 if invalid). +LIBSESSION_EXPORT seqno_t +state_current_seqno(state_object* state, const char* pubkey_hex, NAMESPACE namespace_); + +/// API: state/state_dump +/// +/// Returns a bt-encoded dict containing the dumps of each of the current config states for +/// storage in the database; the values in the dict would individually get passed into `load` to +/// reconstitute the object (including the push/not pushed status). Resets the `needs_dump()` +/// flag to false. Allocates a new buffer and sets +/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated +/// C string. +/// +/// NB: It is the caller's responsibility to `free()` the buffer when done with it. +/// +/// Immediately after this is called `state_needs_dump` will start returning falst (until the +/// configuration is next modified). +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `full_dump` -- [in] Flag when true the returned bt-encoded dict will include dumps for the +/// entire state, even if they would normally return `false` for `needs_dump()`. +/// - `out` -- [out] Pointer to the output location +/// - `outlen` -- [out] Length of output +LIBSESSION_EXPORT bool state_dump( + state_object* state, bool full_dump, unsigned char** out, size_t* outlen); + +/// API: state/state_dump_namespace +/// +/// Returns a binary dump of the current state of the config object for the specified namespace and +/// pubkey. This dump can be used to resurrect the object at a later point (e.g. after a restart). +/// Allocates a new buffer and sets it in `out` and the length in `outlen`. Note that this is +/// binary data, *not* a null-terminated C string. +/// +/// NB: It is the caller's responsibility to `free()` the buffer when done with it. +/// +/// Immediately after this is called `state_needs_dump` will start returning false (until the +/// configuration is next modified). +/// +/// Inputs: +/// - `state` -- [in] Pointer to state_object object +/// - `namespace` -- [in] the namespace where config messages of the desired dump are stored. +/// - `pubkey_hex` -- [in] optional pubkey the dump is associated to (in hex, with prefix - 66 +/// bytes). Required for group dumps. +/// - `out` -- [out] Pointer to the output location +/// - `outlen` -- [out] Length of output +LIBSESSION_EXPORT bool state_dump_namespace( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex, + unsigned char** out, + size_t* outlen); + +/// API: state/state_get_keys +/// +/// Obtains the current group decryption keys. +/// +/// Returns a buffer where each consecutive 32 bytes is an encryption key for the object, in +/// priority order (i.e. the key at 0 is the encryption key, and the first decryption key). +/// +/// This function is mainly for debugging/diagnostics purposes; most config types have one single +/// key (based on the secret key), and multi-keyed configs such as groups have their own methods for +/// encryption/decryption that are already aware of the multiple keys. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `out` -- [out] pointer to newly malloced key data (a multiple of 32 bytes); the pointer +/// belongs to the caller and must be `free()`d when done with it. +/// - `outlen` -- [out] Pointer where the number of keys will be written (that is: the returned +/// pointer +/// will be to a buffer which has a size of of this value times 32). +LIBSESSION_EXPORT bool state_get_keys( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen); + +/// API: state/state_mutate_user +/// +/// Calls the callback provided with a mutable version of the `state_object` for user changes. +/// +/// If an error occurs while the mutation callback is being performed the function will return false +/// and the `state->last_error` will be populated with the error information. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `callback` -- [in] callback to be called with the `mutable_user_state_object` in order to +/// modify the user state. +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +/// +/// Outputs: +/// - `bool` -- Whether the mutation succeeded or not +LIBSESSION_EXPORT bool state_mutate_user( + state_object* state, void (*callback)(mutable_user_state_object*, void*), void* ctx); + +/// API: state/state_mutate_group +/// +/// Calls the callback provided with a mutable version of the `state_object` for group changes. +/// +/// If an error occurs while the mutation callback is being performed the function will return false +/// and the `state->last_error` will be populated with the error information. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object +/// - `pubkey_hex` -- [in] the group's public key (in hex, including prefix - 66 bytes) +/// - `callback` -- [in] callback to be called with the `mutable_group_state_object` in order to +/// modify the group state. +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +/// +/// Outputs: +/// - `bool` -- Whether the mutation succeeded or not +LIBSESSION_EXPORT bool state_mutate_group( + state_object* state, + const char* pubkey_hex, + void (*callback)(mutable_group_state_object*, void*), + void* ctx); + +/// API: state/mutable_user_state_set_error_if_empty +/// +/// Updates the `state->last_error` value to the provided message if it is currently empty. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `err` -- [in] the error value to store in the state +/// - `err_len` -- [in] length of 'err' +LIBSESSION_EXPORT void mutable_user_state_set_error_if_empty( + mutable_user_state_object* state, const char* err, size_t err_len); + +/// API: state/mutable_group_state_set_error_if_empty +/// +/// Updates the `state->last_error` value to the provided message if it is currently empty. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the mutable state object +/// - `err` -- [in] the error value to store in the state +/// - `err_len` -- [in] length of 'err' +LIBSESSION_EXPORT void mutable_group_state_set_error_if_empty( + mutable_group_state_object* state, const char* err, size_t err_len); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/state.hpp b/include/session/state.hpp new file mode 100644 index 00000000..f84876b9 --- /dev/null +++ b/include/session/state.hpp @@ -0,0 +1,538 @@ +#pragma once + +#include "config.h" +#include "config/contacts.hpp" +#include "config/convo_info_volatile.hpp" +#include "config/groups/info.hpp" +#include "config/groups/keys.hpp" +#include "config/groups/members.hpp" +#include "config/namespaces.hpp" +#include "config/user_groups.hpp" +#include "config/user_profile.hpp" +#include "ed25519.hpp" +#include "session/util.hpp" +#include "state.h" + +namespace session::state { + +class State; + +using Ed25519PubKey = std::array; +using Ed25519Secret = std::array; + +/// Struct containing group configs. +class GroupConfigs { + public: + GroupConfigs( + ustring_view pubkey, + ustring_view user_sk, + std::optional ed25519_secretkey = std::nullopt); + + 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 MutableUserConfigs { + private: + State* parent_state; + + public: + MutableUserConfigs( + State* state, + session::config::Contacts& contacts, + session::config::ConvoInfoVolatile& convo_info_volatile, + session::config::UserGroups& user_groups, + session::config::UserProfile& user_profile, + std::optional> on_error) : + parent_state(state), + contacts(contacts), + convo_info_volatile(convo_info_volatile), + user_groups(user_groups), + user_profile(user_profile), + on_error(on_error) {} + + session::config::Contacts& contacts; + session::config::ConvoInfoVolatile& convo_info_volatile; + session::config::UserGroups& user_groups; + session::config::UserProfile& user_profile; + std::optional> on_error; + + ~MutableUserConfigs(); +}; + +class MutableGroupConfigs { + private: + State& parent_state; + + public: + MutableGroupConfigs( + State& state, + session::config::groups::Info& info, + session::config::groups::Members& members, + session::config::groups::Keys& keys, + std::optional> on_error) : + parent_state(state), info(info), members(members), keys(keys), on_error(on_error) {} + + session::config::groups::Info& info; + session::config::groups::Members& members; + session::config::groups::Keys& keys; + std::optional> on_error; + + std::chrono::milliseconds get_network_offset() const; + + ~MutableGroupConfigs(); +}; + +struct namespaced_dump { + config::Namespace namespace_; + std::optional pubkey_hex; + ustring data; + + namespaced_dump( + config::Namespace namespace_, + std::optional pubkey_hex, + ustring data) : + namespace_{namespace_}, pubkey_hex{pubkey_hex}, data{data} {}; + + namespaced_dump() = delete; + namespaced_dump(namespaced_dump&&) = default; + namespaced_dump(const namespaced_dump&) = default; + namespaced_dump& operator=(namespaced_dump&&) = default; + namespaced_dump& operator=(const namespaced_dump&) = default; +}; + +struct config_message { + config::Namespace namespace_; + std::string hash; + uint64_t timestamp_ms; + ustring data; + + config_message( + config::Namespace namespace_, std::string hash, uint64_t timestamp_ms, ustring data) : + namespace_{namespace_}, hash{hash}, timestamp_ms{timestamp_ms}, data{data} {}; + config_message( + config::Namespace namespace_, + std::string hash, + uint64_t timestamp_ms, + ustring_view data) : + namespace_{namespace_}, hash{hash}, timestamp_ms{timestamp_ms}, data{data} {}; +}; + +struct PreparedPush { + struct Info { + bool is_config_push; + bool requires_response; + config::Namespace namespace_; + seqno_t seqno; + }; + + ustring payload; + std::vector info; +}; + +class State { + private: + 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::map> _config_groups; + + protected: + Ed25519PubKey _user_pk; + Ed25519Secret _user_sk; + std::string _user_x_pk_hex; + + std::function + _store; + std::function + received_response)> + _send; + + public: + std::chrono::milliseconds network_offset; + + // Constructs a state with a secretkey that will be used for signing. + State(ustring_view ed25519_secretkey, std::vector dumps); + + // Constructs a new state, this will generate a random secretkey and should only be used for + // creating a new account. + State() : State(to_unsigned_sv(session::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. + State(State&&) = delete; + State(const State&) = delete; + State& operator=(State&&) = delete; + State& operator=(const State&) = delete; + + // If set then we log things by calling this callback + std::function logger; + + // Invokes the `logger` callback if set, does nothing if there is no logger. + void log(session::config::LogLevel lvl, std::string msg) { + if (logger) + logger(lvl, std::move(msg)); + } + + // Hook which will be called whenever config dumps need to be saved to persistent storage. The + // hook will immediately be called upon assignment if the state needs to be stored. + void on_store(std::function< + void(config::Namespace namespace_, + std::string prefixed_pubkey, + uint64_t timestamp_ms, + ustring data)> hook) { + _store = std::move(hook); + + if (!_store) + return; + + config_changed(std::nullopt, true, false, std::nullopt); + + for (auto& [key, val] : _config_groups) + config_changed(key, true, false, std::nullopt); + }; + + /// Hook which will be called whenever config messages need to be sent via the API. The hook + /// will immediately be called upon assignment if the state needs to be pushed. + /// + /// Parameters: + /// - `pubkey` -- the pubkey (in hex) for the swarm where the data should be sent. + /// - `payload` -- payload which should be sent to the API. + /// - `ctx` -- contextual data which should be used when processing the response. + /// - `received_response` -- callback which should be called with the response from the send + /// request. + void on_send(std::function< + void(std::string pubkey, + ustring payload, + std::function + received_response)> hook) { + _send = std::move(hook); + + if (!_send) + return; + + config_changed(std::nullopt, false, true, std::nullopt); + + for (auto& [key, val] : _config_groups) + config_changed(key, false, true, std::nullopt); + }; + + /// API: state/State::load + /// + /// Loads a dump into the state. 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. + /// - `pubkey_hex` -- 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 pubkey_hex, + ustring_view dump); + + /// API: state/State::has_pending_send + /// + /// Returns whether the state currently has local changes which are waiting to be sent. + /// + /// Outputs: + /// - `bool` -- Flag indicating whether the state has local changes which are waiting to be + /// sent. + bool has_pending_send() const; + + /// API: state/State::config_changed + /// + /// This is called internally whenever a config gets dirtied. This function then validates the + /// state of all config objects associated to the `pubkey_hex` and triggers the `store` and + /// `send` hooks if needed. If there is an open suppression then the suppressed hook(s) will not + /// be called. + /// + /// Inputs: + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). Required for group changes. + /// - `allow_store` -- boolean value to specify whether this change can trigger the store hook. + /// - `allow_send` -- boolean value to specify whether this change can trigger the send hook. + /// - `server_timestamp_ms` -- timestamp value provided when the change was triggered from a + /// merge rather than a user action. + void config_changed( + std::optional pubkey_hex, + bool allow_store, + bool allow_send, + std::optional server_timestamp_ms, + std::optional> + after_send = std::nullopt); + + /// API: state/State::merge + /// + /// This takes all of the messages pulled down from the server and does whatever is necessary to + /// merge (or replace) the current values. + /// + /// Values are pairs of the message hash (as provided by the server) and the raw message body. + /// + /// During this call the `send` and `store` callbacks will be triggered at the appropriate times + /// to correctly update the dump data and push any data to the server again if needed (for + /// example, because the data contained conflicts that required another update to resolve). + /// + /// Returns a vector of successfully merged hashes. + /// + /// Will throw on serious error (i.e. if neither the current nor any of the given configs are + /// parseable). This should not happen (the current config, at least, should always be + /// re-parseable). + /// + /// + /// Inputs: + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). + /// Required for group dumps. + /// - `configs` -- vector of `config_message` types which include the data needed to properly + /// merge. + /// + /// Outputs: + /// - vector of successfully parsed hashes. Note that this does not mean the hash was recent or + /// that it changed the config, merely that the returned hash was properly parsed and + /// processed as a config message, even if it was too old to be useful (or was already known + /// to be included). The hashes will be in the same order as in the input vector. + std::vector merge( + std::optional pubkey_hex, const std::vector& configs); + + /// API: state/State::current_hashes + /// + /// The current config hashes; this can be empty if the current hashes are unknown or the + /// current state is not clean (i.e. a push is needed or pending). + /// + /// Inputs: + /// - `pubkey_hex` -- optional pubkey to retrieve the hashes for (in hex, with prefix - 66 + /// bytes). Required for group hashes. + /// + /// Outputs: + /// - `std::vector` -- Returns current config hashes + std::vector current_hashes( + std::optional pubkey_hex = std::nullopt); + + /// API: state/State::dump + /// + /// Returns a bt-encoded dict containing the dumps of each of the current config states for + /// storage in the database; the values in the dict would individually get passed into `load` to + /// reconstitute the object (including the push/not pushed status). Resets the `needs_dump()` + /// flag to false. + /// + /// Inputs: + /// - `full_dump` -- when true the returned bt-encoded dict will include dumps for the entire + /// state, even if they would normally return `false` for `needs_dump()`. + /// + /// Outputs: + /// - `ustring` -- Returns bt-encoded dict of the state dump + ustring dump(bool full_dump = false); + + /// API: state/State::dump + /// + /// Returns a dump of the current config state for the specified namespace and pubkey for + /// storage in the database; this value would get passed into `load` to reconstitute the object + /// (including the push/not pushed status). Resets the `needs_dump()` flag to false for the + /// specific config. + /// + /// Inputs: + /// - `namespace` -- the namespace where config messages of the desired dump are stored. + /// - `pubkey_hex` -- optional pubkey the dump is associated to (in hex, with prefix - 66 + /// bytes). Required for group dumps. + /// + /// Outputs: + /// - `ustring` -- Returns binary data of the state dump + ustring dump( + config::Namespace namespace_, + std::optional pubkey_hex = std::nullopt); + + /// API: state/State::get_keys + /// + /// Returns a vector of encryption keys, in priority order (i.e. element 0 is the encryption + /// key, and the first decryption key). + /// + /// This method is mainly for debugging/diagnostics purposes; most config types have one single + /// key (based on the secret key), and multi-keyed configs such as groups have their own methods + /// for encryption/decryption that are already aware of the multiple keys. + /// + /// Inputs: + /// - `namespace` -- the namespace where the desired config messages are stored. + /// - `pubkey_hex` -- optional pubkey the config is associated to (in hex, with prefix - 66 + /// bytes). Required for group configs. + /// + /// Outputs: + /// - `std::vector` -- Returns vector of encryption keys + std::vector get_keys( + config::Namespace namespace_, std::optional pubkey_hex_); + + /// API: groups/State::create_group + /// + /// Creates a new group with the provided values defining the initial state. Triggers the + /// callback upon success or error, if an error occurred the `error` value will be populated, + /// otherwise the `group_id` and `group_sk` will be populated. + /// + /// This function will add the updated group into the user groups config and setup the initial + /// group configs. The '_send' and '_store' hooks will be triggered for the newly + /// created/updated config messages. + /// + /// Note: This function **does not** send invitations to the group members so the clients will + /// still need to do so. Any members provided to this funciton will be included in the initial + /// keys generation. + /// + /// Inputs: + /// - `name` -- the name of the group. + /// - `description` -- optional description for the group. + /// - `pic` -- optional display picture for the group. + /// - `members` -- initial members to be added to the group. + /// - `callback` -- a callback to be triggered upon success/failure of the group creation. + void create_group( + std::string_view name, + std::optional description, + std::optional pic, + std::vector members, + std::function< + void(std::string_view group_id, + ustring_view group_sk, + std::optional error)> callback); + + /// API: groups/State::approve_group + /// + /// Approves a group invitation, this will update the 'invited' flag in the user groups config + /// and create the initial group state. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + void approve_group(std::string_view group_id); + + /// API: groups/State::load_group_admin_key + /// + /// Loads the admin keys into a group, upgrading the user from a member to an admin within the + /// keys and members objects, and storing the group secret key within the user groups config. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `secret` -- the group's 64-byte secret key or 32-byte seed + /// + /// Outputs: nothing. After a successful call, `admin()` will return true. Throws if the given + /// secret key does not match the group's pubkey. + void load_group_admin_key(std::string_view group_id, ustring_view secret); + + /// API: groups/add_group_members + /// + /// Adds members to Members for the group and performs either a key rotation or a key + /// supplement. Only admins can call this. + /// + /// Invite details, auth signature, etc. will still need to be sent separately to the new user. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `supplemental_rotation` -- flag to control whether a supplemental (when true) or full + /// (when false) key rotation should be performed. Doing a supplemental rotation will + /// distributes the existing active keys so that the new members can access existing key, + /// configs and messages. + /// - `members` -- vector of members to add to the group. + /// - `callback` -- Callback function called once the send process completes. + void add_group_members( + std::string_view group_id, + bool supplemental_rotation, + const std::vector members, + std::function error)> callback); + + /// API: groups/State::erase_group + /// + /// Removes the group state and, if specified, removes the group from the user groups config. + /// + /// Inputs: + /// - `group_id` -- the group id/pubkey, in hex, beginning with "03". + /// - `remove_user_record` -- flag to indicate whether the user groups entry should be removed. + void erase_group(std::string_view group_id, bool remove_user_record); + + // Retrieves a read-only version of the user config + template + const ConfigType& config() const; + + // Retrieves a read-only version of the group config for the given public key + template + const ConfigType& config(std::string_view pubkey_hex) const; + + // Retrieves an editable version of the user config. Once the returned value is deconstructed it + // will trigger the `send` and `store` hooks. + MutableUserConfigs mutable_config( + std::optional> on_error = std::nullopt); + + // Retrieves an editable version of the group config for the given public key. Once the returned + // value is deconstructed it will trigger the `send` and `store` hooks. + MutableGroupConfigs mutable_config( + std::string_view pubkey_hex, + std::optional> on_error = std::nullopt); + + private: + template + void add_child_logger(ConfigType& base); + + PreparedPush prepare_push( + std::string pubkey_hex, + std::chrono::milliseconds timestamp, + std::vector configs, + std::optional group_sk = std::nullopt); + void handle_config_push_response( + std::string pubkey, + std::vector push_info, + bool success, + uint16_t status_code, + ustring response); +}; + +inline State& unbox(state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline const State& unbox(const state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline MutableUserConfigs& unbox(mutable_user_state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} +inline MutableGroupConfigs& unbox(mutable_group_state_object* state) { + assert(state && state->internals); + return *static_cast(state->internals); +} + +inline bool set_error(state_object* state, std::string_view e) { + if (e.size() > 255) + e.remove_suffix(e.size() - 255); + std::memcpy(state->_error_buf, e.data(), e.size()); + state->_error_buf[e.size()] = 0; + state->last_error = state->_error_buf; + return false; +} + +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 session::state diff --git a/include/session/state_groups.h b/include/session/state_groups.h new file mode 100644 index 00000000..685fabbe --- /dev/null +++ b/include/session/state_groups.h @@ -0,0 +1,125 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "config/groups/members.h" +#include "config/namespaces.h" +#include "config/profile_pic.h" +#include "export.h" +#include "state.h" + +/// API: groups/state_create_group +/// +/// Creates a new group with the provided values defining the initial state. Triggers the callback +/// upon success or error, if an error occurred the `error` value will be populated, otherwise the +/// `group_id` and `group_sk` will be populated. +/// +/// This function will add the updated group into the user groups config and setup the initial group +/// configs. The '_send' and '_store' hooks will be triggered for the newly created/updated config +/// messages. +/// +/// Note: This function **does not** send invitations to the group members so the clients will still +/// need to do so. Any members provided to this funciton will be included in the initial keys +/// generation. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `name` -- the name of the group. +/// - `name_len` -- the length of the 'name' +/// - `description` -- optional description for the group. +/// - `description_len` -- the length of the 'description'. +/// - `pic` -- optional display picture for the group. +/// - `members` -- initial members to be added to the group. +/// - `members_len` -- the length of the 'members' array. +/// - `callback` -- a callback to be triggered upon success/failure of the group creation. +/// - `ctx` -- Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_create_group( + state_object* state, + const char* name, + size_t name_len, + const char* description, + size_t description_len, + const user_profile_pic pic, + const state_group_member* members, + const size_t members_len, + void (*callback)( + const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx), + void* ctx); + +/// API: groups/state_approve_group +/// +/// Approves a group invitation, this will update the 'invited' flag in the user groups config and +/// create the initial group state. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +LIBSESSION_EXPORT void state_approve_group(state_object* state, const char* group_id); + +/// API: groups/state_load_group_admin_key +/// +/// Loads the admin keys into a group, upgrading the user from a member to an admin within the keys +/// and members objects, and storing the group secret key within the user groups config. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `seed` -- pointer to the 32-byte seed. +/// +/// Outputs: +/// - `true` if the member has been upgraded to admin status, or was already admin status; `false` +/// if the given seed value does not match the group's public key. If this returns `true` then +/// after the call a call to `state_is_group_admin` would also return `true`. +LIBSESSION_EXPORT bool state_load_group_admin_key( + state_object* state, const char* group_id, unsigned const char* seed); + +/// API: groups/state_add_group_members +/// +/// Adds members to Members for the group and performs either a key rotation or a key supplement. +/// Only admins can call this. +/// +/// Invite details, auth signature, etc. will still need to be sent separately to the new user. +/// +/// Inputs: +/// - `state` -- [in] Pointer to the state object. +/// - `group_id` -- [in] the group id/pubkey, in hex, beginning with "03". +/// - `supplemental_rotation` -- [in] flag to control whether a supplemental (when true) or full +/// (when false) key rotation should be performed. Doing a supplemental rotation will distributes +/// the existing active keys so that the new members can access existing key, configs and messages. +/// - `members` -- [in] array of members to add to the group. +/// - `members_len` -- [in] length of the `members` array +/// - `callback` -- [in] Callback function called once the send process completes +/// - `ctx` --- [in, optional] Pointer to an optional context. Set to NULL if unused +LIBSESSION_EXPORT void state_add_group_members( + state_object* state, + const char* group_id, + const bool supplemental_rotation, + const state_group_member* members, + const size_t members_len, + void (*callback)(const char* error, const size_t error_len, void* ctx), + void* ctx); + +/// API: groups/state_erase_group +/// +/// Removes the group state and, if specified, removes the group from the user groups config. +/// +/// Inputs: +/// - `state` -- Pointer to the mutable state object +/// - `group_id` -- the group id/pubkey, in hex, beginning with "03". +/// - `remove_user_record` -- flag to indicate whether the user groups entry should be removed. +LIBSESSION_EXPORT void state_erase_group( + state_object* state, const char* group_id, bool remove_user_record); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/include/session/util.hpp b/include/session/util.hpp index 65ae5fb2..b8bc8358 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -27,6 +27,9 @@ inline const unsigned char* to_unsigned(const std::byte* x) { inline unsigned char* to_unsigned(std::byte* x) { return reinterpret_cast(x); } +inline ustring to_unsigned(std::string x) { + return {to_unsigned(x.data()), x.size()}; +} // These do nothing, but having them makes template metaprogramming easier: inline const unsigned char* to_unsigned(const unsigned char* x) { return x; @@ -47,6 +50,10 @@ inline ustring_view to_unsigned_sv(std::string_view v) { inline ustring_view to_unsigned_sv(std::basic_string_view v) { return {to_unsigned(v.data()), v.size()}; } +template +inline ustring_view to_unsigned_sv(const std::array& v) { + return {v.data(), v.size()}; +} inline ustring_view to_unsigned_sv(ustring_view v) { return v; // no-op, but helps with template metaprogamming } @@ -66,10 +73,18 @@ inline std::basic_string_view to_sv(const std::array& v) { return {v.data(), N}; } +inline ustring_view operator""_usv(const char* __str, size_t __len) { + return {to_unsigned(__str), __len}; +} + inline uint64_t get_timestamp() { return std::chrono::steady_clock::now().time_since_epoch().count(); } +inline std::string bool_to_string(bool v) { + return (v ? "true" : "false"); +} + /// Returns true if the first string is equal to the second string, compared case-insensitively. inline bool string_iequal(std::string_view s1, std::string_view s2) { return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6c3f1158..f38af186 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -73,11 +73,12 @@ add_libsession_util_library(config config/protos.cpp config/user_groups.cpp config/user_profile.cpp + errors.cpp + state.cpp + state_c_wrapper.cpp fields.cpp ) - - target_link_libraries(crypto PUBLIC common @@ -91,6 +92,7 @@ target_link_libraries(config common libsession::protos PRIVATE + nlohmann_json::nlohmann_json libsodium::sodium-internal libzstd::static ) diff --git a/src/config/base.cpp b/src/config/base.cpp index ae44ffcc..aecac1d5 100644 --- a/src/config/base.cpp +++ b/src/config/base.cpp @@ -13,9 +13,9 @@ #include #include "internal.hpp" -#include "session/config/base.h" #include "session/config/encrypt.hpp" #include "session/config/protos.hpp" +#include "session/errors.hpp" #include "session/export.h" #include "session/util.hpp" @@ -25,7 +25,7 @@ namespace session::config { void ConfigBase::set_state(ConfigState s) { if (s == ConfigState::Dirty && is_readonly()) - throw std::runtime_error{"Unable to make changes to a read-only config object"}; + throw std::runtime_error{Error::READ_ONLY_CONFIG}; if (_state == ConfigState::Clean && !_curr_hash.empty()) { _old_hashes.insert(std::move(_curr_hash)); @@ -597,174 +597,4 @@ std::array ConfigSig::seed_hash(std::string_view key) const { return out; } -void set_error(config_object* conf, std::string e) { - auto& error = unbox(conf).error; - error = std::move(e); - conf->last_error = error.c_str(); -} - } // namespace session::config - -extern "C" { - -using namespace session; -using namespace session::config; - -LIBSESSION_EXPORT void config_free(config_object* conf) { - delete conf; -} - -LIBSESSION_EXPORT int16_t config_storage_namespace(const config_object* conf) { - return static_cast(unbox(conf)->storage_namespace()); -} - -LIBSESSION_EXPORT config_string_list* config_merge( - config_object* conf, - const char** msg_hashes, - const unsigned char** configs, - const size_t* lengths, - size_t count) { - auto& config = *unbox(conf); - std::vector> confs; - confs.reserve(count); - for (size_t i = 0; i < count; i++) - confs.emplace_back(msg_hashes[i], ustring_view{configs[i], lengths[i]}); - - return make_string_list(config.merge(confs)); -} - -LIBSESSION_EXPORT bool config_needs_push(const config_object* conf) { - return unbox(conf)->needs_push(); -} - -LIBSESSION_EXPORT config_push_data* config_push(config_object* conf) { - auto& config = *unbox(conf); - auto [seqno, data, obs] = config.push(); - - // We need to do one alloc here that holds everything: - // - the returned struct - // - pointers to the obsolete message hash strings - // - the data - // - the message hash strings - size_t buffer_size = sizeof(config_push_data) + obs.size() * sizeof(char*) + data.size(); - for (auto& o : obs) - buffer_size += o.size(); - buffer_size += obs.size(); // obs msg hash string NULL terminators - - auto* ret = static_cast(std::malloc(buffer_size)); - - ret->seqno = seqno; - - static_assert(alignof(config_push_data) >= alignof(char*)); - ret->obsolete = reinterpret_cast(ret + 1); - ret->obsolete_len = obs.size(); - - ret->config = reinterpret_cast(ret->obsolete + ret->obsolete_len); - ret->config_len = data.size(); - - std::memcpy(ret->config, data.data(), data.size()); - char* obsptr = reinterpret_cast(ret->config + ret->config_len); - for (size_t i = 0; i < obs.size(); i++) { - std::memcpy(obsptr, obs[i].c_str(), obs[i].size() + 1); - ret->obsolete[i] = obsptr; - obsptr += obs[i].size() + 1; - } - - return ret; -} - -LIBSESSION_EXPORT void config_confirm_pushed( - config_object* conf, seqno_t seqno, const char* msg_hash) { - unbox(conf)->confirm_pushed(seqno, msg_hash); -} - -LIBSESSION_EXPORT void config_dump(config_object* conf, unsigned char** out, size_t* outlen) { - assert(out && outlen); - auto data = unbox(conf)->dump(); - *outlen = data.size(); - *out = static_cast(std::malloc(data.size())); - std::memcpy(*out, data.data(), data.size()); -} - -LIBSESSION_EXPORT bool config_needs_dump(const config_object* conf) { - return unbox(conf)->needs_dump(); -} - -LIBSESSION_EXPORT config_string_list* config_current_hashes(const config_object* conf) { - return make_string_list(unbox(conf)->current_hashes()); -} - -LIBSESSION_EXPORT unsigned char* config_get_keys(const config_object* conf, size_t* len) { - const auto keys = unbox(conf)->get_keys(); - assert(std::count_if(keys.begin(), keys.end(), [](const auto& k) { return k.size() == 32; }) == - keys.size()); - assert(len); - *len = keys.size(); - if (keys.empty()) - return nullptr; - auto* buf = static_cast(std::malloc(32 * keys.size())); - auto* cur = buf; - for (const auto& k : keys) { - std::memcpy(cur, k.data(), 32); - cur += 32; - } - - return buf; -} - -LIBSESSION_EXPORT void config_add_key(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}); -} -LIBSESSION_EXPORT void config_add_key_low_prio(config_object* conf, const unsigned char* key) { - unbox(conf)->add_key({key, 32}, /*high_priority=*/false); -} -LIBSESSION_EXPORT int config_clear_keys(config_object* conf) { - return unbox(conf)->clear_keys(); -} -LIBSESSION_EXPORT bool config_remove_key(config_object* conf, const unsigned char* key) { - return unbox(conf)->remove_key({key, 32}); -} -LIBSESSION_EXPORT int config_key_count(const config_object* conf) { - return unbox(conf)->key_count(); -} -LIBSESSION_EXPORT bool config_has_key(const config_object* conf, const unsigned char* key) { - return unbox(conf)->has_key({key, 32}); -} -LIBSESSION_EXPORT const unsigned char* config_key(const config_object* conf, size_t i) { - return unbox(conf)->key(i).data(); -} - -LIBSESSION_EXPORT const char* config_encryption_domain(const config_object* conf) { - return unbox(conf)->encryption_domain(); -} - -LIBSESSION_EXPORT void config_set_sig_keys(config_object* conf, const unsigned char* secret) { - unbox(conf)->set_sig_keys({secret, 64}); -} - -LIBSESSION_EXPORT void config_set_sig_pubkey(config_object* conf, const unsigned char* pubkey) { - unbox(conf)->set_sig_pubkey({pubkey, 32}); -} - -LIBSESSION_EXPORT const unsigned char* config_get_sig_pubkey(const config_object* conf) { - const auto& pk = unbox(conf)->get_sig_pubkey(); - if (pk) - return pk->data(); - return nullptr; -} - -LIBSESSION_EXPORT void config_clear_sig_keys(config_object* conf) { - unbox(conf)->clear_sig_keys(); -} - -LIBSESSION_EXPORT void config_set_logger( - config_object* conf, void (*callback)(config_log_level, const char*, void*), void* ctx) { - if (!callback) - unbox(conf)->logger = nullptr; - else - unbox(conf)->logger = [callback, ctx](LogLevel lvl, std::string msg) { - callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); - }; -} - -} // extern "C" diff --git a/src/config/contacts.cpp b/src/config/contacts.cpp index fa61e1ed..0b1129e1 100644 --- a/src/config/contacts.cpp +++ b/src/config/contacts.cpp @@ -9,6 +9,8 @@ #include "session/config/contacts.h" #include "session/config/error.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -31,10 +33,6 @@ static_assert(CONVO_NOTIFY_ALL == static_cast(notify_mode::all)); static_assert(CONVO_NOTIFY_DISABLED == static_cast(notify_mode::disabled)); static_assert(CONVO_NOTIFY_MENTIONS_ONLY == static_cast(notify_mode::mentions_only)); -LIBSESSION_C_API bool session_id_is_valid(const char* session_id) { - return std::strlen(session_id) == 66 && oxenc::is_hex(session_id, session_id + 66); -} - contact_info::contact_info(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } @@ -56,15 +54,6 @@ Contacts::Contacts(ustring_view ed25519_secretkey, std::optional d 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(""); @@ -131,6 +120,7 @@ void contact_info::into(contacts_contact& c) const { c.blocked = blocked; c.priority = priority; c.notifications = static_cast(notifications); + c.mute_until = mute_until; c.exp_mode = static_cast(exp_mode); c.exp_seconds = exp_timer.count(); if (c.exp_seconds <= 0 && c.exp_mode != CONVO_EXPIRATION_NONE) @@ -153,6 +143,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id, blocked = c.blocked; priority = c.priority; notifications = static_cast(c.notifications); + mute_until = c.mute_until; exp_mode = static_cast(c.exp_mode); exp_timer = exp_mode == expiration_mode::none ? 0s : std::chrono::seconds{c.exp_seconds}; if (exp_timer <= 0s && exp_mode != expiration_mode::none) @@ -172,21 +163,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) { - try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get(session_id)) { - c->into(*contact); - return true; - } - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - } - return false; -} - contact_info Contacts::get_or_construct(std::string_view pubkey_hex) const { if (auto maybe = get(pubkey_hex)) return *std::move(maybe); @@ -194,19 +170,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) { - try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct(session_id).into(*contact); - return true; - } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; - } -} - void Contacts::set(const contact_info& contact) { std::string pk = session_id_to_bytes(contact.session_id); auto info = data["c"][pk]; @@ -245,10 +208,6 @@ void Contacts::set(const contact_info& contact) { set_positive_int(info["j"], contact.created); } -LIBSESSION_C_API void contacts_set(config_object* conf, const contacts_contact* contact) { - unbox(conf)->set(contact_info{*contact}); -} - 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)); @@ -314,24 +273,12 @@ 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(); -} - /// 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). void Contacts::iterator::_load_info() { @@ -372,9 +319,55 @@ Contacts::iterator& Contacts::iterator::operator++() { return *this; } -LIBSESSION_C_API contacts_iterator* contacts_iterator_new(const config_object* conf) { +using namespace session::state; +using namespace session::config; + +extern "C" { + +LIBSESSION_C_API bool state_get_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) { + try { + if (auto c = unbox(state).config().get(session_id)) { + c->into(*contact); + return true; + } + } catch (const std::exception& e) { + set_error_value(error, e.what()); + } + return false; +} + +LIBSESSION_C_API bool state_get_or_construct_contact( + const state_object* state, contacts_contact* contact, const char* session_id, char* error) { + try { + unbox(state).config().get_or_construct(session_id).into(*contact); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API void state_set_contact( + mutable_user_state_object* state, const contacts_contact* contact) { + unbox(state).contacts.set(contact_info{*contact}); +} + +LIBSESSION_C_API bool state_erase_contact( + mutable_user_state_object* state, const char* session_id) { + try { + return unbox(state).contacts.erase(session_id); + } catch (...) { + return false; + } +} + +LIBSESSION_C_API size_t state_size_contacts(const state_object* state) { + return unbox(state).config().size(); +} + +LIBSESSION_C_API contacts_iterator* contacts_iterator_new(const state_object* state) { auto* it = new contacts_iterator{}; - it->_internals = new Contacts::iterator{unbox(conf)->begin()}; + it->_internals = new Contacts::iterator{unbox(state).config().begin()}; return it; } @@ -394,3 +387,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 421a8666..e1f4867d 100644 --- a/src/config/convo_info_volatile.cpp +++ b/src/config/convo_info_volatile.cpp @@ -15,6 +15,8 @@ #include "session/config/convo_info_volatile.h" #include "session/config/error.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -476,6 +478,7 @@ ConvoInfoVolatile::iterator& ConvoInfoVolatile::iterator::operator++() { } // namespace session::config +using namespace session::state; using namespace session::config; extern "C" { @@ -484,233 +487,224 @@ struct convo_info_volatile_iterator { }; } -LIBSESSION_C_API -int convo_info_volatile_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, +LIBSESSION_C_API bool state_get_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, char* error) { - return c_wrapper_init( - conf, ed25519_secretkey_bytes, dumpstr, dumplen, error); -} - -LIBSESSION_C_API bool convo_info_volatile_get_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_1to1(session_id)) { + if (auto c = unbox(state).config().get_1to1(session_id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_1to1( - config_object* conf, convo_info_volatile_1to1* convo, const char* session_id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_1to1( + const state_object* state, + convo_info_volatile_1to1* convo, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_1to1(session_id).into(*convo); + unbox(state).config().get_or_construct_1to1(session_id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_community( - config_object* conf, +LIBSESSION_C_API bool state_get_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* og, const char* base_url, - const char* room) { + const char* room, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_community(base_url, room)) { + if (auto c = unbox(state).config().get_community(base_url, room)) { c->into(*og); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_community( - config_object* conf, +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_community( + const state_object* state, convo_info_volatile_community* convo, const char* base_url, const char* room, - unsigned const char* pubkey) { + unsigned const char* pubkey, + char* error) { try { - conf->last_error = nullptr; - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + unbox(state) + .config() + .get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) .into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_group(id)) { + if (auto c = unbox(state).config().get_group(id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_group( - config_object* conf, convo_info_volatile_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_group( + const state_object* state, convo_info_volatile_group* convo, const char* id, char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_group(id).into(*convo); + unbox(state).config().get_or_construct_group(id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool convo_info_volatile_get_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_legacy_group(id)) { + if (auto c = unbox(state).config().get_legacy_group(id)) { c->into(*convo); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool convo_info_volatile_get_or_construct_legacy_group( - config_object* conf, convo_info_volatile_legacy_group* convo, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_convo_info_volatile_legacy_group( + const state_object* state, + convo_info_volatile_legacy_group* convo, + const char* id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_legacy_group(id).into(*convo); + unbox(state).config().get_or_construct_legacy_group(id).into(*convo); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void convo_info_volatile_set_1to1( - config_object* conf, const convo_info_volatile_1to1* convo) { - unbox(conf)->set(convo::one_to_one{*convo}); +LIBSESSION_C_API void state_set_convo_info_volatile_1to1( + mutable_user_state_object* state, const convo_info_volatile_1to1* convo) { + unbox(state).convo_info_volatile.set(convo::one_to_one{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_community( - config_object* conf, const convo_info_volatile_community* convo) { - unbox(conf)->set(convo::community{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_community( + mutable_user_state_object* state, const convo_info_volatile_community* convo) { + unbox(state).convo_info_volatile.set(convo::community{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_group( - config_object* conf, const convo_info_volatile_group* convo) { - unbox(conf)->set(convo::group{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_group( + mutable_user_state_object* state, const convo_info_volatile_group* convo) { + unbox(state).convo_info_volatile.set(convo::group{*convo}); } -LIBSESSION_C_API void convo_info_volatile_set_legacy_group( - config_object* conf, const convo_info_volatile_legacy_group* convo) { - unbox(conf)->set(convo::legacy_group{*convo}); + +LIBSESSION_C_API void state_set_convo_info_volatile_legacy_group( + mutable_user_state_object* state, const convo_info_volatile_legacy_group* convo) { + unbox(state).convo_info_volatile.set(convo::legacy_group{*convo}); } -LIBSESSION_C_API bool convo_info_volatile_erase_1to1(config_object* conf, const char* session_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_1to1( + mutable_user_state_object* state, const char* session_id) { try { - return unbox(conf)->erase_1to1(session_id); + return unbox(state).convo_info_volatile.erase_1to1(session_id); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_community( - config_object* conf, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_community( + mutable_user_state_object* state, const char* base_url, const char* room) { try { - return unbox(conf)->erase_community(base_url, room); + return unbox(state).convo_info_volatile.erase_community(base_url, room); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_group(config_object* conf, const char* group_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_group( + mutable_user_state_object* state, const char* group_id) { try { - return unbox(conf)->erase_group(group_id); + return unbox(state).convo_info_volatile.erase_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API bool convo_info_volatile_erase_legacy_group( - config_object* conf, const char* group_id) { +LIBSESSION_C_API bool state_erase_convo_info_volatile_legacy_group( + mutable_user_state_object* state, const char* group_id) { try { - return unbox(conf)->erase_legacy_group(group_id); + return unbox(state).convo_info_volatile.erase_legacy_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API size_t convo_info_volatile_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t state_size_convo_info_volatile(const state_object* state) { + return unbox(state).config().size(); } -LIBSESSION_C_API size_t convo_info_volatile_size_1to1(const config_object* conf) { - return unbox(conf)->size_1to1(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_1to1(const state_object* state) { + return unbox(state).config().size_1to1(); } -LIBSESSION_C_API size_t convo_info_volatile_size_communities(const config_object* conf) { - return unbox(conf)->size_communities(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_communities(const state_object* state) { + return unbox(state).config().size_communities(); } -LIBSESSION_C_API size_t convo_info_volatile_size_groups(const config_object* conf) { - return unbox(conf)->size_groups(); +LIBSESSION_C_API size_t state_size_convo_info_volatile_groups(const state_object* state) { + return unbox(state).config().size_groups(); } -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 state_size_convo_info_volatile_legacy_groups(const state_object* state) { + return unbox(state).config().size_legacy_groups(); } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = new ConvoInfoVolatile::iterator{unbox(conf)->begin()}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(state).config().begin()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_1to1( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = new ConvoInfoVolatile::iterator{unbox(conf)->begin_1to1()}; + it->_internals = + new ConvoInfoVolatile::iterator{unbox(state).config().begin_1to1()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_communities( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_communities()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_communities()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_groups( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_groups()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_groups()}; return it; } LIBSESSION_C_API convo_info_volatile_iterator* convo_info_volatile_iterator_new_legacy_groups( - const config_object* conf) { + const state_object* state) { auto* it = new convo_info_volatile_iterator{}; - it->_internals = - new ConvoInfoVolatile::iterator{unbox(conf)->begin_legacy_groups()}; + it->_internals = new ConvoInfoVolatile::iterator{ + unbox(state).config().begin_legacy_groups()}; return it; } diff --git a/src/config/groups/info.cpp b/src/config/groups/info.cpp index 88b3a1eb..f987424e 100644 --- a/src/config/groups/info.cpp +++ b/src/config/groups/info.cpp @@ -9,6 +9,8 @@ #include "session/config/error.h" #include "session/config/groups/info.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -117,270 +119,153 @@ bool Info::is_destroyed() const { } // namespace session::config::groups using namespace session; +using namespace session::state; using namespace session::config; +extern "C" { + LIBSESSION_C_API const size_t GROUP_INFO_NAME_MAX_LENGTH = groups::Info::NAME_MAX_LENGTH; LIBSESSION_C_API const size_t GROUP_INFO_DESCRIPTION_MAX_LENGTH = groups::Info::DESCRIPTION_MAX_LENGTH; -LIBSESSION_C_API int groups_info_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) { - return c_group_wrapper_init( - conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); -} - -/// API: groups_info/groups_info_get_name -/// -/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at -/// all. Should be copied right away as the pointer may not remain valid beyond other API calls. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `char*` -- Pointer to the currently-set name as a null-terminated string, or NULL if there is -/// no name -LIBSESSION_C_API const char* groups_info_get_name(const config_object* conf) { - if (auto s = unbox(conf)->get_name()) - return s->data(); - return nullptr; -} - -/// API: groups_info/groups_info_set_name -/// -/// Sets the group's name to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `name` -- [in] Pointer to the name as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_name(config_object* conf, const char* name) { +LIBSESSION_C_API bool state_get_group_name( + const state_object* state, const char* group_id, char* name) { try { - unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + if (auto s = unbox(state).config({group_id, 66}).get_name()) { + std::string res = {s->data(), s->size()}; + if (res.size() > groups::Info::NAME_MAX_LENGTH) + res.resize(groups::Info::NAME_MAX_LENGTH); + std::memcpy(name, res.c_str(), res.size() + 1); + return true; + } + } catch (...) { } - return 0; -} - -/// API: groups_info/groups_info_get_description -/// -/// Returns a pointer to the currently-set description (null-terminated), or NULL if there is -/// no description at all. Should be copied right away as the pointer may not remain valid -/// beyond other API calls. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `char*` -- Pointer to the currently-set description as a null-terminated string, or NULL -/// if there is no description -LIBSESSION_C_API const char* groups_info_get_description(const config_object* conf) { - if (auto s = unbox(conf)->get_description()) - return s->data(); - return nullptr; -} - -/// API: groups_info/groups_info_set_description -/// -/// Sets the group's description to the null-terminated C string. Returns 0 on success, non-zero on -/// error (and sets the config_object's error string). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `description` -- [in] Pointer to the description as a null-terminated C string -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_description(config_object* conf, const char* description) { + return false; +} + +LIBSESSION_C_API void state_set_group_name(mutable_group_state_object* state, const char* name) { + unbox(state).info.set_name(name); +} + +LIBSESSION_C_API bool state_get_group_description( + const state_object* state, const char* group_id, char* description) { try { - unbox(conf)->set_description(description); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + if (auto s = unbox(state).config({group_id, 66}).get_description()) { + std::string res = {s->data(), s->size()}; + if (res.size() > groups::Info::DESCRIPTION_MAX_LENGTH) + res.resize(groups::Info::DESCRIPTION_MAX_LENGTH); + std::memcpy(description, res.c_str(), res.size() + 1); + return true; + } + } catch (...) { } - return 0; -} - -/// API: groups_info/groups_info_get_pic -/// -/// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile -/// pic is not currently set, and otherwise should be copied right away (they will not be valid -/// beyond other API calls on this config object). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `user_profile_pic` -- Pointer to the currently-set profile pic (despite the "user_profile" in -/// the struct name, this is the group's profile pic). -LIBSESSION_C_API user_profile_pic groups_info_get_pic(const config_object* conf) { - user_profile_pic p; - if (auto pic = unbox(conf)->get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; + return false; +} + +LIBSESSION_C_API void state_set_group_description( + mutable_group_state_object* state, const char* description) { + unbox(state).info.set_description(description); +} + +LIBSESSION_C_API bool state_get_group_pic( + const state_object* state, const char* group_id, user_profile_pic* pic) { + try { + if (auto p = unbox(state).config({group_id, 66}).get_profile_pic()) { + copy_c_str(pic->url, p.url); + std::memcpy(pic->key, p.key.data(), 32); + return true; + } + } catch (...) { } - return p; -} - -/// API: groups_info/groups_info_set_pic -/// -/// Sets a user profile -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `pic` -- [in] Pointer to the pic -/// -/// Outputs: -/// - `int` -- Returns 0 on success, non-zero on error -LIBSESSION_C_API int groups_info_set_pic(config_object* conf, user_profile_pic pic) { + return false; +} + +LIBSESSION_C_API void state_set_group_pic(mutable_group_state_object* state, user_profile_pic pic) { std::string_view url{pic.url}; ustring_view key; if (!url.empty()) key = {pic.key, 32}; + unbox(state).info.set_profile_pic(url, key); +} + +LIBSESSION_C_API bool state_get_group_expiry_timer( + const state_object* state, const char* group_id, int* timer) { + try { + *timer = unbox(state) + .config({group_id, 66}) + .get_expiry_timer() + .value_or(0s) + .count(); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_set_group_expiry_timer(mutable_group_state_object* state, int expiry) { + unbox(state).info.set_expiry_timer(std::max(0, expiry) * 1s); +} + +LIBSESSION_C_API bool state_get_group_created( + const state_object* state, const char* group_id, int64_t* created) { + try { + *created = unbox(state).config({group_id, 66}).get_created().value_or(0); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_set_group_created(mutable_group_state_object* state, int64_t ts) { + unbox(state).info.set_created(std::max(0, ts)); +} + +LIBSESSION_C_API bool state_get_group_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before) { + try { + *delete_before = + unbox(state).config({group_id, 66}).get_delete_before().value_or(0); + return true; + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_set_group_delete_before(mutable_group_state_object* state, int64_t ts) { + unbox(state).info.set_delete_before(std::max(0, ts)); +} + +LIBSESSION_C_API bool state_get_group_attach_delete_before( + const state_object* state, const char* group_id, int64_t* delete_before) { try { - unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); + *delete_before = unbox(state) + .config({group_id, 66}) + .get_delete_attach_before() + .value_or(0); + return true; + } catch (...) { } + return false; +} - return 0; -} - -/// API: groups_info/groups_info_get_expiry_timer -/// -/// Gets the group's message expiry timer (seconds). Returns 0 if not set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int` -- Returns the expiry timer in seconds. Returns 0 if not set -LIBSESSION_C_API int groups_info_get_expiry_timer(const config_object* conf) { - if (auto t = unbox(conf)->get_expiry_timer(); t && *t > 0s) - return t->count(); - return 0; -} - -/// API: groups_info/groups_info_set_expiry_timer -/// -/// Sets the group's message expiry timer (seconds). Setting 0 (or negative) will clear the current -/// timer. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `expiry` -- [in] Integer of the expiry timer in seconds -LIBSESSION_C_API void groups_info_set_expiry_timer(config_object* conf, int expiry) { - unbox(conf)->set_expiry_timer(std::max(0, expiry) * 1s); -} - -/// API: groups_info/groups_info_get_created -/// -/// Returns the timestamp (unix time, in seconds) when the group was created. Returns 0 if unset. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp when the group was created (if set by an admin). -LIBSESSION_C_API int64_t groups_info_get_created(const config_object* conf) { - return unbox(conf)->get_created().value_or(0); -} - -/// API: groups_info/groups_info_set_created -/// -/// Sets the creation time (unix timestamp, in seconds) when the group was created. Setting 0 -/// clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_created(config_object* conf, int64_t ts) { - unbox(conf)->set_created(std::max(0, ts)); -} - -/// API: groups_info/groups_info_get_delete_before -/// -/// Returns the delete-before timestamp (unix time, in seconds); clients should deleted all messages -/// from the group with timestamps earlier than this value, if set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_C_API int64_t groups_info_get_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_before().value_or(0); -} - -/// API: groups_info/groups_info_set_delete_before -/// -/// Sets the delete-before time (unix timestamp, in seconds) before which messages should be delete. -/// Setting 0 clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_before(std::max(0, ts)); -} - -/// API: groups_info/groups_info_get_attach_delete_before -/// -/// Returns the delete-before timestamp (unix time, in seconds) for attachments; clients should drop -/// all attachments from messages from the group with timestamps earlier than this value, if set. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `int64_t` -- Unix timestamp before which messages should be deleted. Returns 0 if not set. -LIBSESSION_C_API int64_t groups_info_get_attach_delete_before(const config_object* conf) { - return unbox(conf)->get_delete_attach_before().value_or(0); -} - -/// API: groups_info/groups_info_set_attach_delete_before -/// -/// Sets the delete-before time (unix timestamp, in seconds) for attachments; attachments should be -/// dropped from messages older than this value. Setting 0 clears the value. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// - `ts` -- [in] the unix timestamp, or 0 to clear a current value. -LIBSESSION_C_API void groups_info_set_attach_delete_before(config_object* conf, int64_t ts) { - unbox(conf)->set_delete_attach_before(std::max(0, ts)); -} - -/// API: groups_info/groups_info_is_destroyed(const config_object* conf); -/// -/// Returns true if this group has been marked destroyed by an admin, which indicates to a receiving -/// client that they should destroy it locally. -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -/// -/// Outputs: -/// - `true` if the group has been nuked, `false` otherwise. -LIBSESSION_C_API bool groups_info_is_destroyed(const config_object* conf) { - return unbox(conf)->is_destroyed(); -} - -/// API: groups_info/groups_info_destroy_group(const config_object* conf); -/// -/// Nukes a group from orbit. This is permanent (i.e. there is no removing this setting once set). -/// -/// Inputs: -/// - `conf` -- [in] Pointer to the config object -LIBSESSION_C_API void groups_info_destroy_group(config_object* conf) { - unbox(conf)->destroy_group(); +LIBSESSION_C_API void state_set_group_attach_delete_before( + mutable_group_state_object* state, int64_t ts) { + unbox(state).info.set_delete_attach_before(std::max(0, ts)); } + +LIBSESSION_C_API bool state_group_is_destroyed(const state_object* state, const char* group_id) { + try { + if (unbox(state).config({group_id, 66}).is_destroyed()) { + return true; + } + } catch (...) { + } + return false; +} + +LIBSESSION_C_API void state_destroy_group(mutable_group_state_object* state) { + unbox(state).info.destroy_group(); +} + +} // extern "C" \ No newline at end of file diff --git a/src/config/groups/keys.cpp b/src/config/groups/keys.cpp index a3b034ab..5a355d44 100644 --- a/src/config/groups/keys.cpp +++ b/src/config/groups/keys.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -22,6 +23,8 @@ #include "session/config/groups/keys.h" #include "session/config/groups/members.hpp" #include "session/multi_encrypt.hpp" +#include "session/state.h" +#include "session/state.hpp" #include "session/xed25519.hpp" using namespace std::literals; @@ -538,6 +541,40 @@ ustring Keys::key_supplement(const std::vector& sids) const { return ustring{to_unsigned_sv(d.view())}; } +std::pair Keys::prepare_supplement_payload( + ustring supplement_msg, std::chrono::milliseconds timestamp) const { + if (!admin()) + throw std::runtime_error{"prepare_supplement_payload: Failed to sign; user is not admin"}; + if (!_sign_pk) + throw std::runtime_error{"prepare_supplement_payload: Missing group pubkey"}; + + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store"); + verification += to_unsigned_sv(std::to_string(static_cast(storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); + + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), _sign_sk.data())) + throw std::runtime_error{ + "config_changed: Failed to sign; perhaps the secret key is invalid?"}; + + auto group_id = "03" + oxenc::to_hex(_sign_pk->begin(), _sign_pk->end()); + nlohmann::json params{ + {"namespace", storage_namespace()}, + {"pubkey", group_id}, + {"ttl", default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(supplement_msg)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + nlohmann::json request_json{{"method", "store"}, {"params", params}}; + + return {group_id, to_unsigned(request_json.dump())}; +} + // Blinding factor for subaccounts: H(sessionid || groupid) mod L, where H is 64-byte blake2b, using // a hash key derived from the group's seed. std::array Keys::subaccount_blind_factor( @@ -1371,270 +1408,97 @@ std::pair Keys::decrypt_message(ustring_view ciphertext) c using namespace session; using namespace session::config; +using namespace session::state; -namespace { -groups::Keys& unbox(config_group_keys* conf) { - assert(conf && conf->internals); - return *static_cast(conf->internals); -} -const groups::Keys& unbox(const config_group_keys* conf) { - assert(conf && conf->internals); - return *static_cast(conf->internals); -} - -void set_error(config_group_keys* conf, std::string_view e) { - if (e.size() > 255) - e.remove_suffix(e.size() - 255); - std::memcpy(conf->_error_buf, e.data(), e.size()); - conf->_error_buf[e.size()] = 0; - conf->last_error = conf->_error_buf; -} -} // namespace - -LIBSESSION_C_API int groups_keys_init( - config_group_keys** conf, - const unsigned char* user_ed25519_secretkey, - const unsigned char* group_ed25519_pubkey, - const unsigned char* group_ed25519_secretkey, - config_object* cinfo, - config_object* cmembers, - const unsigned char* dump, - size_t dumplen, - char* error) { - - assert(user_ed25519_secretkey && group_ed25519_pubkey && cinfo && cmembers); - - ustring_view user_sk{user_ed25519_secretkey, 64}; - ustring_view group_pk{group_ed25519_pubkey, 32}; - std::optional group_sk; - if (group_ed25519_secretkey) - group_sk.emplace(group_ed25519_secretkey, 64); - std::optional dumped; - if (dump && dumplen) - dumped.emplace(dump, dumplen); - - auto& info = *unbox(cinfo); - auto& members = *unbox(cmembers); - auto c_conf = std::make_unique(); +extern "C" { +LIBSESSION_C_API size_t state_size_group_keys(const state_object* state, const char* group_id) { try { - c_conf->internals = new groups::Keys{user_sk, group_pk, group_sk, dumped, info, members}; - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return SESSION_ERR_INVALID_DUMP; + return unbox(state).config(group_id).size(); + } catch (...) { + return 0; } - - c_conf->last_error = nullptr; - *conf = c_conf.release(); - return SESSION_ERR_NONE; -} - -LIBSESSION_C_API size_t groups_keys_size(const config_group_keys* conf) { - return unbox(conf).size(); -} - -LIBSESSION_C_API const unsigned char* group_keys_get_key(const config_group_keys* conf, size_t N) { - auto keys = unbox(conf).group_keys(); - if (N >= keys.size()) - return nullptr; - return keys[N].data(); -} - -LIBSESSION_C_API bool groups_keys_is_admin(const config_group_keys* conf) { - return unbox(conf).admin(); } -LIBSESSION_C_API bool groups_keys_load_admin_key( - config_group_keys* conf, - const unsigned char* secret, - config_object* info, - config_object* members) { +LIBSESSION_C_API const unsigned char* state_get_group_key( + const state_object* state, const char* group_id, size_t N) { try { - unbox(conf).load_admin_key( - ustring_view{secret, 32}, - *unbox(info), - *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + auto keys = unbox(state).config(group_id).group_keys(); + if (N >= keys.size()) + return nullptr; + return keys[N].data(); + } catch (...) { + return nullptr; } - return true; } -LIBSESSION_C_API bool groups_keys_rekey( - config_group_keys* conf, - config_object* info, - config_object* members, - const unsigned char** out, - size_t* outlen) { - assert(info && members && out && outlen); - auto& keys = unbox(conf); - ustring_view to_push; +LIBSESSION_C_API bool state_is_group_admin(const state_object* state, const char* group_id) { try { - to_push = keys.rekey(*unbox(info), *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); + return unbox(state).config(group_id).admin(); + } catch (...) { return false; } - *out = to_push.data(); - *outlen = to_push.size(); - return true; -} - -LIBSESSION_C_API bool groups_keys_pending_config( - const config_group_keys* conf, const unsigned char** out, size_t* outlen) { - assert(out && outlen); - if (auto pending = unbox(conf).pending_config()) { - *out = pending->data(); - *outlen = pending->size(); - return true; - } - return false; } -LIBSESSION_C_API bool groups_keys_load_message( - config_group_keys* conf, - const char* msg_hash, - const unsigned char* data, - size_t datalen, - int64_t timestamp_ms, - config_object* info, - config_object* members) { - assert(data && info && members); +LIBSESSION_C_API bool state_group_needs_rekey(const state_object* state, const char* group_id) { try { - unbox(conf).load_key_message( - msg_hash, - ustring_view{data, datalen}, - timestamp_ms, - *unbox(info), - *unbox(members)); - } catch (const std::exception& e) { - set_error(conf, e.what()); + return unbox(state).config(group_id).needs_rekey(); + } catch (...) { return false; } - return true; -} - -LIBSESSION_C_API config_string_list* groups_keys_current_hashes(const config_group_keys* conf) { - return make_string_list(unbox(conf).current_hashes()); -} - -LIBSESSION_C_API bool groups_keys_needs_rekey(const config_group_keys* conf) { - return unbox(conf).needs_rekey(); -} - -LIBSESSION_C_API bool groups_keys_needs_dump(const config_group_keys* conf) { - return unbox(conf).needs_dump(); -} - -LIBSESSION_C_API void groups_keys_dump( - config_group_keys* conf, unsigned char** out, size_t* outlen) { - assert(out && outlen); - auto dump = unbox(conf).dump(); - *out = static_cast(std::malloc(dump.size())); - std::memcpy(*out, dump.data(), dump.size()); - *outlen = dump.size(); } -LIBSESSION_C_API void groups_keys_encrypt_message( - const config_group_keys* conf, - const unsigned char* plaintext_in, - size_t plaintext_len, - unsigned char** ciphertext_out, - size_t* ciphertext_len) { - assert(plaintext_in && ciphertext_out && ciphertext_len); - - ustring ciphertext; +LIBSESSION_C_API bool state_rekey_group(mutable_group_state_object* state) { try { - ciphertext = unbox(conf).encrypt_message(ustring_view{plaintext_in, plaintext_len}); - *ciphertext_out = static_cast(std::malloc(ciphertext.size())); - std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); - *ciphertext_len = ciphertext.size(); - } catch (...) { - *ciphertext_out = nullptr; - *ciphertext_len = 0; - } -} - -LIBSESSION_C_API bool groups_keys_decrypt_message( - config_group_keys* conf, - const unsigned char* ciphertext_in, - size_t ciphertext_len, - char* session_id, - unsigned char** plaintext_out, - size_t* plaintext_len) { - assert(ciphertext_in && plaintext_out && plaintext_len); - - try { - auto [sid, plaintext] = - unbox(conf).decrypt_message(ustring_view{ciphertext_in, ciphertext_len}); - std::memcpy(session_id, sid.c_str(), sid.size() + 1); - *plaintext_out = static_cast(std::malloc(plaintext.size())); - std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); - *plaintext_len = plaintext.size(); + unbox(state).keys.rekey(unbox(state).info, unbox(state).members); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)(e.what()); + return false; } - return false; } -LIBSESSION_C_API bool groups_keys_key_supplement( - config_group_keys* conf, - const char** sids, - size_t sids_len, - unsigned char** message, - size_t* message_len) { - assert(sids && message && message_len); - - std::vector session_ids; - for (size_t i = 0; i < sids_len; i++) - session_ids.emplace_back(sids[i]); +LIBSESSION_EXPORT int state_get_current_group_generation( + const state_object* state, const char* group_id) { try { - auto msg = unbox(conf).key_supplement(session_ids); - *message = static_cast(malloc(msg.size())); - *message_len = msg.size(); - std::memcpy(*message, msg.data(), msg.size()); - return true; - } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return unbox(state).config(group_id).current_generation(); + } catch (...) { + return 0; } } -LIBSESSION_EXPORT int groups_keys_current_generation(config_group_keys* conf) { - return unbox(conf).current_generation(); -} - -LIBSESSION_C_API bool groups_keys_swarm_make_subaccount_flags( - config_group_keys* conf, +LIBSESSION_C_API bool state_make_group_swarm_subaccount_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* sign_value) { + unsigned char* sign_value, + char* error) { assert(sign_value); try { - auto val = unbox(conf).swarm_make_subaccount(session_id, write, del); + auto val = unbox(state).config(group_id).swarm_make_subaccount( + session_id, write, del); assert(val.size() == 100); std::memcpy(sign_value, val.data(), val.size()); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_make_subaccount( - config_group_keys* conf, const char* session_id, unsigned char* sign_value) { - return groups_keys_swarm_make_subaccount_flags(conf, session_id, true, false, sign_value); +LIBSESSION_C_API bool state_make_group_swarm_subaccount( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* sign_value, + char* error) { + return state_make_group_swarm_subaccount_flags( + state, group_id, session_id, true, false, sign_value, error); } -LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( +LIBSESSION_C_API bool verify_group_swarm_subaccount_flags( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value, @@ -1652,7 +1516,7 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount_flags( } } -LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( +LIBSESSION_C_API bool verify_group_swarm_subaccount( const char* group_id, const unsigned char* session_ed25519_secretkey, const unsigned char* signing_value) { @@ -1662,18 +1526,20 @@ LIBSESSION_C_API bool groups_keys_swarm_verify_subaccount( ustring_view{signing_value, 100}); } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( - config_group_keys* conf, +LIBSESSION_C_API bool state_sign_group_swarm_subaccount( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, char* subaccount, char* subaccount_sig, - char* signature) { + char* signature, + char* error) { assert(msg && signing_value && subaccount && subaccount_sig && signature); try { - auto auth = unbox(conf).swarm_subaccount_sign( + auto auth = unbox(state).config(group_id).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}); assert(auth.subaccount.size() == 48); assert(auth.subaccount_sig.size() == 88); @@ -1683,23 +1549,24 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign( std::memcpy(signature, auth.signature.c_str(), auth.signature.size() + 1); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( - config_group_keys* conf, +LIBSESSION_C_API bool state_sign_group_swarm_subaccount_binary( + const state_object* state, + const char* group_id, const unsigned char* msg, size_t msg_len, const unsigned char* signing_value, unsigned char* subaccount, unsigned char* subaccount_sig, - unsigned char* signature) { + unsigned char* signature, + char* error) { assert(msg && signing_value && subaccount && subaccount_sig && signature); try { - auto auth = unbox(conf).swarm_subaccount_sign( + auto auth = unbox(state).config(group_id).swarm_subaccount_sign( ustring_view{msg, msg_len}, ustring_view{signing_value, 100}, true); assert(auth.subaccount.size() == 36); assert(auth.subaccount_sig.size() == 64); @@ -1709,29 +1576,83 @@ LIBSESSION_C_API bool groups_keys_swarm_subaccount_sign_binary( std::memcpy(signature, auth.signature.data(), 64); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_token_flags( - config_group_keys* conf, +LIBSESSION_C_API bool state_get_group_swarm_subaccount_token_flags( + const state_object* state, + const char* group_id, const char* session_id, bool write, bool del, - unsigned char* token) { + unsigned char* token, + char* error) { try { - auto tok = unbox(conf).swarm_subaccount_token(session_id, write, del); + auto tok = unbox(state).config(group_id).swarm_subaccount_token( + session_id, write, del); assert(tok.size() == 36); std::memcpy(token, tok.data(), 36); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool groups_keys_swarm_subaccount_token( - config_group_keys* conf, const char* session_id, unsigned char* token) { - return groups_keys_swarm_subaccount_token_flags(conf, session_id, true, false, token); +LIBSESSION_C_API bool state_get_group_swarm_subaccount_token( + const state_object* state, + const char* group_id, + const char* session_id, + unsigned char* token, + char* error) { + return state_get_group_swarm_subaccount_token_flags( + state, group_id, session_id, true, false, token, error); } + +LIBSESSION_C_API void state_encrypt_group_message( + const state_object* state, + const char* group_id, + const unsigned char* plaintext_in, + size_t plaintext_len, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { + assert(plaintext_in && ciphertext_out && ciphertext_len); + + ustring ciphertext; + try { + ciphertext = unbox(state).config(group_id).encrypt_message( + ustring_view{plaintext_in, plaintext_len}); + *ciphertext_out = static_cast(std::malloc(ciphertext.size())); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + *ciphertext_len = ciphertext.size(); + } catch (...) { + *ciphertext_out = nullptr; + *ciphertext_len = 0; + } +} + +LIBSESSION_C_API bool state_decrypt_group_message( + const state_object* state, + const char* group_id, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + char* session_id, + unsigned char** plaintext_out, + size_t* plaintext_len, + char* error) { + assert(ciphertext_in && plaintext_out && plaintext_len); + + try { + auto [sid, plaintext] = unbox(state).config(group_id).decrypt_message( + ustring_view{ciphertext_in, ciphertext_len}); + std::memcpy(session_id, sid.c_str(), sid.size() + 1); + *plaintext_out = static_cast(std::malloc(plaintext.size())); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + *plaintext_len = plaintext.size(); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +} // extern "C" diff --git a/src/config/groups/members.cpp b/src/config/groups/members.cpp index 8db53d9b..b033452c 100644 --- a/src/config/groups/members.cpp +++ b/src/config/groups/members.cpp @@ -4,6 +4,8 @@ #include "../internal.hpp" #include "session/config/groups/members.h" +#include "session/state.h" +#include "session/state.hpp" namespace session::config::groups { @@ -132,7 +134,7 @@ member::member(std::string sid) : session_id{std::move(sid)} { check_session_id(session_id); } -member::member(const config_group_member& m) : session_id{m.session_id, 66} { +member::member(const state_group_member& m) : session_id{m.session_id, 66} { assert(std::strlen(m.name) <= MAX_NAME_LENGTH); name = m.name; assert(std::strlen(m.profile_pic.url) <= profile_pic::MAX_URL_LENGTH); @@ -149,7 +151,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} { supplement = m.supplement; } -void member::into(config_group_member& m) const { +void member::into(state_group_member& m) const { std::memcpy(m.session_id, session_id.data(), 67); copy_c_str(m.name, name); if (profile_picture) { @@ -177,65 +179,69 @@ void member::set_name(std::string n) { using namespace session; using namespace session::config; +using namespace session::state; -LIBSESSION_C_API int groups_members_init( - config_object** conf, - const unsigned char* ed25519_pubkey, - const unsigned char* ed25519_secretkey, - const unsigned char* dump, - size_t dumplen, - char* error) { - return c_group_wrapper_init( - conf, ed25519_pubkey, ed25519_secretkey, dump, dumplen, error); -} +extern "C" { -LIBSESSION_C_API bool groups_members_get( - config_object* conf, config_group_member* member, const char* session_id) { +LIBSESSION_C_API bool state_get_group_member( + const state_object* state, + const char* group_id, + state_group_member* member, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get(session_id)) { + if (auto c = unbox(state).config(group_id).get(session_id)) { c->into(*member); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool groups_members_get_or_construct( - config_object* conf, config_group_member* member, const char* session_id) { +LIBSESSION_C_API bool state_get_or_construct_group_member( + const state_object* state, + const char* group_id, + state_group_member* member, + const char* session_id, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct(session_id).into(*member); + unbox(state).config(group_id).get_or_construct(session_id).into(*member); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); return false; } } -LIBSESSION_C_API void groups_members_set(config_object* conf, const config_group_member* member) { - unbox(conf)->set(groups::member{*member}); +LIBSESSION_C_API void state_set_group_member( + mutable_group_state_object* state, const state_group_member* member) { + unbox(state).members.set(groups::member{*member}); } -LIBSESSION_C_API bool groups_members_erase(config_object* conf, const char* session_id) { +LIBSESSION_C_API bool state_erase_group_member( + mutable_group_state_object* state, const char* session_id) { try { - return unbox(conf)->erase(session_id); + return unbox(state).members.erase(session_id); } catch (...) { return false; } } -LIBSESSION_C_API size_t groups_members_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t state_size_group_members(const state_object* state, const char* group_id) { + try { + return unbox(state).config(group_id).size(); + } catch (...) { + return 0; + } } -LIBSESSION_C_API groups_members_iterator* groups_members_iterator_new(const config_object* conf) { +LIBSESSION_C_API groups_members_iterator* groups_members_iterator_new( + const state_object* state, const char* group_id) { auto* it = new groups_members_iterator{}; - it->_internals = new groups::Members::iterator{unbox(conf)->begin()}; + it->_internals = + new groups::Members::iterator{unbox(state).config(group_id).begin()}; return it; } @@ -245,7 +251,7 @@ LIBSESSION_C_API void groups_members_iterator_free(groups_members_iterator* it) } LIBSESSION_C_API bool groups_members_iterator_done( - groups_members_iterator* it, config_group_member* c) { + groups_members_iterator* it, state_group_member* c) { auto& real = *static_cast(it->_internals); if (real.done()) return true; @@ -256,3 +262,5 @@ LIBSESSION_C_API bool groups_members_iterator_done( LIBSESSION_C_API void groups_members_iterator_advance(groups_members_iterator* it) { ++*static_cast(it->_internals); } + +} // extern "C" diff --git a/src/config/internal.hpp b/src/config/internal.hpp index 8872ea4c..b0d333c4 100644 --- a/src/config/internal.hpp +++ b/src/config/internal.hpp @@ -7,73 +7,13 @@ #include #include -#include "session/config/base.h" +#include "session/config.h" #include "session/config/base.hpp" #include "session/config/error.h" #include "session/types.hpp" namespace session::config { -template -[[nodiscard]] int c_wrapper_init_generic(config_object** conf, char* error, Args&&... args) { - auto c = std::make_unique>(); - auto c_conf = std::make_unique(); - - try { - c->config = std::make_unique(std::forward(args)...); - } catch (const std::exception& e) { - if (error) { - std::string msg = e.what(); - if (msg.size() > 255) - msg.resize(255); - std::memcpy(error, msg.c_str(), msg.size() + 1); - } - return SESSION_ERR_INVALID_DUMP; - } - - c_conf->internals = c.release(); - c_conf->last_error = nullptr; - *conf = c_conf.release(); - return SESSION_ERR_NONE; -} - -template -[[nodiscard]] int c_wrapper_init( - config_object** conf, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dumpstr, - size_t dumplen, - char* error) { - assert(ed25519_secretkey_bytes); - ustring_view ed25519_secretkey{ed25519_secretkey_bytes, 64}; - std::optional dump; - if (dumpstr && dumplen) - dump.emplace(dumpstr, dumplen); - return c_wrapper_init_generic(conf, error, ed25519_secretkey, dump); -} - -template -[[nodiscard]] int c_group_wrapper_init( - config_object** conf, - const unsigned char* ed25519_pubkey_bytes, - const unsigned char* ed25519_secretkey_bytes, - const unsigned char* dump_bytes, - size_t dumplen, - char* error) { - - assert(ed25519_pubkey_bytes); - - ustring_view ed25519_pubkey{ed25519_pubkey_bytes, 32}; - std::optional ed25519_secretkey; - if (ed25519_secretkey_bytes) - ed25519_secretkey.emplace(ed25519_secretkey_bytes, 64); - std::optional dump; - if (dump_bytes && dumplen) - dump.emplace(dump_bytes, dumplen); - - return c_wrapper_init_generic(conf, error, ed25519_pubkey, ed25519_secretkey, dump); -} - template void copy_c_str(char (&dest)[N], std::string_view src) { if (src.size() >= N) @@ -82,34 +22,34 @@ void copy_c_str(char (&dest)[N], std::string_view src) { dest[src.size()] = 0; } -// Copies a container of std::strings into a self-contained malloc'ed config_string_list for +// Copies a container of std::strings into a self-contained malloc'ed session_string_list for // returning to C code with the strings and pointers of the string list in the same malloced space, // hanging off the end (so that everything, including string values, is freed by a single `free()`). template < typename Container, typename = std::enable_if_t>> -config_string_list* make_string_list(Container vals) { - // We malloc space for the config_string_list struct itself, plus the required number of string +session_string_list* make_string_list(Container vals) { + // We malloc space for the session_string_list struct itself, plus the required number of string // pointers to store its strings, and the space to actually contain a copy of the string data. // When we're done, the malloced memory we grab is going to look like this: // - // {config_string_list} + // {session_string_list} // {pointer1}{pointer2}... // {string data 1\0}{string data 2\0}... // - // where config_string_list.value points at the beginning of {pointer1}, and each pointerN + // where session_string_list.value points at the beginning of {pointer1}, and each pointerN // points at the beginning of the {string data N\0} c string. // // Since we malloc it all at once, when the user frees it, they also free the entire thing. - size_t sz = sizeof(config_string_list) + vals.size() * sizeof(char*); + size_t sz = sizeof(session_string_list) + vals.size() * sizeof(char*); // plus, for each string, the space to store it (including the null) for (auto& v : vals) sz += v.size() + 1; - auto* ret = static_cast(std::malloc(sz)); + auto* ret = static_cast(std::malloc(sz)); ret->len = vals.size(); - static_assert(alignof(config_string_list) >= alignof(char*)); + static_assert(alignof(session_string_list) >= alignof(char*)); // value points at the space immediately after the struct itself, which is the first element in // the array of c string pointers. diff --git a/src/config/protos.cpp b/src/config/protos.cpp index affcc83c..855784bb 100644 --- a/src/config/protos.cpp +++ b/src/config/protos.cpp @@ -8,6 +8,7 @@ #include "SessionProtos.pb.h" #include "WebSocketResources.pb.h" +#include "session/config/namespaces.hpp" #include "session/session_encrypt.hpp" namespace session::config::protos { diff --git a/src/config/user_groups.cpp b/src/config/user_groups.cpp index 9b08aa0c..c2dc96ce 100644 --- a/src/config/user_groups.cpp +++ b/src/config/user_groups.cpp @@ -16,6 +16,8 @@ #include "session/config/error.h" #include "session/config/user_groups.h" #include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" #include "session/util.hpp" @@ -456,6 +458,7 @@ bool UserGroups::erase(const community_info& c) { server_info.erase(); } } + return gone; } bool UserGroups::erase(const group_info& c) { @@ -604,158 +607,175 @@ UserGroups::iterator& UserGroups::iterator::operator++() { } // namespace session::config using namespace session::config; +using namespace session::state; extern "C" { struct user_groups_iterator { UserGroups::iterator it; }; -} +} // extern "C" -LIBSESSION_C_API -int user_groups_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); +namespace { +template +bool user_groups_it_is_impl(user_groups_iterator* it, C* c) { + auto& convo = *it->it; + if (auto* d = std::get_if(&convo)) { + d->into(*c); + return true; + } + return false; } +} // namespace + +extern "C" { -LIBSESSION_C_API bool user_groups_get_community( - config_object* conf, ugroups_community_info* comm, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_get_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error) { try { - conf->last_error = nullptr; - if (auto c = unbox(conf)->get_community(base_url, room)) { - c->into(*comm); + if (auto g = unbox(state).config().get_group(group_id)) { + g->into(*group); return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool user_groups_get_or_construct_community( - config_object* conf, - ugroups_community_info* comm, - const char* base_url, - const char* room, - unsigned const char* pubkey) { + +LIBSESSION_C_API bool state_get_or_construct_ugroups_group( + const state_object* state, ugroups_group_info* group, const char* group_id, char* error) { try { - conf->last_error = nullptr; - unbox(conf) - ->get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) - .into(*comm); + unbox(state).config().get_or_construct_group(group_id).into(*group); return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API bool user_groups_get_group( - config_object* conf, ugroups_group_info* group, const char* group_id) { + +LIBSESSION_C_API bool state_get_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + char* error) { try { - conf->last_error = nullptr; - if (auto g = unbox(conf)->get_group(group_id)) { - g->into(*group); + if (auto c = unbox(state).config().get_community(base_url, room)) { + c->into(*comm); return true; } } catch (const std::exception& e) { - set_error(conf, e.what()); + set_error_value(error, e.what()); } return false; } -LIBSESSION_C_API bool user_groups_get_or_construct_group( - config_object* conf, ugroups_group_info* group, const char* group_id) { + +LIBSESSION_C_API bool state_get_or_construct_ugroups_community( + const state_object* state, + ugroups_community_info* comm, + const char* base_url, + const char* room, + unsigned const char* pubkey, + char* error) { try { - conf->last_error = nullptr; - unbox(conf)->get_or_construct_group(group_id).into(*group); + unbox(state) + .config() + .get_or_construct_community(base_url, room, ustring_view{pubkey, 32}) + .into(*comm); return true; } catch (const std::exception& e) { - set_error(conf, e.what()); - return false; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { - if (group && group->_internal) { - delete static_cast(group->_internal); - group->_internal = nullptr; - } -} - -LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_legacy_group( - config_object* conf, const char* id) { +LIBSESSION_C_API bool state_get_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error) { try { - conf->last_error = nullptr; auto group = std::make_unique(); group->_internal = nullptr; - if (auto c = unbox(conf)->get_legacy_group(id)) { + if (auto c = unbox(state).config().get_legacy_group(id)) { std::move(c)->into(*group); - return group.release(); + *legacy_group_info = group.release(); + return true; } } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; + set_error_value(error, e.what()); } - return nullptr; + return false; } -LIBSESSION_C_API ugroups_legacy_group_info* user_groups_get_or_construct_legacy_group( - config_object* conf, const char* id) { +LIBSESSION_C_API bool state_get_or_construct_ugroups_legacy_group( + const state_object* state, + ugroups_legacy_group_info** legacy_group_info, + const char* id, + char* error) { try { - conf->last_error = nullptr; auto group = std::make_unique(); group->_internal = nullptr; - unbox(conf)->get_or_construct_legacy_group(id).into(*group); - return group.release(); + unbox(state).config().get_or_construct_legacy_group(id).into(*group); + *legacy_group_info = group.release(); + return true; } catch (const std::exception& e) { - copy_c_str(conf->_error_buf, e.what()); - conf->last_error = conf->_error_buf; - return nullptr; + return set_error_value(error, e.what()); } } -LIBSESSION_C_API void user_groups_set_community( - config_object* conf, const ugroups_community_info* comm) { - unbox(conf)->set(community_info{*comm}); +LIBSESSION_C_API void state_set_ugroups_community( + mutable_user_state_object* state, const ugroups_community_info* comm) { + unbox(state).user_groups.set(community_info{*comm}); } -LIBSESSION_C_API void user_groups_set_group(config_object* conf, const ugroups_group_info* group) { - unbox(conf)->set(group_info{*group}); + +LIBSESSION_C_API void state_set_ugroups_group( + mutable_user_state_object* state, const ugroups_group_info* group) { + unbox(state).user_groups.set(group_info{*group}); } -LIBSESSION_C_API void user_groups_set_legacy_group( - config_object* conf, const ugroups_legacy_group_info* group) { - unbox(conf)->set(legacy_group_info{*group}); + +LIBSESSION_C_API void state_set_ugroups_legacy_group( + mutable_user_state_object* state, const ugroups_legacy_group_info* group) { + unbox(state).user_groups.set(legacy_group_info{*group}); } -LIBSESSION_C_API void user_groups_set_free_legacy_group( - config_object* conf, ugroups_legacy_group_info* group) { - unbox(conf)->set(legacy_group_info{std::move(*group)}); + +LIBSESSION_C_API void state_set_free_ugroups_legacy_group( + mutable_user_state_object* state, ugroups_legacy_group_info* group) { + unbox(state).user_groups.set(legacy_group_info{std::move(*group)}); } -LIBSESSION_C_API bool user_groups_erase_community( - config_object* conf, const char* base_url, const char* room) { +LIBSESSION_C_API bool state_erase_ugroups_community( + mutable_user_state_object* state, const char* base_url, const char* room) { try { - return unbox(conf)->erase_community(base_url, room); + return unbox(state).user_groups.erase_community(base_url, room); } catch (...) { return false; } } -LIBSESSION_C_API bool user_groups_erase_group(config_object* conf, const char* group_id) { + +LIBSESSION_C_API bool state_erase_ugroups_group( + mutable_user_state_object* state, const char* group_id) { try { - return unbox(conf)->erase_group(group_id); + return unbox(state).user_groups.erase_group(group_id); } catch (...) { return false; } } -LIBSESSION_C_API bool user_groups_erase_legacy_group(config_object* conf, const char* group_id) { + +LIBSESSION_C_API bool state_erase_ugroups_legacy_group( + mutable_user_state_object* state, const char* group_id) { try { - return unbox(conf)->erase_legacy_group(group_id); + return unbox(state).user_groups.erase_legacy_group(group_id); } catch (...) { return false; } } +LIBSESSION_C_API void ugroups_legacy_group_free(ugroups_legacy_group_info* group) { + if (group && group->_internal) { + delete static_cast(group->_internal); + group->_internal = nullptr; + } +} + LIBSESSION_C_API void ugroups_group_set_kicked(ugroups_group_info* group) { assert(group); group->have_auth_data = false; @@ -793,19 +813,16 @@ LIBSESSION_C_API bool ugroups_legacy_members_next( return false; } -LIBSESSION_C_API -void ugroups_legacy_members_erase(ugroups_legacy_members_iterator* it) { +LIBSESSION_C_API void ugroups_legacy_members_erase(ugroups_legacy_members_iterator* it) { it->it = it->members.erase(it->it); it->need_advance = false; } -LIBSESSION_C_API -void ugroups_legacy_members_free(ugroups_legacy_members_iterator* it) { +LIBSESSION_C_API void ugroups_legacy_members_free(ugroups_legacy_members_iterator* it) { delete it; } -LIBSESSION_C_API -bool ugroups_legacy_member_add( +LIBSESSION_C_API bool ugroups_legacy_member_add( ugroups_legacy_group_info* group, const char* session_id, bool admin) { try { check_session_id(session_id); @@ -823,8 +840,8 @@ bool ugroups_legacy_member_add( return true; } -LIBSESSION_C_API -bool ugroups_legacy_member_remove(ugroups_legacy_group_info* group, const char* session_id) { +LIBSESSION_C_API bool ugroups_legacy_member_remove( + ugroups_legacy_group_info* group, const char* session_id) { return static_cast(group->_internal)->members.erase(session_id); } @@ -849,33 +866,36 @@ LIBSESSION_C_API size_t ugroups_legacy_members_count( return mems.size(); } -LIBSESSION_C_API size_t user_groups_size(const config_object* conf) { - return unbox(conf)->size(); +LIBSESSION_C_API size_t state_size_ugroups(const state_object* state) { + return unbox(state).config().size(); } -LIBSESSION_C_API size_t user_groups_size_communities(const config_object* conf) { - return unbox(conf)->size_communities(); + +LIBSESSION_C_API size_t state_size_ugroups_communities(const state_object* state) { + return unbox(state).config().size_communities(); } -LIBSESSION_C_API size_t user_groups_size_groups(const config_object* conf) { - return unbox(conf)->size_groups(); + +LIBSESSION_C_API size_t state_size_ugroups_groups(const state_object* state) { + return unbox(state).config().size_groups(); } -LIBSESSION_C_API size_t user_groups_size_legacy_groups(const config_object* conf) { - return unbox(conf)->size_legacy_groups(); + +LIBSESSION_C_API size_t state_size_ugroups_legacy_groups(const state_object* state) { + return unbox(state).config().size_legacy_groups(); } -LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new(const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin()}}; +LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new(const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin()}}; } LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_communities( - const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_communities()}}; + const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_communities()}}; } -LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_groups(const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_groups()}}; +LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_groups(const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_groups()}}; } LIBSESSION_C_API user_groups_iterator* user_groups_iterator_new_legacy_groups( - const config_object* conf) { - return new user_groups_iterator{{unbox(conf)->begin_legacy_groups()}}; + const state_object* state) { + return new user_groups_iterator{{unbox(state).config().begin_legacy_groups()}}; } LIBSESSION_C_API void user_groups_iterator_free(user_groups_iterator* it) { @@ -890,18 +910,6 @@ LIBSESSION_C_API void user_groups_iterator_advance(user_groups_iterator* it) { ++it->it; } -namespace { -template -bool user_groups_it_is_impl(user_groups_iterator* it, C* c) { - auto& convo = *it->it; - if (auto* d = std::get_if(&convo)) { - d->into(*c); - return true; - } - return false; -} -} // namespace - LIBSESSION_C_API bool user_groups_it_is_community( user_groups_iterator* it, ugroups_community_info* c) { return user_groups_it_is_impl(it, c); @@ -915,3 +923,5 @@ LIBSESSION_C_API bool user_groups_it_is_legacy_group( user_groups_iterator* it, ugroups_legacy_group_info* g) { return user_groups_it_is_impl(it, g); } + +} // extern "C" \ No newline at end of file diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index 1c5a3df2..5eebd001 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -1,54 +1,30 @@ -#include "session/config/user_profile.h" +#include "session/config/user_profile.hpp" #include #include "internal.hpp" -#include "session/config/error.h" -#include "session/config/user_profile.hpp" -#include "session/export.h" +#include "session/config/user_profile.h" +#include "session/state.h" +#include "session/state.hpp" #include "session/types.hpp" using namespace session::config; using session::ustring_view; -LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; - UserProfile::UserProfile(ustring_view ed25519_secretkey, std::optional dumped) : ConfigBase{dumped} { load_key(ed25519_secretkey); } -LIBSESSION_C_API int user_profile_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); -} - std::optional UserProfile::get_name() const { if (auto* s = data["n"].string(); s && !s->empty()) return *s; return std::nullopt; } -LIBSESSION_C_API const char* user_profile_get_name(const config_object* conf) { - if (auto s = unbox(conf)->get_name()) - return s->data(); - return nullptr; -} void UserProfile::set_name(std::string_view new_name) { set_nonempty_str(data["n"], new_name); } -LIBSESSION_C_API int user_profile_set_name(config_object* conf, const char* name) { - try { - unbox(conf)->set_name(name); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - return 0; -} profile_pic UserProfile::get_profile_pic() const { profile_pic pic{}; @@ -59,17 +35,6 @@ profile_pic UserProfile::get_profile_pic() const { return pic; } -LIBSESSION_C_API user_profile_pic user_profile_get_pic(const config_object* conf) { - user_profile_pic p; - if (auto pic = unbox(conf)->get_profile_pic(); pic) { - copy_c_str(p.url, pic.url); - std::memcpy(p.key, pic.key.data(), 32); - } else { - p.url[0] = 0; - } - return p; -} - void UserProfile::set_profile_pic(std::string_view url, ustring_view key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); } @@ -78,21 +43,6 @@ void UserProfile::set_profile_pic(profile_pic pic) { set_profile_pic(pic.url, pic.key); } -LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic pic) { - std::string_view url{pic.url}; - ustring_view key; - if (!url.empty()) - key = {pic.key, 32}; - - try { - unbox(conf)->set_profile_pic(url, key); - } catch (const std::exception& e) { - return set_error(conf, SESSION_ERR_BAD_VALUE, e); - } - - return 0; -} - void UserProfile::set_nts_priority(int priority) { set_nonzero_int(data["+"], priority); } @@ -101,14 +51,6 @@ int UserProfile::get_nts_priority() const { return data["+"].integer_or(0); } -LIBSESSION_C_API int user_profile_get_nts_priority(const config_object* conf) { - return unbox(conf)->get_nts_priority(); -} - -LIBSESSION_C_API void user_profile_set_nts_priority(config_object* conf, int priority) { - unbox(conf)->set_nts_priority(priority); -} - void UserProfile::set_nts_expiry(std::chrono::seconds expiry) { set_positive_int(data["e"], expiry.count()); } @@ -119,14 +61,6 @@ std::optional UserProfile::get_nts_expiry() const { return std::nullopt; } -LIBSESSION_C_API int user_profile_get_nts_expiry(const config_object* conf) { - return unbox(conf)->get_nts_expiry().value_or(0s).count(); -} - -LIBSESSION_C_API void user_profile_set_nts_expiry(config_object* conf, int expiry) { - unbox(conf)->set_nts_expiry(std::max(0, expiry) * 1s); -} - void UserProfile::set_blinded_msgreqs(std::optional value) { if (!value) data["M"].erase(); @@ -140,15 +74,70 @@ std::optional UserProfile::get_blinded_msgreqs() const { return std::nullopt; } -LIBSESSION_C_API int user_profile_get_blinded_msgreqs(const config_object* conf) { - if (auto opt = unbox(conf)->get_blinded_msgreqs()) +using namespace session::state; + +extern "C" { + +LIBSESSION_C_API const char* state_get_profile_name(const state_object* state) { + if (auto s = unbox(state).config().get_name()) + return s->data(); + return nullptr; +} + +LIBSESSION_C_API void state_set_profile_name(mutable_user_state_object* state, const char* name) { + unbox(state).user_profile.set_name(name); +} + +LIBSESSION_C_API user_profile_pic state_get_profile_pic(const state_object* state) { + user_profile_pic p; + if (auto pic = unbox(state).config().get_profile_pic(); pic) { + copy_c_str(p.url, pic.url); + std::memcpy(p.key, pic.key.data(), 32); + } else { + p.url[0] = 0; + } + return p; +} + +LIBSESSION_C_API void state_set_profile_pic( + mutable_user_state_object* state, user_profile_pic pic) { + std::string_view url{pic.url}; + ustring_view key; + if (!url.empty()) + key = {pic.key, 32}; + + unbox(state).user_profile.set_profile_pic(url, key); +} + +LIBSESSION_C_API int state_get_profile_nts_priority(const state_object* state) { + return unbox(state).config().get_nts_priority(); +} + +LIBSESSION_C_API void state_set_profile_nts_priority( + mutable_user_state_object* state, int priority) { + unbox(state).user_profile.set_nts_priority(priority); +} + +LIBSESSION_C_API int state_get_profile_nts_expiry(const state_object* state) { + return unbox(state).config().get_nts_expiry().value_or(0s).count(); +} + +LIBSESSION_C_API void state_set_profile_nts_expiry(mutable_user_state_object* state, int expiry) { + unbox(state).user_profile.set_nts_expiry(std::max(0, expiry) * 1s); +} + +LIBSESSION_C_API int state_get_profile_blinded_msgreqs(const state_object* state) { + if (auto opt = unbox(state).config().get_blinded_msgreqs()) return static_cast(*opt); return -1; } -LIBSESSION_C_API void user_profile_set_blinded_msgreqs(config_object* conf, int enabled) { +LIBSESSION_C_API void state_set_profile_blinded_msgreqs( + mutable_user_state_object* state, int enabled) { std::optional val; if (enabled >= 0) val = static_cast(enabled); - unbox(conf)->set_blinded_msgreqs(std::move(val)); + unbox(state).user_profile.set_blinded_msgreqs(val); } + +} // extern "C" \ No newline at end of file diff --git a/src/curve25519.cpp b/src/curve25519.cpp index 81870cc3..7c0035db 100644 --- a/src/curve25519.cpp +++ b/src/curve25519.cpp @@ -1,5 +1,6 @@ #include "session/curve25519.hpp" +#include #include #include @@ -11,6 +12,9 @@ namespace session::curve25519 { std::pair, std::array> curve25519_key_pair() { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + std::array curve_pk; std::array curve_sk; crypto_box_keypair(curve_pk.data(), curve_sk.data()); diff --git a/src/ed25519.cpp b/src/ed25519.cpp index 87297b09..d3a6f0f9 100644 --- a/src/ed25519.cpp +++ b/src/ed25519.cpp @@ -1,5 +1,6 @@ #include "session/ed25519.hpp" +#include #include #include @@ -17,6 +18,9 @@ using uc32 = std::array; using cleared_uc64 = cleared_array<64>; std::pair, std::array> ed25519_key_pair() { + if (sodium_init() == -1) + throw std::runtime_error{"libsodium initialization failed!"}; + std::array ed_pk; std::array ed_sk; crypto_sign_ed25519_keypair(ed_pk.data(), ed_sk.data()); diff --git a/src/errors.cpp b/src/errors.cpp new file mode 100644 index 00000000..67f97b11 --- /dev/null +++ b/src/errors.cpp @@ -0,0 +1,10 @@ +#include "session/errors.hpp" + +#include "session/errors.h" +#include "session/export.h" + +namespace session { + +LIBSESSION_C_API const char* SESSION_ERROR_READ_ONLY_CONFIG = Error::READ_ONLY_CONFIG; + +} // namespace session diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp index 2ace3237..c0a917d8 100644 --- a/src/onionreq/response_parser.cpp +++ b/src/onionreq/response_parser.cpp @@ -38,7 +38,7 @@ ustring ResponseParser::decrypt(ustring ciphertext) const { ciphertext, destination_x25519_public_key_); else - throw e; + throw; } } diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 88bc4e7a..5c8f5255 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -594,6 +594,8 @@ ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { using namespace session; +extern "C" { + LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( const unsigned char* plaintext_in, size_t plaintext_len, @@ -754,3 +756,5 @@ LIBSESSION_C_API bool session_decrypt_push_notification( return false; } } + +} // extern "C" diff --git a/src/state.cpp b/src/state.cpp new file mode 100644 index 00000000..341a8f22 --- /dev/null +++ b/src/state.cpp @@ -0,0 +1,1366 @@ +#include "session/state.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "config/internal.hpp" +#include "session/config/base.hpp" +#include "session/config/contacts.hpp" +#include "session/config/convo_info_volatile.hpp" +#include "session/config/groups/keys.hpp" +#include "session/config/groups/members.hpp" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" +#include "session/config/user_groups.hpp" +#include "session/config/user_profile.hpp" +#include "session/export.h" +#include "session/state.h" +#include "session/util.hpp" + +using namespace std::literals; +using namespace session::config; + +namespace session::state { + +GroupConfigs::GroupConfigs( + ustring_view pubkey, ustring_view user_sk, std::optional ed25519_secretkey) { + info = std::make_unique(pubkey, ed25519_secretkey, std::nullopt); + members = std::make_unique(pubkey, ed25519_secretkey, std::nullopt); + keys = std::make_unique( + user_sk, pubkey, ed25519_secretkey, std::nullopt, *info, *members); +} + +State::State(ustring_view ed25519_secretkey, std::vector dumps) { + 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; + 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"}; + + _user_x_pk_hex.reserve(66); + _user_x_pk_hex += "05"; + oxenc::to_hex(user_x_pk.begin(), user_x_pk.end(), std::back_inserter(_user_x_pk_hex)); + + // Load in the dumps + auto sorted_dumps = dumps; + std::sort(sorted_dumps.begin(), sorted_dumps.end(), [](const auto& a, const auto& b) { + return namespace_load_order(a.namespace_) < namespace_load_order(b.namespace_); + }); + + for (auto dump : sorted_dumps) { + load(dump.namespace_, dump.pubkey_hex, dump.data); + } + + // Initialise empty config states for any missing required config types + if (!_config_contacts) { + _config_contacts = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_contacts); + } + + if (!_config_convo_info_volatile) { + _config_convo_info_volatile = + std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_convo_info_volatile); + } + + if (!_config_user_groups) { + _config_user_groups = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_user_groups); + } + + if (!_config_user_profile) { + _config_user_profile = std::make_unique(ed25519_secretkey, std::nullopt); + add_child_logger(_config_user_profile); + } + + // If we have a group in the 'user_groups' that isn't in the 'invited' state but didn't have a + // dump for it then most likely the group has been approved but we haven't completed the initial + // poll - in this case we want to create the group configs because we can assume that the client + // will try to poll and merge the state into the group + for (auto group_it = _config_user_groups->begin_groups(); + group_it != _config_user_groups->end(); + ++group_it) { + if (group_it->invited) + continue; + + std::optional group_sk; + + if (!group_it->secretkey.empty()) + group_sk = {group_it->secretkey.data(), group_it->secretkey.size()}; + + if (auto [it, b] = _config_groups.try_emplace(group_it->id, nullptr); b) { + auto ed_pk_data = oxenc::from_hex(group_it->id.begin() + 2, group_it->id.end()); + auto ed_pk = to_unsigned_sv(ed_pk_data); + _config_groups[group_it->id] = + std::make_unique(ed_pk, to_unsigned_sv(_user_sk), group_sk); + } + } +} + +void State::load( + Namespace namespace_, std::optional pubkey_hex_, ustring_view dump) { + switch (namespace_) { + case Namespace::Contacts: + _config_contacts = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(_config_contacts); + return; + + case Namespace::ConvoInfoVolatile: + _config_convo_info_volatile = std::make_unique( + to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(_config_convo_info_volatile); + return; + + case Namespace::UserGroups: + _config_user_groups = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(_config_user_groups); + return; + + case Namespace::UserProfile: + _config_user_profile = + std::make_unique(to_unsigned_sv({_user_sk.data(), 64}), dump); + add_child_logger(_config_user_profile); + return; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + throw std::invalid_argument{ + "load: Invalid pubkey_hex - required for group config namespaces"}; + if (pubkey_hex_->size() != 66) + throw std::invalid_argument{"load: Invalid pubkey_hex - expected 66 bytes"}; + + // Retrieve any keys for the group + std::string_view pubkey_hex = *pubkey_hex_; + auto user_group_info = _config_user_groups->get_group(pubkey_hex); + + if (!user_group_info) + throw std::runtime_error{ + "Unable to retrieve group " + std::string(pubkey_hex) + " from user_groups config"}; + + auto pubkey = session_id_pk(pubkey_hex, "03"); + std::string gid = {pubkey_hex.data(), pubkey_hex.size()}; + ustring_view pubkey_sv = to_unsigned_sv(pubkey); + ustring_view user_ed25519_secretkey = {_user_sk.data(), 64}; + 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(gid, 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[gid] = std::make_unique(pubkey_sv, user_ed25519_secretkey); + } + + // Reload the specified namespace with the dump + if (namespace_ == Namespace::GroupInfo) { + _config_groups.at(gid)->info = + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); + add_child_logger(_config_groups.at(gid)->info); + } else if (namespace_ == Namespace::GroupMembers) { + _config_groups.at(gid)->members = + std::make_unique(pubkey_sv, group_ed25519_secretkey, dump); + add_child_logger(_config_groups.at(gid)->members); + } else if (namespace_ == Namespace::GroupKeys) { + auto info = _config_groups.at(gid)->info.get(); + auto members = _config_groups.at(gid)->members.get(); + auto keys = std::make_unique( + user_ed25519_secretkey, pubkey_sv, group_ed25519_secretkey, dump, *info, *members); + _config_groups.at(gid)->keys = std::move(keys); + } else + throw std::runtime_error{"Attempted to load unknown namespace"}; +} + +bool State::has_pending_send() const { + bool needs_push = + (_config_contacts->needs_push() || _config_convo_info_volatile->needs_push() || + _config_user_groups->needs_push() || _config_user_profile->needs_push()); + + if (!needs_push) { + for (const auto& it : _config_groups) { + needs_push = + (it.second->keys->admin() && + (it.second->info->needs_push() || it.second->members->needs_push() || + it.second->keys->pending_config())); + + if (needs_push) + break; + } + } + + return needs_push; +} + +void State::config_changed( + std::optional pubkey_hex, + bool allow_store, + bool allow_send, + std::optional server_timestamp_ms, + std::optional> + after_send) { + auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); + std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); + + std::string info_title = "User configs"; + bool needs_push = false; + bool needs_dump = false; + std::vector configs; + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + if (!is_group_pubkey) { + needs_push = + (allow_send && + (_config_contacts->needs_push() || _config_convo_info_volatile->needs_push() || + _config_user_groups->needs_push() || _config_user_profile->needs_push())); + needs_dump = + (allow_store && + (_config_contacts->needs_dump() || _config_convo_info_volatile->needs_dump() || + _config_user_groups->needs_dump() || _config_user_profile->needs_dump())); + configs = { + _config_contacts.get(), + _config_convo_info_volatile.get(), + _config_user_groups.get(), + _config_user_profile.get()}; + } else { + // Other namespaces are unique for a given pubkey_hex_ + if (target_pubkey_hex.size() != 66) + throw std::invalid_argument{"config_changed: Invalid pubkey_hex - expected 66 bytes"}; + + // Ensure we have the admin key for the group + auto user_group_info = _config_user_groups->get_group(target_pubkey_hex); + + if (!user_group_info) + throw std::runtime_error{ + "config_changed: Unable to retrieve group " + target_pubkey_hex + + " from user_groups config"}; + + // Only group admins can push group config changes + auto& group = _config_groups.at(target_pubkey_hex); + needs_push = + (allow_send && !user_group_info->secretkey.empty() && + (group->info->needs_push() || group->members->needs_push() || + group->keys->pending_config())); + needs_dump = + (allow_store && (group->info->needs_dump() || group->members->needs_dump() || + group->keys->needs_dump())); + configs = {group->info.get(), group->members.get()}; + info_title = "Group configs for " + target_pubkey_hex; + } + + std::string send_info = + (!allow_send ? "send suppressed" : ("needs send: " + bool_to_string(needs_push))); + std::string store_info = + (!allow_store ? "store suppressed" : ("needs store: " + bool_to_string(needs_dump))); + log(LogLevel::debug, + "config_changed: " + info_title + " (" + send_info + ", " + store_info + ")"); + + // Call the hook to store the dump if needed + if (_store && needs_dump && allow_store) { + std::chrono::milliseconds store_timestamp = + std::chrono::milliseconds(server_timestamp_ms.value_or(timestamp.count())); + std::vector> sorted_stores; + + for (auto& config : configs) { + if (config->needs_dump()) + sorted_stores.emplace_back(config->storage_namespace(), config->dump()); + } + + // GroupKeys needs special handling as it's not a `ConfigBase` + if (is_group_pubkey && _config_groups.at(target_pubkey_hex)->keys->needs_dump()) { + auto config = _config_groups.at(target_pubkey_hex)->keys.get(); + sorted_stores.emplace_back(config->storage_namespace(), config->dump()); + } + + // Sort the namespaces based on the order they should be merged in to minimise the chance + // that config messages dependant on others are saved before their dependencies + std::sort(sorted_stores.begin(), sorted_stores.end(), [](const auto& a, const auto& b) { + return namespace_merge_order(a.first) < namespace_merge_order(b.first); + }); + + for (auto& info : sorted_stores) { + log(LogLevel::debug, + "config_changed: call 'store' for " + namespace_name(info.first) + " in " + + target_pubkey_hex); + _store(info.first, target_pubkey_hex, store_timestamp.count(), info.second); + } + } + + // Call the hook to perform a push if needed + if (_send && needs_push && allow_send) { + auto push = prepare_push(target_pubkey_hex, timestamp, configs); + + log(LogLevel::debug, "config_changed: Call 'send'"); + _send(target_pubkey_hex, + push.payload, + [this, pubkey = target_pubkey_hex, push, after_send = std::move(after_send)]( + bool success, uint16_t status_code, ustring response) { + handle_config_push_response(pubkey, push.info, success, status_code, response); + + // Now that we have confirmed the push we need to store the configs again + config_changed(pubkey, true, false, std::nullopt); + + // Call the 'after_send' callback if provided + if (after_send) + (*after_send)(success, status_code, response); + }); + } + log(LogLevel::debug, "config_changed: Complete"); +} + +PreparedPush State::prepare_push( + std::string pubkey_hex, + std::chrono::milliseconds timestamp, + std::vector configs, + std::optional group_sk) { + auto is_group_pubkey = (!pubkey_hex.empty() && pubkey_hex.substr(0, 2) != "05"); + std::vector requests; + std::vector obsolete_hashes; + std::array seckey; + + // Prepare for signing + if (is_group_pubkey) { + // If we were given an explicit secret key then use that, otherwise retrieve it from the + // user groups config + if (group_sk) + memcpy(seckey.data(), group_sk->data(), 64); + else { + auto config = _config_groups.at(pubkey_hex)->keys.get(); + auto user_group = _config_user_groups->get_group(pubkey_hex); + + if (!config->admin() || !user_group || user_group->secretkey.empty()) + throw std::runtime_error{ + "prepare_push: Only groups admins can push config changes"}; + + memcpy(seckey.data(), user_group->secretkey.data(), 64); + } + } else + memcpy(seckey.data(), _user_sk.data(), 64); + + // Check the configs for changes + for (auto& config : configs) { + if (!config->needs_push()) + continue; + + log(LogLevel::debug, + "prepare_push: generate push for " + namespace_name(config->storage_namespace()) + + ", (" + pubkey_hex + ")"); + auto [seqno, msg, obs] = config->push(); + + for (auto hash : obs) + obsolete_hashes.emplace_back(hash); + + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store"); + verification += + to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); + + nlohmann::json params{ + {"namespace", static_cast(config->storage_namespace())}, + {"pubkey", pubkey_hex}, + {"ttl", config->default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(msg)}, + }; + + // Sign the request + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), seckey.data())) + throw std::runtime_error{ + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; + + params["signature"] = oxenc::to_base64(sig.begin(), sig.end()); + + // For user config storage we also need to add `pubkey_ed25519` + if (!is_group_pubkey) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + // Add the 'seqno' temporarily to the params (this will be removed from the payload + // before sending but is needed for handling the push result) + params["seqno"] = seqno; + + requests.emplace_back(params); + } + + // GroupKeys needs special handling as it's not a `ConfigBase` + if (is_group_pubkey) { + auto config = _config_groups.at(pubkey_hex)->keys.get(); + auto pending = config->pending_config(); + + if (pending) { + log(LogLevel::debug, + "prepare_push: generate push for " + namespace_name(config->storage_namespace()) + + ", (" + pubkey_hex + ")"); + // Ed25519 signature of `("store" || namespace || timestamp)`, where namespace and + // `timestamp` are the base10 expression of the namespace and `timestamp` values + std::array sig; + ustring verification = to_unsigned("store"); + verification += + to_unsigned_sv(std::to_string(static_cast(config->storage_namespace()))); + verification += to_unsigned_sv(std::to_string(timestamp.count())); + + if (0 != crypto_sign_ed25519_detached( + sig.data(), + nullptr, + verification.data(), + verification.size(), + seckey.data())) + throw std::runtime_error{ + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"namespace", config->storage_namespace()}, + {"pubkey", pubkey_hex}, + {"ttl", config->default_ttl().count()}, + {"timestamp", timestamp.count()}, + {"data", oxenc::to_base64(*pending)}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + + // The 'GROUP_KEYS' push data doesn't need a 'seqno', but to avoid index + // out-of-bounds issues we add one anyway (this will be removed from the payload + // before sending but is needed for handling the push result) + params["seqno"] = 0; + + requests.emplace_back(params); + } + } + + // Sort the namespaces based on the order they should be stored in to minimise the chance + // that config messages dependant on others are stored before their dependencies + auto sorted_requests = requests; + std::sort(sorted_requests.begin(), sorted_requests.end(), [](const auto& a, const auto& b) { + return namespace_send_order(static_cast(a["namespace"])) < + namespace_send_order(static_cast(b["namespace"])); + }); + + std::vector push_info; + nlohmann::json sequence_params; + + for (auto& request : sorted_requests) { + push_info.push_back( + {true, + true, + request["namespace"].get(), + request["seqno"].get()}); + request.erase("seqno"); // Erase the 'seqno' as it shouldn't be in the request payload + + nlohmann::json request_json{{"method", "store"}, {"params", request}}; + sequence_params["requests"].push_back(request_json); + } + + // Also delete obsolete hashes + if (!obsolete_hashes.empty()) { + // Ed25519 signature of `("delete" || messages...)` + std::array sig; + ustring verification = to_unsigned("delete"); + log(LogLevel::debug, "prepare_push: has obsolete hashes"); + + for (auto& hash : obsolete_hashes) + verification += to_unsigned_sv(hash); + + if (0 != + crypto_sign_ed25519_detached( + sig.data(), nullptr, verification.data(), verification.size(), seckey.data())) + throw std::runtime_error{ + "prepare_push: Failed to sign; perhaps the secret key is invalid?"}; + + nlohmann::json params{ + {"messages", obsolete_hashes}, + {"pubkey", pubkey_hex}, + {"signature", oxenc::to_base64(sig.begin(), sig.end())}, + }; + + // For user config storage we also need to add `pubkey_ed25519` + if (!is_group_pubkey) + params["pubkey_ed25519"] = oxenc::to_hex(_user_pk.begin(), _user_pk.end()); + + nlohmann::json request_json{{"method", "delete"}, {"params", params}}; + sequence_params["requests"].push_back(request_json); + + // Not strictly needed but means the request count will match + push_info.push_back({false, false, Namespace::UserProfile, 0}); + } + + nlohmann::json payload{{"method", "sequence"}, {"params", sequence_params}}; + + return {to_unsigned(payload.dump()), push_info}; +} + +std::optional max_merged_timestamp( + const std::vector& messages, + const std::vector& merged_hashes) { + if (messages.empty() || merged_hashes.empty()) + return std::nullopt; + + // Filter messages based on merged_hashes + std::vector merged_messages; + std::copy_if( + messages.begin(), + messages.end(), + std::back_inserter(merged_messages), + [&merged_hashes](const config_message& msg) { + return std::find(merged_hashes.begin(), merged_hashes.end(), msg.hash) != + merged_hashes.end(); + }); + auto max_timestamp_message = std::max_element( + merged_messages.begin(), + merged_messages.end(), + [](const config_message& msg1, const config_message& msg2) { + return msg1.timestamp_ms < msg2.timestamp_ms; + }); + + if (max_timestamp_message != messages.end()) { + return max_timestamp_message->timestamp_ms; + } + + return std::nullopt; +} + +std::vector State::merge( + std::optional pubkey_hex, const std::vector& configs) { + log(LogLevel::debug, "merge: Called with " + std::to_string(configs.size()) + " configs"); + if (configs.empty()) + return {}; + + // Sort the namespaces based on the order they should be merged in to minimise conflicts between + // different config messages + auto sorted_configs = configs; + std::sort(sorted_configs.begin(), sorted_configs.end(), [](const auto& a, const auto& b) { + return namespace_merge_order(a.namespace_) < namespace_merge_order(b.namespace_); + }); + + bool is_group_merge = false; + std::vector good_hashes; + std::vector pending_configs; + auto is_group_pubkey = (pubkey_hex && !pubkey_hex->empty() && pubkey_hex->substr(0, 2) != "05"); + std::string target_pubkey_hex = (is_group_pubkey ? std::string(*pubkey_hex) : _user_x_pk_hex); + + // Sanity check the size of the pubkey + if (target_pubkey_hex.size() != 66) + throw std::invalid_argument{"merge: Invalid pubkey_hex - expected 66 bytes"}; + + for (size_t i = 0; i < sorted_configs.size(); ++i) { + auto& config = sorted_configs[i]; + + // If this is different from the last config, or it's a 'GroupKeys' config (GroupKeys + // only support individual merging) then clear 'pending_configs' so we can prepare for + // a new batch-merge + if (config.namespace_ == Namespace::GroupKeys || + (i > 0 && config.namespace_ != sorted_configs[i - 1].namespace_)) + pending_configs.clear(); + + pending_configs.emplace_back(config); + + // If this is not a GroupKeys config, the last config or the next config is not in the same + // namespace then go to the next loop so we can batch-merge the configs in a later loop + if (config.namespace_ != Namespace::GroupKeys && i != (sorted_configs.size() - 1) && + config.namespace_ == sorted_configs[i + 1].namespace_) + continue; + + // Process the previously grouped configs + log(LogLevel::debug, + "merge: Merging " + namespace_name(config.namespace_) + " config (" + + std::string(target_pubkey_hex) + ")"); + std::vector> to_merge; + to_merge.reserve(pending_configs.size()); + std::transform( + pending_configs.begin(), + pending_configs.end(), + std::back_inserter(to_merge), + [](const config_message& msg) { + return std::pair{msg.hash, msg.data}; + }); + + std::vector merged_hashes; + switch (config.namespace_) { + case Namespace::Contacts: + merged_hashes = _config_contacts->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::ConvoInfoVolatile: + merged_hashes = _config_convo_info_volatile->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::UserGroups: + merged_hashes = _config_user_groups->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::UserProfile: + merged_hashes = _config_user_profile->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex) + throw std::invalid_argument{ + "merge: Invalid pubkey_hex - required for group config namespaces"}; + + auto& group = _config_groups.at(target_pubkey_hex); + auto info = group->info.get(); + auto members = group->members.get(); + is_group_merge = true; + + switch (config.namespace_) { + case Namespace::GroupInfo: + merged_hashes = info->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::GroupMembers: + merged_hashes = members->merge(to_merge); + good_hashes.insert(good_hashes.end(), merged_hashes.begin(), merged_hashes.end()); + + // Immediately store changes if a merge was successful + if (auto timestamp_ms = max_merged_timestamp(pending_configs, merged_hashes)) + config_changed(target_pubkey_hex, true, false, timestamp_ms); + continue; + + case Namespace::GroupKeys: + // GroupKeys doesn't support merging multiple messages at once so do them + // individually + if (group->keys->load_key_message( + config.hash, config.data, config.timestamp_ms, *info, *members)) { + good_hashes.emplace_back(config.hash); + config_changed(target_pubkey_hex, true, false, config.timestamp_ms); + } + continue; + + default: throw std::runtime_error{"merge: Attempted to merge from unknown namespace"}; + } + } + + // If two admins rekeyed for different member changes at the same time then there is a "key + // collision" and the "needs rekey" function will return true to indicate that a 3rd `rekey` + // needs to be made to have a final set of keys which includes all members + if (is_group_merge) { + auto& group = _config_groups.at(target_pubkey_hex); + + if (group->keys->needs_rekey()) { + auto info = group->info.get(); + auto members = group->members.get(); + group->keys->rekey(*info, *members); + } + } + + // Now that all of the merges have been completed we want to trigger the `send` hook just in + // case if there is a pending push (the 'server_timestamp_ms' is only needed for the `store` + // hook so no need to pass here) + config_changed(target_pubkey_hex, false, true, std::nullopt); + + log(LogLevel::debug, "merge: Complete"); + return good_hashes; +} + +std::vector State::current_hashes(std::optional pubkey_hex) { + std::vector result; + + if (!pubkey_hex || pubkey_hex->empty() || pubkey_hex->substr(0, 2) == "05") { + auto contact_hashes = _config_contacts->current_hashes(); + auto convo_info_volatile_hashes = _config_convo_info_volatile->current_hashes(); + auto user_group_hashes = _config_user_groups->current_hashes(); + auto user_profile_hashes = _config_user_profile->current_hashes(); + result.insert(result.end(), contact_hashes.begin(), contact_hashes.end()); + result.insert( + result.end(), convo_info_volatile_hashes.begin(), convo_info_volatile_hashes.end()); + result.insert(result.end(), user_group_hashes.begin(), user_group_hashes.end()); + result.insert(result.end(), user_profile_hashes.begin(), user_profile_hashes.end()); + } else { + if (pubkey_hex->size() != 66) + throw std::invalid_argument{"current_hashes: Invalid pubkey_hex - expected 66 bytes"}; + + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); + auto info_hashes = group->info->current_hashes(); + auto members_hashes = group->members->current_hashes(); + auto keys_hashes = group->keys->current_hashes(); + result.insert(result.end(), info_hashes.begin(), info_hashes.end()); + result.insert(result.end(), members_hashes.begin(), members_hashes.end()); + result.insert(result.end(), keys_hashes.begin(), keys_hashes.end()); + } + + return result; +} + +ustring State::dump(bool full_dump) { + oxenc::bt_dict_producer combined; + + // NOTE: the keys have to be in ascii-sorted order: + if (full_dump || _config_contacts->needs_dump()) + combined.append("contacts", session::from_unsigned_sv(_config_contacts->dump())); + + if (full_dump || _config_convo_info_volatile->needs_dump()) + combined.append( + "convo_info_volatile", + session::from_unsigned_sv(_config_convo_info_volatile->dump())); + + if (full_dump || _config_user_groups->needs_dump()) + combined.append("user_groups", session::from_unsigned_sv(_config_user_groups->dump())); + + if (full_dump || _config_user_profile->needs_dump()) + combined.append("user_profile", session::from_unsigned_sv(_config_user_profile->dump())); + + // NOTE: `std::map` sorts keys in ascending order so can just add them in order + if (_config_groups.size() > 0) { + for (const auto& [key, config] : _config_groups) { + if (full_dump || config->info->needs_dump() || config->keys->needs_dump() || + config->members->needs_dump()) { + oxenc::bt_dict_producer group_combined = combined.append_dict(key); + + if (full_dump || config->info->needs_dump()) + group_combined.append("info", session::from_unsigned_sv(config->info->dump())); + + if (full_dump || config->keys->needs_dump()) + group_combined.append("keys", session::from_unsigned_sv(config->keys->dump())); + + if (full_dump || config->members->needs_dump()) + group_combined.append( + "members", session::from_unsigned_sv(config->members->dump())); + } + } + } + + auto to_dump = std::move(combined).str(); + + return session::ustring{to_unsigned_sv(to_dump)}; +} + +ustring State::dump(config::Namespace namespace_, std::optional pubkey_hex) { + switch (namespace_) { + case Namespace::Contacts: return _config_contacts->dump(); + case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->dump(); + case Namespace::UserGroups: return _config_user_groups->dump(); + case Namespace::UserProfile: return _config_user_profile->dump(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex + if (!pubkey_hex) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + + // Retrieve the group configs for this pubkey + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); + + switch (namespace_) { + case Namespace::GroupInfo: return group->info->dump(); + case Namespace::GroupMembers: return group->members->dump(); + case Namespace::GroupKeys: return group->keys->dump(); + default: throw std::runtime_error{"Attempted to load unknown namespace"}; + } +} + +std::optional extract_error(bool success, int status_code, ustring response) { + // If we have an explicit failure and there is no response data then return an error + if (!success && response.empty()) + return "Failed with status code: " + std::to_string(status_code) + "."; + else if (response.empty()) + return std::nullopt; // An empty response might be valid but we can't parse + + std::string response_string = {from_unsigned(response.data()), response.size()}; + + try { + auto response_json = nlohmann::json::parse(response); + + // If the status code for the root request failed then try to extract the 'reason', + // otherwise just return the response as the error + if (status_code < 200 || status_code > 299) { + if (response_json.contains("reason")) + return response_json["reason"].get(); + else + return response_string; + } + + // If it wasn't a batch/sequence request then assume it was successful and return no error + if (!response_json.contains("results")) + return std::nullopt; + + auto results = response_json["results"]; + + // Check if all of the results has the same status code + int single_status_code = -1; + std::optional error_body; + for (const auto& result : results.items()) { + // Invalid subresponse, just return the response as the error + if (!result.value().contains("code")) + return response_string; + + auto code = result.value()["code"].get(); + + // If the code was different from all former codes then there wasn't a single error (ie. + // it needs specific handling) so return no error + if (single_status_code != -1 && code != single_status_code) + return std::nullopt; + + single_status_code = code; + + if (result.value().contains("body") && result.value()["body"].is_string()) + error_body = result.value()["body"].get(); + } + + // Return the error if all results failed with the same error + if (single_status_code < 200 || single_status_code > 299) { + // Custom handle a clock out of sync error (v4 returns '425' but included the '406' just + // in case) + if (single_status_code == 406 || single_status_code == 425) + return "The user's clock is out of sync with the service node network."; + + return error_body.value_or( + "Failed with status code: " + std::to_string(single_status_code) + "."); + } + + return std::nullopt; + } catch (...) { + return response_string; + } +} + +void State::handle_config_push_response( + std::string pubkey, + std::vector push_info, + bool success, + uint16_t status_code, + ustring response) { + // If the request failed then just error + if (auto error = extract_error(success, status_code, response); error) + throw std::runtime_error{*error}; + + log(LogLevel::debug, "handle_config_push_response: No simple error detected."); + + // Otherwise process the response data + auto response_json = nlohmann::json::parse(response); + + if (!response_json.contains("results")) + throw std::invalid_argument{ + "handle_config_push_response: Invalid response - expected to contain 'results' " + "array"}; + if (response_json["results"].size() == 0) + throw std::invalid_argument{ + "handle_config_push_response: Invalid response - 'results' array is empty"}; + + // If the response includes a timestamp value then we should update the network offset + auto results = response_json["results"]; + + if (auto first_result = results[0]; + first_result.contains("body") && first_result["body"].contains("t")) + network_offset = + (std::chrono::milliseconds(first_result["body"]["t"].get()) - + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch())); + + size_t required_response_count = + std::count_if(push_info.begin(), push_info.end(), [](const PreparedPush::Info& info) { + return info.requires_response; + }); + if (results.size() < required_response_count) + throw std::invalid_argument{ + "handle_config_push_response: Invalid response - Number of responses smaller than " + "the number of requests requiring responses."}; + + for (int i = 0, n = results.size(); i < n; ++i) { + if (push_info.size() <= i || !push_info[i].is_config_push) + continue; + + auto result_code = results[i]["code"].get(); + + if (result_code < 200 || result_code > 299 || !results[i].contains("body") || + !results[i]["body"].contains("hash")) + continue; + + auto hash = results[i]["body"]["hash"].get(); + + switch (push_info[i].namespace_) { + case Namespace::Contacts: + _config_contacts->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::ConvoInfoVolatile: + _config_convo_info_volatile->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::UserGroups: + _config_user_groups->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::UserProfile: + _config_user_profile->confirm_pushed(push_info[i].seqno, hash); + continue; + default: break; + } + + // Other namespaces are unique for a given pubkey + auto& group = _config_groups.at(pubkey); + + switch (push_info[i].namespace_) { + case Namespace::GroupInfo: + group->info->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::GroupMembers: + group->members->confirm_pushed(push_info[i].seqno, hash); + continue; + case Namespace::GroupKeys: continue; // No need to do anything here + default: + throw std::runtime_error{ + "handle_config_push_response: Attempted to load unknown namespace"}; + } + } + + log(LogLevel::debug, "handle_config_push_response: Completed"); +} + +std::vector State::get_keys( + Namespace namespace_, std::optional pubkey_hex) { + switch (namespace_) { + case Namespace::Contacts: return _config_contacts->get_keys(); + case Namespace::ConvoInfoVolatile: return _config_convo_info_volatile->get_keys(); + case Namespace::UserGroups: return _config_user_groups->get_keys(); + case Namespace::UserProfile: return _config_user_profile->get_keys(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex + if (!pubkey_hex) + throw std::invalid_argument{ + "Invalid pubkey_hex: pubkey_hex required for group config namespaces"}; + if (pubkey_hex->size() != 64) + throw std::invalid_argument{"Invalid pubkey_hex: expected 64 bytes"}; + + // Retrieve the group configs for this pubkey + std::string gid = {pubkey_hex->data(), pubkey_hex->size()}; + auto& group = _config_groups.at(gid); + + switch (namespace_) { + case Namespace::GroupInfo: return group->info->get_keys(); + case Namespace::GroupMembers: return group->members->get_keys(); + case Namespace::GroupKeys: return group->keys->group_keys(); + default: throw std::runtime_error{"Attempted to load unknown namespace"}; + } +} + +void State::create_group( + std::string_view name, + std::optional description, + std::optional pic, + std::vector members_, + std::function error)> + callback) { + auto key_pair = ed25519::ed25519_key_pair(); + auto group_id = "03" + oxenc::to_hex(key_pair.first.begin(), key_pair.first.end()); + ustring ed_pk = {key_pair.first.data(), key_pair.first.size()}; + ustring ed_sk = {key_pair.second.data(), key_pair.second.size()}; + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + // Sanity check to avoid group collision + if (auto [it, b] = _config_groups.try_emplace(group_id, nullptr); b) { + _config_groups[group_id] = std::make_unique(ed_pk, to_unsigned_sv(_user_sk)); + } else { + throw std::runtime_error{"create_group: Tried to create group matching an existing group"}; + } + + // Store the group info + auto& group = _config_groups.at(group_id); + group->info = std::make_unique(ed_pk, ed_sk, std::nullopt); + group->info->set_name(name); + group->info->set_created(timestamp.count()); + + if (description) + group->info->set_description(*description); + + if (pic) + group->info->set_profile_pic(*pic); + + // Need to load the members before creating the Keys config to ensure they + // are included in the initial key rotation + group->members = std::make_unique(ed_pk, ed_sk, std::nullopt); + + // Insert the current user as a group admin + auto admin_member = groups::member{_user_x_pk_hex}; + admin_member.admin = true; + admin_member.profile_picture = _config_user_profile->get_profile_pic(); + + if (auto name = _config_user_profile->get_name()) + admin_member.name = *name; + + group->members->set(admin_member); + + // Add other members (ignore the current user if they happen to be included) + for (auto m : members_) + if (m.session_id != _user_x_pk_hex) + group->members->set(m); + + // Finally create the keys + auto info = group->info.get(); + auto members = group->members.get(); + group->keys = std::make_unique( + to_unsigned_sv(_user_sk), ed_pk, ed_sk, std::nullopt, *info, *members); + + // Prepare and trigger the push for the group configs (need to explicitly provide the 'ed_sk' + // here as we won't load the group into user groups until after we have successfully pushed the + // group configs) + std::vector configs = {group->info.get(), group->members.get()}; + auto push = prepare_push(group_id, timestamp, configs, ed_sk); + + _send(group_id, + push.payload, + [this, + gid = group_id, + push_info = push.info, + secretkey = std::move(ed_sk), + n = std::move(name), + timestamp, + cb = std::move(callback)](bool success, int16_t status_code, ustring response) { + try { + // Call through to the default 'handle_config_push_response' first to update it's + // state correctly (this will also result in the configs getting stored to disk) + handle_config_push_response(gid, push_info, success, status_code, response); + + // Retrieve the group configs for this pubkey and setup an entry in the user + // groups config for it (the 'at' call will throw if the group doesn't exist) + _config_groups.at(gid); + auto user_group = _config_user_groups->get_or_construct_group(gid); + user_group.name = n; + user_group.joined_at = timestamp.count(); + user_group.secretkey = secretkey; + _config_user_groups->set(user_group); + + // Manually trigger 'config_changed' because we modified '_config_user_groups' + // directly rather than via the 'MutableUserConfigs' so it won't automatically get + // triggered + config_changed(std::nullopt, true, true, std::nullopt); + + // Now that we have a `_config_user_groups` entry for the group and have confirmed + // the push we need to store the group configs (we can't do this until after the + // `_config_user_groups` has been updated) + config_changed(gid, true, false, std::nullopt); + + // Lastly trigger the 'callback' to communicate the group was successfully created + cb(gid, secretkey, std::nullopt); + } catch (const std::exception& e) { + cb(""sv, ""_usv, e.what()); + throw; + } + }); +} + +void State::approve_group(std::string_view group_id) { + std::string gid = {group_id.data(), group_id.size()}; + + // If we don't already have GroupConfigs then create them + if (auto [it, b] = _config_groups.try_emplace(gid, nullptr); b) { + auto ed_pk_data = oxenc::from_hex(group_id.begin() + 2, group_id.end()); + auto ed_pk = to_unsigned_sv(ed_pk_data); + _config_groups[gid] = + std::make_unique(ed_pk, to_unsigned_sv(_user_sk), std::nullopt); + } + + // Update the USER_GROUPS config to have the group marked as approved + auto group = _config_user_groups->get_or_construct_group(group_id); + group.invited = false; + _config_user_groups->set(group); + + // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(std::nullopt, true, true, std::nullopt); +} + +void State::load_group_admin_key(std::string_view group_id, ustring_view secret) { + if (secret.size() == 64) + secret.remove_suffix(32); + else if (secret.size() != 32) + throw std::invalid_argument{ + "Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"}; + + std::string gid = {group_id.data(), group_id.size()}; + std::array pk; + sodium_cleared> sk; + crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), secret.data()); + + // Load the secret key into the Keys config + auto& group = _config_groups.at(gid); + auto info = group->info.get(); + auto members = group->members.get(); + group->keys->load_admin_key(secret, *info, *members); + + // Update the group member record to flag the current user as an admin + auto member = members->get_or_construct(_user_x_pk_hex); + member.admin = true; + member.invite_status = 0; // Just in case + member.promotion_status = 0; + group->members->set(member); + + // Update the user groups record to include the admin key + auto user_group = _config_user_groups->get_or_construct_group(group_id); + user_group.secretkey = {sk.data(), sk.size()}; + _config_user_groups->set(user_group); + + // Trigger the 'config_changed' callbacks directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(std::nullopt, true, true, std::nullopt); + config_changed(group_id, true, true, std::nullopt); +} + +void State::add_group_members( + std::string_view group_id, + bool supplemental_rotation, + const std::vector members, + std::function error)> callback) { + if (members.empty()) { + callback(std::nullopt); + return; + } + + std::string gid = {group_id.data(), group_id.size()}; + auto& group = _config_groups.at(gid); + std::chrono::milliseconds timestamp = + (std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + + network_offset); + + // Add the members to Members + for (auto m : members) + group->members->set(m); + + // Don't bother rotating the keys if there are only admins + size_t non_admin_count = std::count_if( + members.begin(), members.end(), [](const groups::member& m) { return !m.admin; }); + + // If there are non-admins and it's not a supplemental rotation then do a rekey + if (non_admin_count > 0 && !supplemental_rotation) { + auto info = _config_groups.at(gid)->info.get(); + auto members = _config_groups.at(gid)->members.get(); + group->keys->rekey(*info, *members); + } + + // Prepare the push payload for the group configs + std::vector configs = {group->info.get(), group->members.get()}; + auto push = prepare_push(gid, timestamp, configs); + + // If there are non-admins and it's a supplemental rotation then we want to include the key + // supplement within the batch request we are going to send + if (non_admin_count > 0 && supplemental_rotation) { + std::vector sids; + std::transform( + members.begin(), + members.end(), + std::back_inserter(sids), + [](const groups::member& m) { return m.session_id; }); + auto msg = group->keys->key_supplement(sids); + auto [pubkey, payload] = group->keys->prepare_supplement_payload(msg, timestamp); + + // We need to update the current payload (we want to insert the supplement to the beginning + // of the batch requests) + auto updated_push_info = push.info; + auto updated_payload = nlohmann::json::parse(push.payload); + auto payload_json = nlohmann::json::parse(payload); + auto requests_ptr = nlohmann::json::json_pointer("/params/requests"); + + if (!updated_payload.contains(requests_ptr)) + throw std::runtime_error{"add_group_members: Prepared payload structure is invalid."}; + + // No seqno for keys messages + auto updated_requests = updated_payload[requests_ptr]; + updated_requests.insert(updated_requests.begin(), payload_json); + updated_payload[requests_ptr] = updated_requests; + updated_push_info.insert( + updated_push_info.begin(), {false, true, Namespace::UserProfile, 0}); + push = {to_unsigned(updated_payload.dump()), updated_push_info}; + } + + _send(gid, + push.payload, + [this, gid = gid, push_info = push.info, cb = std::move(callback)]( + bool success, int16_t status_code, ustring response) { + try { + // Call through to the default 'handle_config_push_response' first to update it's + // state correctly (this will also result in the configs getting stored to disk) + handle_config_push_response(gid, push_info, success, status_code, response); + cb(std::nullopt); + } catch (const std::exception& e) { + cb(e.what()); + throw; + } + }); +} + +void State::erase_group(std::string_view group_id, bool remove_user_record) { + std::string gid = {group_id.data(), group_id.size()}; + + // Remove the group configs + _config_groups.erase(gid); + + // If we don't want to remove the user record then stop here + if (!remove_user_record) + return; + + _config_user_groups->erase_group(group_id); + + // Trigger the 'config_changed' callback directly since we aren't using 'MutableUserConfig' (We + // don't call it for the group config because there is no data so it's likely we are creating + // the initial state upon accepting an invite so have no data yet) + config_changed(std::nullopt, true, true, std::nullopt); +} + +// Template functions + +template +void State::add_child_logger(ConfigType& config) { + config->logger = [this](LogLevel lvl, std::string msg) { log(lvl, msg); }; +} + +template +const ConfigType& State::config() const { + throw std::runtime_error{"config: Attempted to retrieve config for unknown namespace"}; +}; + +template +const ConfigType& State::config(std::string_view pubkey_hex) const { + throw std::runtime_error{"config: Attempted to retrieve config for unknown namespace"}; +}; + +template <> +const Contacts& State::config() const { + return *_config_contacts; +} + +template <> +const ConvoInfoVolatile& State::config() const { + return *_config_convo_info_volatile; +}; + +template <> +const UserGroups& State::config() const { + return *_config_user_groups; +}; + +template <> +const UserProfile& State::config() const { + return *_config_user_profile; +}; + +template <> +const groups::Info& State::config(std::string_view pubkey_hex) const { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->info; +}; + +template <> +const groups::Members& State::config(std::string_view pubkey_hex) const { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->members; +}; + +template <> +const groups::Keys& State::config(std::string_view pubkey_hex) const { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + return *_config_groups.at({pubkey_hex.data(), pubkey_hex.size()})->keys; +}; + +MutableUserConfigs State::mutable_config( + std::optional> on_error) { + return MutableUserConfigs( + this, + *_config_contacts, + *_config_convo_info_volatile, + *_config_user_groups, + *_config_user_profile, + on_error); +}; + +MutableUserConfigs::~MutableUserConfigs() { + try { + parent_state->config_changed(std::nullopt, true, true, std::nullopt); + } catch (const std::exception& e) { + if (on_error) + (*on_error)(e.what()); + } +}; + +MutableGroupConfigs State::mutable_config( + std::string_view pubkey_hex, + std::optional> on_error) { + if (pubkey_hex.size() != 66) + throw std::invalid_argument{"config: Invalid pubkey_hex - expected 66 bytes"}; + + auto& group = _config_groups.at({pubkey_hex.data(), pubkey_hex.size()}); + return MutableGroupConfigs(*this, *group->info, *group->members, *group->keys, on_error); +}; + +std::chrono::milliseconds MutableGroupConfigs::get_network_offset() const { + return parent_state.network_offset; +}; + +MutableGroupConfigs::~MutableGroupConfigs() { + try { + parent_state.config_changed(info.id, true, true, std::nullopt); + } catch (const std::exception& e) { + if (on_error) + (*on_error)(e.what()); + } +}; + +} // namespace session::state diff --git a/src/state_c_wrapper.cpp b/src/state_c_wrapper.cpp new file mode 100644 index 00000000..d5586271 --- /dev/null +++ b/src/state_c_wrapper.cpp @@ -0,0 +1,548 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "config/internal.hpp" +#include "session/config/base.hpp" +#include "session/config/contacts.h" +#include "session/config/contacts.hpp" +#include "session/config/convo_info_volatile.hpp" +#include "session/config/groups/members.h" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" +#include "session/config/user_groups.hpp" +#include "session/config/user_profile.hpp" +#include "session/export.h" +#include "session/state.h" +#include "session/state.hpp" +#include "session/state_groups.h" +#include "session/util.hpp" + +using namespace std::literals; +using namespace session; +using namespace session::config; +using namespace session::state; + +LIBSESSION_C_API const size_t PROFILE_PIC_MAX_URL_LENGTH = profile_pic::MAX_URL_LENGTH; + +extern "C" { + +// Util Functions + +LIBSESSION_C_API bool session_id_is_valid(const char* session_id) { + return std::strlen(session_id) == 66 && oxenc::is_hex(session_id, session_id + 66); +} + +// State Functions + +LIBSESSION_EXPORT void state_free(state_object* state) { + delete state; +} + +LIBSESSION_C_API bool state_init( + state_object** state, + const unsigned char* ed25519_secretkey_bytes, + state_namespaced_dump* dumps_, + size_t count, + char* error) { + try { + std::vector dumps = {}; + dumps.reserve(count); + + for (size_t i = 0; i < count; i++) { + std::optional pubkey_hex; + + if (dumps_[i].pubkey_hex) + pubkey_hex.emplace(dumps_[i].pubkey_hex, 66); + + dumps.emplace_back( + static_cast(dumps_[i].namespace_), + pubkey_hex, + ustring{dumps_[i].data, dumps_[i].datalen}); + } + + auto s = std::make_unique( + session::ustring_view{ed25519_secretkey_bytes, 64}, dumps); + auto s_object = std::make_unique(); + + s_object->internals = s.release(); + s_object->last_error = nullptr; + *state = s_object.release(); + return true; + } catch (const std::exception& e) { + return set_error_value(error, e.what()); + } +} + +LIBSESSION_C_API bool state_load( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + const unsigned char* dump, + size_t dumplen) { + assert(state && dump && dumplen); + + session::ustring_view dumped{dump, dumplen}; + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + try { + auto target_namespace = static_cast(namespace_); + + unbox(state).load(target_namespace, pubkey_hex, dumped); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_set_logger( + state_object* state, void (*callback)(state_log_level, const char*, void*), void* ctx) { + if (!callback) + unbox(state).logger = nullptr; + else { + unbox(state).logger = [callback, ctx](session::config::LogLevel lvl, std::string msg) { + callback(static_cast(static_cast(lvl)), msg.c_str(), ctx); + }; + } +} + +using response_callback_t = + std::function; +struct response_callback_info { + state_object* state; + response_callback_t cb; + + response_callback_info(state_object* state, response_callback_t cb) : state{state}, cb{cb} {} +}; + +LIBSESSION_C_API bool state_set_send_callback( + state_object* state, + void (*callback)( + const char* pubkey, + const unsigned char* data, + size_t data_len, + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context), + void* app_ctx) { + try { + if (!callback) + unbox(state).on_send(nullptr); + else { + unbox(state).on_send([state, callback, app_ctx]( + std::string pubkey, + ustring data, + response_callback_t received_response) { + // We leak ownership of this std::function below in the `.release()` call, then we + // recapture it inside the inner response callback below. + auto on_response = std::make_unique( + state, std::move(received_response)); + + callback( + pubkey.c_str(), + data.data(), + data.size(), + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + // Recapture the std::function callback here in a unique_ptr so that + // we clean it up at the end of this lambda. + std::unique_ptr info{ + static_cast(callback_context)}; + try { + info->cb(success, status_code, {res, reslen}); + return true; + } catch (const std::exception& e) { + return set_error(info->state, e.what()); + } + }, + app_ctx, + on_response.release()); + }); + } + + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_set_store_callback( + state_object* state, + void (*callback)(NAMESPACE, const char*, uint64_t, const unsigned char*, size_t, void*), + void* ctx) { + try { + if (!callback) + unbox(state).on_store(nullptr); + else { + // Setting this can result in the callback being immediately triggered which could throw + unbox(state).on_store([callback, ctx]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + callback( + static_cast(namespace_), + pubkey.c_str(), + timestamp_ms, + data.data(), + data.size(), + ctx); + }); + } + + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_set_service_node_offset(state_object* state, int64_t offset_ms) { + unbox(state).network_offset = std::chrono::milliseconds(offset_ms); +} + +LIBSESSION_C_API int64_t state_network_offset(const state_object* state) { + return unbox(state).network_offset.count(); +} + +LIBSESSION_C_API bool state_has_pending_send(const state_object* state) { + return unbox(state).has_pending_send(); +} + +LIBSESSION_C_API bool state_merge( + state_object* state, + const char* pubkey_hex_, + state_config_message* configs, + size_t count, + session_string_list** successful_hashes) { + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + std::vector confs; + confs.reserve(count); + + for (size_t i = 0; i < count; i++) + confs.emplace_back( + static_cast(configs[i].namespace_), + configs[i].hash, + configs[i].timestamp_ms, + ustring{configs[i].data, configs[i].datalen}); + + auto result = unbox(state).merge(pubkey_hex, confs); + + if (successful_hashes) + *successful_hashes = make_string_list(result); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_current_hashes( + state_object* state, const char* pubkey_hex_, session_string_list** current_hashes) { + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto result = unbox(state).current_hashes(pubkey_hex); + *current_hashes = make_string_list(result); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API seqno_t +state_current_seqno(state_object* state, const char* pubkey_hex_, NAMESPACE namespace_) { + switch (namespace_) { + case NAMESPACE_CONTACTS: return unbox(state).config().get_seqno(); + case NAMESPACE_CONVO_INFO_VOLATILE: + return unbox(state).config().get_seqno(); + case NAMESPACE_USER_GROUPS: return unbox(state).config().get_seqno(); + case NAMESPACE_USER_PROFILE: return unbox(state).config().get_seqno(); + default: break; + } + + // Other namespaces are unique for a given pubkey_hex_ + if (!pubkey_hex_) + return -1; + + try { + std::string_view pubkey_hex = {pubkey_hex_, 66}; + + switch (namespace_) { + case NAMESPACE_GROUP_INFO: + return unbox(state).config({pubkey_hex_, 66}).get_seqno(); + case NAMESPACE_GROUP_MEMBERS: + return unbox(state).config({pubkey_hex_, 66}).get_seqno(); + case NAMESPACE_GROUP_KEYS: return 0; // No seqno needed for GROUP_KEYS + default: return -1; + } + } catch (...) { + return -1; + } +} + +LIBSESSION_C_API bool state_dump( + state_object* state, bool full_dump, unsigned char** out, size_t* outlen) { + try { + assert(out && outlen); + auto data = unbox(state).dump(full_dump); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_dump_namespace( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen) { + assert(out && outlen); + + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto target_namespace = static_cast(namespace_); + auto data = unbox(state).dump(target_namespace, pubkey_hex); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API bool state_get_keys( + state_object* state, + NAMESPACE namespace_, + const char* pubkey_hex_, + unsigned char** out, + size_t* outlen) { + try { + std::optional pubkey_hex; + if (pubkey_hex_) + pubkey_hex.emplace(pubkey_hex_, 66); + + auto target_namespace = static_cast(namespace_); + auto data = unbox(state).get_keys(target_namespace, pubkey_hex); + *outlen = data.size(); + *out = static_cast(std::malloc(data.size())); + std::memcpy(*out, data.data(), data.size()); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_create_group( + state_object* state, + const char* name, + size_t name_len, + const char* description_, + size_t description_len, + const user_profile_pic pic_, + const state_group_member* members_, + const size_t members_len, + void (*callback)( + const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx), + void* ctx) { + assert(name && callback); + try { + std::optional description; + if (description_) + description = {description_, description_len}; + + std::string_view url{pic_.url}; + ustring_view key; + if (!url.empty()) + key = {pic_.key, 32}; + + std::optional pic = profile_pic{url, key}; + std::vector members = {}; + members.reserve(members_len); + + for (size_t i = 0; i < members_len; i++) + members.emplace_back(groups::member{members_[i]}); + + unbox(state).create_group( + {name, name_len}, + description, + pic, + members, + [callback, ctx]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { + if (error) + callback( + group_id.data(), + group_sk.data(), + error->data(), + error->size(), + ctx); + else + callback(group_id.data(), group_sk.data(), nullptr, 0, ctx); + }); + } catch (const std::exception& e) { + std::string_view err = e.what(); + callback(nullptr, nullptr, e.what(), err.size(), ctx); + } +} + +LIBSESSION_C_API void state_approve_group(state_object* state, const char* group_id) { + try { + unbox(state).approve_group({group_id, 66}); + } catch (...) { + } +} + +LIBSESSION_C_API bool state_load_group_admin_key( + state_object* state, const char* group_id, unsigned const char* seed) { + try { + unbox(state).load_group_admin_key({group_id, 66}, ustring_view{seed, 32}); + return true; + } catch (const std::exception& e) { + return set_error(state, e.what()); + } +} + +LIBSESSION_C_API void state_add_group_members( + state_object* state, + const char* group_id, + const bool supplemental_rotation, + const state_group_member* members_, + const size_t members_len, + void (*callback)(const char* error, size_t error_len, void* ctx), + void* ctx) { + assert(members_ && callback); + try { + std::vector members; + members.reserve(members_len); + + for (size_t i = 0; i < members_len; i++) + members.emplace_back(groups::member{members_[i]}); + + unbox(state).add_group_members( + {group_id, 66}, + supplemental_rotation, + members, + [callback, ctx](std::optional error) { + if (error) + callback(error->data(), error->size(), ctx); + else + callback(nullptr, 0, ctx); + }); + } catch (const std::exception& e) { + std::string_view error = e.what(); + + if (callback) + callback(e.what(), error.size(), ctx); + } +} + +LIBSESSION_C_API void state_erase_group( + state_object* state, const char* group_id, bool remove_user_record) { + try { + unbox(state).erase_group({group_id, 66}, remove_user_record); + } catch (...) { + } +} + +LIBSESSION_C_API bool state_mutate_user( + state_object* state, void (*callback)(mutable_user_state_object*, void*), void* ctx) { + try { + auto s_object = new mutable_user_state_object(); + auto mutable_state = unbox(state).mutable_config([state](std::string_view e) { + // Don't override an existing error + if (state->last_error) + return; + + set_error(state, e); + }); + s_object->internals = &mutable_state; + callback(s_object, ctx); + } catch (const std::exception& e) { + set_error(state, e.what()); + } + + // If the state has an error the it was most likely set above (even if not then it means the + // state has an unhandled error which should be handled then cleared by the caller) + if (state->last_error) + return false; + + return true; +} + +LIBSESSION_C_API bool state_mutate_group( + state_object* state, + const char* pubkey_hex, + void (*callback)(mutable_group_state_object*, void*), + void* ctx) { + try { + auto s_object = new mutable_group_state_object(); + auto mutable_state = + unbox(state).mutable_config({pubkey_hex, 66}, [state](std::string_view e) { + // Don't override an existing error + if (state->last_error) + return; + + set_error(state, e); + }); + s_object->internals = &mutable_state; + callback(s_object, ctx); + } catch (const std::exception& e) { + set_error(state, e.what()); + } + + // If the state has an error the it was most likely set above (even if not then it means the + // state has an unhandled error which should be handled then cleared by the caller) + if (state->last_error) + return false; + + return true; +} + +LIBSESSION_C_API void mutable_user_state_set_error_if_empty( + mutable_user_state_object* state, const char* err, size_t err_len) { + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)({err, err_len}); +} + +LIBSESSION_C_API void mutable_group_state_set_error_if_empty( + mutable_group_state_object* state, const char* err, size_t err_len) { + if (auto on_error = unbox(state).on_error; on_error.has_value()) + (*on_error)({err, err_len}); +} + +} // extern "C" \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a6a1d127..6a2f9bd2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable(testAll test_proto.cpp test_random.cpp test_session_encrypt.cpp + test_state.cpp test_xed25519.cpp ) @@ -29,6 +30,7 @@ target_link_libraries(testAll PRIVATE libsession::config libsession::onionreq libsodium::sodium-internal + nlohmann_json::nlohmann_json Catch2::Catch2WithMain) add_custom_target(check COMMAND testAll) diff --git a/tests/test_compression.cpp b/tests/test_compression.cpp index 24892fbd..f160e204 100644 --- a/tests/test_compression.cpp +++ b/tests/test_compression.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include diff --git a/tests/test_config_contacts.cpp b/tests/test_config_contacts.cpp index 17883d75..59485e0d 100644 --- a/tests/test_config_contacts.cpp +++ b/tests/test_config_contacts.cpp @@ -1,16 +1,23 @@ +#include #include #include #include +#include +#include #include #include +#include #include +#include +#include #include #include "utils.hpp" using namespace std::literals; using namespace oxenc::literals; +using namespace session; static constexpr int64_t created_ts = 1680064059; @@ -225,32 +232,29 @@ TEST_CASE("Contacts", "[config][contacts]") { CHECK(nicknames[1] == "Nickname 3"); } -TEST_CASE("Contacts (C API)", "[config][contacts][c]") { - 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); +TEST_CASE("State contacts (C API)", "[state][contacts][c]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; - 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)); + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; - config_object* conf; - REQUIRE(0 == contacts_init(&conf, ed_sk.data(), NULL, 0, NULL)); + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); const char* const definitely_real_id = "050000000000000000000000000000000000000000000000000000000000000000"; contacts_contact c; - CHECK_FALSE(contacts_get(conf, &c, definitely_real_id)); + CHECK_FALSE(state_get_contact(state, &c, definitely_real_id, nullptr)); - CHECK(contacts_get_or_construct(conf, &c, definitely_real_id)); + CHECK(state_get_or_construct_contact(state, &c, definitely_real_id, nullptr)); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(strlen(c.name) == 0); @@ -267,10 +271,15 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { c.approved_me = true; c.created = created_ts; - contacts_set(conf, &c); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_contact(mutable_state, static_cast(ctx)); + }, + &c); contacts_contact c2; - REQUIRE(contacts_get(conf, &c2, definitely_real_id)); + REQUIRE(state_get_contact(state, &c2, definitely_real_id, nullptr)); CHECK(c2.name == "Joe"sv); CHECK(c2.nickname == "Joey"sv); @@ -279,31 +288,50 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK_FALSE(c2.blocked); CHECK(strlen(c2.profile_pic.url) == 0); - CHECK(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); - - config_push_data* to_push = config_push(conf); - CHECK(to_push->seqno == 1); - - config_object* conf2; - REQUIRE(contacts_init(&conf2, ed_sk.data(), NULL, 0, NULL) == 0); - - const char* merge_hash[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - merge_hash[0] = "fakehash1"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(send_records[0].pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61" + "f46"); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONTACTS) == 1); + + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(send_records[0].payload); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + session_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash1", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "fakehash1"sv); free(accepted); + free(merge_data); - config_confirm_pushed(conf, to_push->seqno, "fakehash1"); - free(to_push); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); contacts_contact c3; - REQUIRE(contacts_get(conf2, &c3, definitely_real_id)); + REQUIRE(state_get_contact(state2, &c3, definitely_real_id, nullptr)); CHECK(c3.name == "Joe"sv); CHECK(c3.nickname == "Joey"sv); CHECK(c3.approved); @@ -312,41 +340,63 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(strlen(c3.profile_pic.url) == 0); CHECK(c3.created == created_ts); + contacts_contact c4; auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; - REQUIRE(contacts_get_or_construct(conf, &c3, another_id)); - CHECK(strlen(c3.name) == 0); - CHECK(strlen(c3.nickname) == 0); - CHECK_FALSE(c3.approved); - CHECK_FALSE(c3.approved_me); - CHECK_FALSE(c3.blocked); - CHECK(strlen(c3.profile_pic.url) == 0); - CHECK(c3.created == 0); - - contacts_set(conf2, &c3); - - to_push = config_push(conf2); - - merge_hash[0] = "fakehash2"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - accepted = config_merge(conf, merge_hash, merge_data, merge_size, 1); + REQUIRE(state_get_or_construct_contact(state, &c4, another_id, nullptr)); + CHECK(strlen(c4.name) == 0); + CHECK(strlen(c4.nickname) == 0); + CHECK_FALSE(c4.approved); + CHECK_FALSE(c4.approved_me); + CHECK_FALSE(c4.blocked); + CHECK(strlen(c4.profile_pic.url) == 0); + CHECK(c4.created == 0); + + state_mutate_user( + state2, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_contact(mutable_state, static_cast(ctx)); + }, + &c4); + + REQUIRE(send_records_2.size() == 1); + auto last_send_json_2 = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(last_send_json_2.contains(first_request_data)); + auto last_send_data_2 = to_unsigned( + oxenc::from_base64(last_send_json_2[first_request_data].get())); + merge_data = new state_config_message[1]; + merge_data[0] = { + NAMESPACE_CONTACTS, + "fakehash2", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; + REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "fakehash2"sv); free(accepted); - - config_confirm_pushed(conf2, to_push->seqno, "fakehash2"); - - REQUIRE(to_push->obsolete_len > 0); - CHECK(to_push->obsolete_len == 1); - CHECK(to_push->obsolete[0] == "fakehash1"sv); - free(to_push); + free(merge_data); + + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash2\"}}]}"); + send_records_2[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[0].callback_context); + + auto messages_key = nlohmann::json::json_pointer("/params/requests/1/params/messages"); + REQUIRE(last_send_json_2.contains(messages_key)); + auto obsolete = last_send_json_2[messages_key].get>(); + REQUIRE(obsolete.size() > 0); + CHECK(obsolete.size() == 1); + CHECK(obsolete[0] == "fakehash1"sv); // Iterate through and make sure we got everything we expected std::vector session_ids; std::vector nicknames; - CHECK(contacts_size(conf) == 2); - contacts_iterator* it = contacts_iterator_new(conf); + CHECK(state_size_contacts(state) == 2); + contacts_iterator* it = contacts_iterator_new(state); contacts_contact ci; for (; !contacts_iterator_done(it, &ci); contacts_iterator_advance(it)) { session_ids.push_back(ci.session_id); @@ -361,7 +411,7 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { CHECK(nicknames[1] == "(N/A)"); // Changing things while iterating: - it = contacts_iterator_new(conf); + it = contacts_iterator_new(state); int deletions = 0, non_deletions = 0; std::vector contacts_to_remove; while (!contacts_iterator_done(it, &ci)) { @@ -373,14 +423,21 @@ TEST_CASE("Contacts (C API)", "[config][contacts][c]") { } contacts_iterator_advance(it); } - for (auto& cont : contacts_to_remove) - contacts_erase(conf, cont.c_str()); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + auto contacts_to_remove = static_cast*>(ctx); + + for (auto& cont : *contacts_to_remove) + state_erase_contact(mutable_state, cont.c_str()); + }, + &contacts_to_remove); CHECK(deletions == 1); CHECK(non_deletions == 1); - CHECK(contacts_get(conf, &ci, definitely_real_id)); - CHECK_FALSE(contacts_get(conf, &ci, another_id)); + CHECK(state_get_contact(state, &ci, definitely_real_id, nullptr)); + CHECK_FALSE(state_get_contact(state, &ci, another_id, nullptr)); } TEST_CASE("huge contacts compression", "[config][compression][contacts]") { diff --git a/tests/test_config_convo_info_volatile.cpp b/tests/test_config_convo_info_volatile.cpp index 85395a5a..157fa52f 100644 --- a/tests/test_config_convo_info_volatile.cpp +++ b/tests/test_config_convo_info_volatile.cpp @@ -1,10 +1,14 @@ +#include #include #include #include #include #include +#include #include +#include +#include #include #include @@ -12,6 +16,9 @@ using namespace std::literals; using namespace oxenc::literals; +using namespace session; + +static constexpr int64_t created_ts = 1680064059; TEST_CASE("Conversations", "[config][conversations]") { @@ -242,30 +249,39 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + char err[256]; + memset(err, 0, 255); + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); const char* const definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; convo_info_volatile_1to1 c; - CHECK_FALSE(convo_info_volatile_get_1to1(conf, &c, definitely_real_id)); - CHECK(conf->last_error == nullptr); - CHECK_FALSE(convo_info_volatile_get_1to1(conf, &c, "05123456")); - CHECK(conf->last_error == - "Invalid session ID: expected 66 hex digits starting with 05; got 05123456"sv); + CHECK_FALSE(state_get_convo_info_volatile_1to1(state, &c, definitely_real_id, err)); + CHECK(err == ""sv); + + CHECK_FALSE(state_get_convo_info_volatile_1to1(state, &c, "05123456", err)); + CHECK(err == "Invalid session ID: expected 66 hex digits starting with 05; got 05123456"sv); - CHECK(convo_info_volatile_size(conf) == 0); + CHECK(state_size_convo_info_volatile(state) == 0); - CHECK(convo_info_volatile_get_or_construct_1to1(conf, &c, definitely_real_id)); + CHECK(state_get_or_construct_convo_info_volatile_1to1(state, &c, definitely_real_id, nullptr)); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK(c.last_read == 0); CHECK_FALSE(c.unread); - CHECK_FALSE(config_needs_push(conf)); - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + CHECK_FALSE(session::state::unbox(state).config().needs_dump()); auto now_ms = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) @@ -273,40 +289,36 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { c.last_read = now_ms; - // The new data doesn't get stored until we call this: - convo_info_volatile_set_1to1(conf, &c); - convo_info_volatile_legacy_group cg; - 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(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); + REQUIRE_FALSE( + state_get_convo_info_volatile_legacy_group(state, &cg, definitely_real_id, nullptr)); const auto open_group_pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"_hexbytes; convo_info_volatile_community og; - CHECK_FALSE(convo_info_volatile_get_or_construct_community( - conf, + CHECK_FALSE(state_get_or_construct_convo_info_volatile_community( + state, &og, "bad-url", "room", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); - CHECK(conf->last_error == "Invalid community URL: invalid/missing protocol://"sv); - CHECK_FALSE(convo_info_volatile_get_or_construct_community( - conf, + "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data(), + err)); + CHECK(err == "Invalid community URL: invalid/missing protocol://"sv); + CHECK_FALSE(state_get_or_construct_convo_info_volatile_community( + state, &og, "https://example.com", "bad room name", - "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data())); - CHECK(conf->last_error == "Invalid community URL: room token contains invalid characters"sv); - - CHECK(convo_info_volatile_get_or_construct_community( - conf, &og, "http://Example.ORG:5678", "SudokuRoom", open_group_pubkey.data())); - CHECK(conf->last_error == nullptr); + "0000000000000000000000000000000000000000000000000000000000000000"_hexbytes.data(), + err)); + CHECK(err == "Invalid community URL: room token contains invalid characters"sv); + + memset(err, 0, 255); + CHECK(state_get_or_construct_convo_info_volatile_community( + state, &og, "http://Example.ORG:5678", "SudokuRoom", open_group_pubkey.data(), err)); + CHECK(err == ""sv); CHECK(og.base_url == "http://example.org:5678"sv); // Note: lower-case CHECK(og.room == "sudokuroom"sv); // Note: lower-case CHECK(oxenc::to_hex(og.pubkey, og.pubkey + 32) == @@ -314,82 +326,137 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { og.unread = true; // The new data doesn't get stored until we call this: - convo_info_volatile_set_community(conf, &og); + std::pair convos = {&c, &og}; + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + auto convos = static_cast< + std::pair*>(ctx); + state_set_convo_info_volatile_1to1(mutable_state, convos->first); + state_set_convo_info_volatile_community(mutable_state, convos->second); + }, + &convos); + + REQUIRE(state_get_convo_info_volatile_1to1(state, &c, definitely_real_id, nullptr)); + CHECK(c.last_read == now_ms); - config_push_data* to_push = config_push(conf); - auto seqno = to_push->seqno; - free(to_push); - CHECK(seqno == 1); + CHECK(session::state::unbox(state).config().needs_push()); + CHECK(session::state::unbox(state).config().needs_dump()); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); // Pretend we uploaded it - config_confirm_pushed(conf, seqno, "hash1"); - CHECK(config_needs_dump(conf)); - CHECK_FALSE(config_needs_push(conf)); - - unsigned char* dump; - size_t dumplen; - config_dump(conf, &dump, &dumplen); - - config_object* conf2; - REQUIRE(convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL) == 0); - free(dump); - - CHECK_FALSE(config_needs_push(conf2)); - CHECK_FALSE(config_needs_dump(conf2)); - - REQUIRE(convo_info_volatile_get_1to1(conf2, &c, definitely_real_id)); + REQUIRE(send_records.size() == 1); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash1\"}}]}"); + send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); + + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + CHECK_FALSE(session::state::unbox(state).config().needs_dump()); + + REQUIRE(store_records.size() == 2); + state_namespaced_dump* dumps = new state_namespaced_dump[1]; + dumps[0] = { + static_cast(store_records[1].namespace_), + store_records[1].pubkey.c_str(), + store_records[1].data.data(), + store_records[1].data.size()}; + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); + free(dumps); + + CHECK_FALSE(session::state::unbox(state2).config().needs_push()); + CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); + + REQUIRE(state_get_convo_info_volatile_1to1(state2, &c, definitely_real_id, nullptr)); CHECK(c.last_read == now_ms); CHECK(c.session_id == std::string_view{definitely_real_id}); CHECK_FALSE(c.unread); - REQUIRE(convo_info_volatile_get_community(conf2, &og, "http://EXAMPLE.org:5678", "sudokuRoom")); + REQUIRE(state_get_convo_info_volatile_community( + state2, &og, "http://EXAMPLE.org:5678", "sudokuRoom", nullptr)); CHECK(og.base_url == "http://example.org:5678"sv); CHECK(og.room == "sudokuroom"sv); CHECK(oxenc::to_hex(og.pubkey, og.pubkey + 32) == to_hex(open_group_pubkey)); auto another_id = "051111111111111111111111111111111111111111111111111111111111111111"; convo_info_volatile_1to1 c2; - REQUIRE(convo_info_volatile_get_or_construct_1to1(conf2, &c2, another_id)); + REQUIRE(state_get_or_construct_convo_info_volatile_1to1(state2, &c2, another_id, nullptr)); c2.unread = true; - convo_info_volatile_set_1to1(conf2, &c2); - REQUIRE(convo_info_volatile_get_or_construct_legacy_group( - conf2, &cg, "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")); + REQUIRE(state_get_or_construct_convo_info_volatile_legacy_group( + state2, + &cg, + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + nullptr)); cg.last_read = now_ms - 50; - convo_info_volatile_set_legacy_group(conf2, &cg); - CHECK(config_needs_push(conf2)); - - to_push = config_push(conf2); - CHECK(to_push->seqno == 2); - - const char* hash_data[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - hash_data[0] = "hash123"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - config_string_list* accepted = config_merge(conf, hash_data, merge_data, merge_size, 1); + std::pair convos2 = {&c2, &cg}; + state_mutate_user( + state2, + [](mutable_user_state_object* mutable_state, void* ctx) { + auto convos = static_cast< + std::pair*>( + ctx); + state_set_convo_info_volatile_1to1(mutable_state, convos->first); + state_set_convo_info_volatile_legacy_group(mutable_state, convos->second); + }, + &convos2); + REQUIRE(state_get_or_construct_convo_info_volatile_legacy_group( + state2, + &cg, + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + nullptr)); + CHECK(session::state::unbox(state2).config().needs_push()); + CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); + + REQUIRE(send_records_2.size() == 1); + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + session_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONVO_INFO_VOLATILE, + "hash123", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "hash123"sv); free(accepted); - config_confirm_pushed(conf2, seqno, "hash123"); - free(to_push); + free(merge_data); - CHECK_FALSE(config_needs_push(conf)); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash123\"}}]}"); + send_records_2[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[0].callback_context); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); std::vector seen; - for (auto* conf : {conf, conf2}) { + for (auto* state : {state, state2}) { // Iterate through and make sure we got everything we expected seen.clear(); - CHECK(convo_info_volatile_size(conf) == 4); - CHECK(convo_info_volatile_size_1to1(conf) == 2); - CHECK(convo_info_volatile_size_communities(conf) == 1); - CHECK(convo_info_volatile_size_legacy_groups(conf) == 1); + CHECK(state_size_convo_info_volatile(state) == 4); + CHECK(state_size_convo_info_volatile_1to1(state) == 2); + CHECK(state_size_convo_info_volatile_communities(state) == 1); + CHECK(state_size_convo_info_volatile_legacy_groups(state) == 1); convo_info_volatile_1to1 c1; convo_info_volatile_community c2; convo_info_volatile_legacy_group c3; - convo_info_volatile_iterator* it = convo_info_volatile_iterator_new(conf); + convo_info_volatile_iterator* it = convo_info_volatile_iterator_new(state); for (; !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { if (convo_info_volatile_it_is_1to1(it, &c1)) { seen.push_back("1-to-1: "s + c1.session_id); @@ -412,22 +479,35 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { "c"}}); } - CHECK_FALSE(config_needs_push(conf)); - convo_info_volatile_erase_1to1( - conf, "052000000000000000000000000000000000000000000000000000000000000000"); - CHECK_FALSE(config_needs_push(conf)); - convo_info_volatile_erase_1to1( - conf, "055000000000000000000000000000000000000000000000000000000000000000"); - CHECK(config_needs_push(conf)); - CHECK(convo_info_volatile_size(conf) == 3); - CHECK(convo_info_volatile_size_1to1(conf) == 1); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_erase_convo_info_volatile_1to1( + mutable_state, + "052000000000000000000000000000000000000000000000000000000000000000"); + }, + nullptr); + CHECK_FALSE(session::state::unbox(state).config().needs_push()); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_erase_convo_info_volatile_1to1( + mutable_state, + "055000000000000000000000000000000000000000000000000000000000000000"); + }, + nullptr); + CHECK(session::state::unbox(state).config().needs_push()); + CHECK(state_size_convo_info_volatile(state) == 3); + CHECK(state_size_convo_info_volatile_1to1(state) == 1); // Check the single-type iterators: seen.clear(); convo_info_volatile_iterator* it; convo_info_volatile_1to1 ci; - for (it = convo_info_volatile_iterator_new_1to1(conf); !convo_info_volatile_iterator_done(it); + for (it = convo_info_volatile_iterator_new_1to1(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_1to1(it, &ci)); seen.push_back(ci.session_id); @@ -439,7 +519,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.clear(); convo_info_volatile_community ogi; - for (it = convo_info_volatile_iterator_new_communities(conf); + for (it = convo_info_volatile_iterator_new_communities(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_community(it, &ogi)); @@ -452,7 +532,7 @@ TEST_CASE("Conversations (C API)", "[config][conversations][c]") { seen.clear(); convo_info_volatile_legacy_group cgi; - for (it = convo_info_volatile_iterator_new_legacy_groups(conf); + for (it = convo_info_volatile_iterator_new_legacy_groups(state); !convo_info_volatile_iterator_done(it); convo_info_volatile_iterator_advance(it)) { REQUIRE(convo_info_volatile_it_is_legacy_group(it, &cgi)); @@ -579,89 +659,152 @@ TEST_CASE("Conversation dump/load state bug", "[config][conversations][dump-load CHECK(oxenc::to_hex(seed.begin(), seed.end()) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - config_object* conf; - REQUIRE(0 == convo_info_volatile_init(&conf, ed_sk.data(), NULL, 0, NULL)); + char err[256]; + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); convo_info_volatile_1to1 c; - CHECK(convo_info_volatile_get_or_construct_1to1( - conf, &c, "050123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state, &c, "050123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", err)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf, &c); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); // Fake push: - config_push_data* to_push = config_push(conf); - seqno_t seqno = to_push->seqno; - free(to_push); - CHECK(seqno == 1); - config_confirm_pushed(conf, seqno, "somehash"); - CHECK(config_needs_dump(conf)); - - // Dump: - unsigned char* dump; - size_t dumplen; - config_dump(conf, &dump, &dumplen); + REQUIRE(send_records.size() == 1); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 1); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"somehash\"}}]}"); + send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); // Load the dump: - config_object* conf2; - REQUIRE(0 == convo_info_volatile_init(&conf2, ed_sk.data(), dump, dumplen, NULL)); - - free(dump); + REQUIRE(store_records.size() == 2); + state_namespaced_dump* dumps = new state_namespaced_dump[1]; + dumps[0] = { + static_cast(store_records[1].namespace_), + store_records[1].pubkey.c_str(), + store_records[1].data.data(), + store_records[1].data.size()}; + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), dumps, 1, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); + free(dumps); // Change the original again, then push it for conf2: - CHECK(convo_info_volatile_get_or_construct_1to1( - conf, &c, "051111111111111111111111111111111111111111111111111111111111111111")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state, + &c, + "051111111111111111111111111111111111111111111111111111111111111111", + nullptr)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf, &c); - - to_push = config_push(conf); - CHECK(to_push->seqno == 2); - config_confirm_pushed(conf, to_push->seqno, "hash5235"); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); + + REQUIRE(send_records.size() == 2); + CHECK(state_current_seqno(state, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 2); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hash5235\"}}]}"); + send_records[1].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[1].callback_context); // But *before* we load the push make a dirtying change to conf2 that we *don't* push (so that // we'll be merging into a dirty-state config): - CHECK(convo_info_volatile_get_or_construct_1to1( - conf2, &c, "052222111111111111111111111111111111111111111111111111111111111111")); + CHECK(state_get_or_construct_convo_info_volatile_1to1( + state2, + &c, + "052222111111111111111111111111111111111111111111111111111111111111", + nullptr)); c.last_read = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); - convo_info_volatile_set_1to1(conf2, &c); - - // And now, *before* we push the dirty config, also merge the incoming push from `conf`: - const char* merge_hash[1]; - const unsigned char* merge_data[1]; - size_t merge_size[1]; - merge_hash[0] = "hash5235"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - - config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); + state_mutate_user( + state2, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx)); + }, + &c); + + // And now, *before* we push the dirty config, also merge the incoming push from `state`: + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(send_records[1].payload); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + session_string_list* accepted; + merge_data[0] = { + NAMESPACE_CONVO_INFO_VOLATILE, + "hash5235", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); REQUIRE(accepted->len == 1); CHECK(accepted->value[0] == "hash5235"sv); free(accepted); - free(to_push); + free(merge_data); - CHECK(config_needs_push(conf2)); + CHECK(session::state::unbox(state2).config().needs_push()); convo_info_volatile_1to1 c1; - REQUIRE(convo_info_volatile_get_or_construct_1to1( - conf2, &c1, "051111111111111111111111111111111111111111111111111111111111111111")); + REQUIRE(state_get_or_construct_convo_info_volatile_1to1( + state2, + &c1, + "051111111111111111111111111111111111111111111111111111111111111111", + nullptr)); c1.last_read += 10; // Prior to the commit that added this test case (and fix), this call would fail with: // Internal error: unexpected dirty but non-mutable ConfigMessage // because of the above dirty->merge->dirty (without an intermediate push) pattern. - REQUIRE_NOTHROW(convo_info_volatile_set_1to1(conf2, &c1)); - - CHECK(config_needs_push(conf2)); - to_push = config_push(conf2); - CHECK(to_push->seqno == 3); - config_confirm_pushed(conf2, to_push->seqno, "hashz"); - CHECK_FALSE(config_needs_push(conf2)); - - config_dump(conf2, &dump, &dumplen); - free(dump); - CHECK_FALSE(config_needs_dump(conf2)); + state_mutate_user( + state2, + [](mutable_user_state_object* mutable_state, void* ctx) { + REQUIRE_NOTHROW(state_set_convo_info_volatile_1to1( + mutable_state, static_cast(ctx))); + }, + &c1); + + REQUIRE(send_records_2.size() == 3); + CHECK(session::state::unbox(state2).config().needs_push()); + CHECK(state_current_seqno(state2, nullptr, NAMESPACE_CONVO_INFO_VOLATILE) == 4); + send_response = to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"hashz\"}}]}"); + send_records_2[2].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records_2[2].callback_context); + CHECK_FALSE(session::state::unbox(state2).config().needs_push()); + CHECK_FALSE(session::state::unbox(state2).config().needs_dump()); } diff --git a/tests/test_config_user_groups.cpp b/tests/test_config_user_groups.cpp index 25425a4e..78071588 100644 --- a/tests/test_config_user_groups.cpp +++ b/tests/test_config_user_groups.cpp @@ -1,9 +1,11 @@ +#include #include #include #include #include #include +#include #include #include #include @@ -13,6 +15,7 @@ using namespace std::literals; using namespace oxenc::literals; +using namespace session; static constexpr int64_t created_ts = 1680064059; @@ -576,15 +579,22 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); char err[256]; - config_object* conf; - rc = user_groups_init(&conf, ed_sk.data(), NULL, 0, err); - REQUIRE(rc == 0); + state_object* state; + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); constexpr auto definitely_real_id = "055000000000000000000000000000000000000000000000000000000000000000"; - ugroups_legacy_group_info* group = - user_groups_get_or_construct_legacy_group(conf, definitely_real_id); + ugroups_legacy_group_info* group; + REQUIRE(state_get_or_construct_ugroups_legacy_group( + state, &group, definitely_real_id, nullptr)); CHECK(group->joined_at == 0); group->joined_at = created_ts; @@ -663,47 +673,88 @@ TEST_CASE("User Groups members C API", "[config][groups][c]") { expected_members.emplace(users[i], true); // Non-freeing, so we can keep using `group`; this is less common: - user_groups_set_legacy_group(conf, group); - - group->session_id[2] = 'e'; - // The "normal" way to set a group when you're done with it (also properly frees `group`). - user_groups_set_free_legacy_group(conf, group); - - config_string_list* hashes = config_current_hashes(conf); - REQUIRE(hashes); - CHECK(hashes->len == 0); - free(hashes); - - config_push_data* to_push = config_push(conf); - CHECK(to_push->seqno == 1); - - hashes = config_current_hashes(conf); - REQUIRE(hashes); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + auto group = static_cast(ctx); + state_set_ugroups_legacy_group(mutable_state, group); + + group->session_id[2] = 'e'; + // The "normal" way to set a group when you're done with it (also properly frees + //`group`). + state_set_free_ugroups_legacy_group(mutable_state, group); + }, + group); + + session_string_list* hashes; + REQUIRE(state_current_hashes(state, nullptr, &hashes)); CHECK(hashes->len == 0); free(hashes); - config_confirm_pushed(conf, to_push->seqno, "fakehash1"); - - hashes = config_current_hashes(conf); + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].pubkey == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(send_records[0].pubkey == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3" + "a72"); + + CHECK(state_current_seqno(state, nullptr, NAMESPACE_USER_GROUPS) == 1); + ustring send_response = + to_unsigned("{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}}]}"); + send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); + + REQUIRE(state_current_hashes(state, nullptr, &hashes)); REQUIRE(hashes); REQUIRE(hashes->len == 1); CHECK(hashes->value[0] == "fakehash1"sv); free(hashes); size_t key_len; - unsigned char* keys = config_get_keys(conf, &key_len); + unsigned char* keys; + state_get_keys(state, NAMESPACE_USER_GROUPS, nullptr, &keys, &key_len); REQUIRE(keys); REQUIRE(key_len == 1); - session::config::UserGroups c2{ustring_view{seed}, std::nullopt}; - - std::vector> to_merge; - to_merge.emplace_back("fakehash1", ustring_view{to_push->config, to_push->config_len}); - CHECK(c2.merge(to_merge) == std::vector{{"fakehash1"}}); - - auto grp = c2.get_legacy_group(definitely_real_id); - REQUIRE(grp); - CHECK(grp->members() == expected_members); + state_object* state2; + REQUIRE(state_init(&state2, ed_sk.data(), nullptr, 0, nullptr)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto last_send_json = nlohmann::json::parse(send_records[0].payload); + REQUIRE(last_send_json.contains(first_request_data)); + auto last_send_data = + to_unsigned(oxenc::from_base64(last_send_json[first_request_data].get())); + state_config_message* merge_data = new state_config_message[1]; + session_string_list* accepted; + merge_data[0] = { + NAMESPACE_USER_GROUPS, + "fakehash1", + created_ts, + last_send_data.data(), + last_send_data.size()}; + REQUIRE(state_merge(state2, nullptr, merge_data, 1, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash1"sv); + free(accepted); + free(merge_data); + + ugroups_legacy_group_info* grp; + REQUIRE(state_get_ugroups_legacy_group(state2, &grp, definitely_real_id, nullptr)); + + found_members.clear(); + it = ugroups_legacy_members_begin(grp); + while (ugroups_legacy_members_next(it, &session_id, &admin)) { + found_members[session_id] = admin; + } + ugroups_legacy_members_free(it); + CHECK(found_members == expected_members); CHECK(grp->joined_at == created_ts); } diff --git a/tests/test_config_userprofile.cpp b/tests/test_config_userprofile.cpp index 66c81cac..7e37f42e 100644 --- a/tests/test_config_userprofile.cpp +++ b/tests/test_config_userprofile.cpp @@ -1,10 +1,11 @@ #include #include -#include #include #include #include +#include +#include #include #include "utils.hpp" @@ -12,17 +13,17 @@ using namespace std::literals; using namespace oxenc::literals; -void log_msg(config_log_level lvl, const char* msg, void*) { - INFO((lvl == LOG_LEVEL_ERROR ? "ERROR" - : lvl == LOG_LEVEL_WARNING ? "Warning" - : lvl == LOG_LEVEL_INFO ? "Info" - : "debug") +void log_msg(session::config::LogLevel lvl, std::string msg) { + INFO((lvl == session::config::LogLevel::error ? "ERROR" + : lvl == session::config::LogLevel::warning ? "Warning" + : lvl == session::config::LogLevel::info ? "Info" + : "debug") << ": " << msg); } -TEST_CASE("user profile C API", "[config][user_profile][c]") { +TEST_CASE("user profile", "[config][user_profile]") { - const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hex; + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; std::array ed_pk, curve_pk; std::array ed_sk; crypto_sign_ed25519_seed_keypair( @@ -36,36 +37,29 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); CHECK(oxenc::to_hex(seed) == oxenc::to_hex(ed_sk.begin(), ed_sk.begin() + 32)); - // Initialize a brand new, empty config because we have no dump data to deal with. - char err[256]; - config_object* conf; - rc = user_profile_init(&conf, ed_sk.data(), NULL, 0, err); - REQUIRE(rc == 0); - - config_set_logger(conf, log_msg, NULL); + session::config::UserProfile conf{ustring_view{seed}, std::nullopt}; + conf.logger = log_msg; // We don't need to push anything, since this is an empty config - CHECK_FALSE(config_needs_push(conf)); + CHECK_FALSE(conf.needs_push()); // And we haven't changed anything so don't need to dump to db - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // Since it's empty there shouldn't be a name. - const char* name = user_profile_get_name(conf); - CHECK(name == nullptr); // (should be NULL instead of nullptr in C) + auto name = conf.get_name(); + CHECK(name == std::nullopt); // We don't need to push since we haven't changed anything, so this call is mainly just for // testing: - config_push_data* to_push = config_push(conf); - REQUIRE(to_push); - CHECK(to_push->seqno == 0); - CHECK(to_push->config_len == 256 + 176); // 176 = protobuf overhead - const char* enc_domain = "UserProfile"; - REQUIRE(config_encryption_domain(conf) == std::string_view{enc_domain}); + auto [seqno, to_push, obs] = conf.push(); + CHECK(seqno == 0); + CHECK(to_push.size() == 256 + 176); // 176 = protobuf overhead + REQUIRE(conf.encryption_domain() == "UserProfile"sv); // There's nothing particularly profound about this value (it is multiple layers of nested // protobuf with some encryption and padding halfway through); this test is just here to ensure // that our pushed messages are deterministic: - CHECK(oxenc::to_hex(to_push->config, to_push->config + to_push->config_len) == + CHECK(oxenc::to_hex(to_push.begin(), to_push.end()) == "080112ab030a0012001aa20308062801429b0326ec9746282053eb119228e6c36012966e7d2642163169ba39" "98af44ca65f967768dd78ee80fffab6f809f6cef49c73a36c82a89622ff0de2ceee06b8c638e2c876fa9047f" "449dbe24b1fc89281a264fe90abdeffcdd44f797bd4572a6c5ae8d88bf372c3c717943ebd570222206fabf0e" @@ -77,43 +71,47 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "5240d90cbb360fafec0b7eff4c676ae598540813d062dc9468365c73b4cfa2ffd02d48cdcd8f0c71324c6d0a" "60346a7a0e50af3be64684b37f9e6c831115bf112ddd18acde08eaec376f0872a3952000"); - free(to_push); - // These should also be unset: - auto pic = user_profile_get_pic(conf); - CHECK(strlen(pic.url) == 0); - CHECK(user_profile_get_nts_priority(conf) == 0); - CHECK(user_profile_get_nts_expiry(conf) == 0); + auto pic = conf.get_profile_pic(); + CHECK(pic.url.size() == 0); + CHECK(conf.get_nts_priority() == 0); + CHECK(conf.get_nts_expiry() == std::nullopt); // Now let's go set them: - CHECK(0 == user_profile_set_name(conf, "Kallie")); - user_profile_pic p; - strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < sizeof(p.url)! - memcpy(p.key, "secret78901234567890123456789012", 32); - CHECK(0 == user_profile_set_pic(conf, p)); - user_profile_set_nts_priority(conf, 9); + conf.set_name("Kallie"); + session::config::profile_pic p; + { + // These don't stay alive, so we use set_key/set_url to make a local copy: + ustring key = "secret78901234567890123456789012"_bytes; + std::string url = + "http://example.org/omg-pic-123.bmp"; // NB: length must be < sizeof(p.url)! + p.set_key(std::move(key)); + p.url = std::move(url); + } + conf.set_profile_pic(p); + conf.set_nts_priority(9); // Retrieve them just to make sure they set properly: - name = user_profile_get_name(conf); - REQUIRE(name != nullptr); // (should be NULL instead of nullptr in C) + name = conf.get_name(); + REQUIRE(name != std::nullopt); CHECK(name == "Kallie"sv); - pic = user_profile_get_pic(conf); - REQUIRE(pic.url); - REQUIRE(pic.key); - CHECK(pic.url == "http://example.org/omg-pic-123.bmp"sv); - CHECK(ustring_view{pic.key, 32} == "secret78901234567890123456789012"_bytes); + pic = conf.get_profile_pic(); + REQUIRE(pic.url.size() > 0); + REQUIRE(pic.key.size() > 0); + CHECK(pic.url == "http://example.org/omg-pic-123.bmp"); + CHECK(pic.key == "secret78901234567890123456789012"_bytes); - CHECK(user_profile_get_nts_priority(conf) == 9); + CHECK(conf.get_nts_priority() == 9); // Since we've made changes, we should need to push new config to the swarm, *and* should need // to dump the updated state: - CHECK(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); - to_push = config_push(conf); - CHECK(to_push->seqno == 1); // incremented since we made changes (this only increments once - // between dumps; even though we changed two fields here). + CHECK(conf.needs_push()); + CHECK(conf.needs_dump()); + std::tie(seqno, to_push, obs) = conf.push(); + CHECK(seqno == 1); // incremented since we made changes (this only increments once + // between dumps; even though we changed two fields here). // The hash of a completely empty, initial seqno=0 message: auto exp_hash0 = "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965"_hexbytes; @@ -149,27 +147,18 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea" "49bf122762d7bc1d6d9c02f6d54f8384"_hexbytes; - // Copy this out; we need to hold onto it to do the confirmation later on - seqno_t seqno = to_push->seqno; - - // config_push gives us back a buffer that we are required to free when done. (Without this - // we'd leak memory!) - free(to_push); - // We haven't dumped, so still need to dump: - CHECK(config_needs_dump(conf)); + CHECK(conf.needs_dump()); // We did call push, but we haven't confirmed it as stored yet, so this will still return true: - CHECK(config_needs_push(conf)); - unsigned char* dump1; - size_t dump1len; + CHECK(conf.needs_push()); - config_dump(conf, &dump1, &dump1len); + auto dump1 = conf.dump(); // (in a real client we'd now store this to disk) - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // clang-format off - CHECK(printable(dump1, dump1len) == printable( + CHECK(printable({dump1.data(), dump1.size()}) == printable( "d" "1:!" "i2e" "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" @@ -177,18 +166,17 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:)" "le" "e")); // clang-format on - free(dump1); // done with the dump; don't leak! // So now imagine we got back confirmation from the swarm that the push has been stored: - config_confirm_pushed(conf, seqno, "fakehash1"); + conf.confirm_pushed(seqno, "fakehash1"); - CHECK_FALSE(config_needs_push(conf)); - CHECK(config_needs_dump(conf)); // The confirmation changes state, so this makes us need a dump - // again. - config_dump(conf, &dump1, &dump1len); + CHECK_FALSE(conf.needs_push()); + CHECK(conf.needs_dump()); // The confirmation changes state, so this makes us need a dump + // again. + dump1 = conf.dump(); // clang-format off - CHECK(printable(dump1, dump1len) == printable( + CHECK(printable({dump1.data(), dump1.size()}) == printable( "d" "1:!" "i0e" "1:$" + std::to_string(exp_push1_decrypted.size()) + ":" + std::string{to_sv(exp_push1_decrypted)} + "" @@ -196,159 +184,136 @@ TEST_CASE("user profile C API", "[config][user_profile][c]") { "1:)" "le" "e")); // clang-format on - free(dump1); - CHECK_FALSE(config_needs_dump(conf)); + CHECK_FALSE(conf.needs_dump()); // Now we're going to set up a second, competing config object (in the real world this would be // another Session client somewhere). // Start with an empty config, as above: - config_object* conf2; - REQUIRE(user_profile_init(&conf2, ed_sk.data(), NULL, 0, err) == 0); - config_set_logger(conf2, log_msg, NULL); - CHECK_FALSE(config_needs_dump(conf2)); + session::config::UserProfile conf2{ustring_view{seed}, std::nullopt}; + conf2.logger = log_msg; + CHECK_FALSE(conf2.needs_dump()); // Now imagine we just pulled down the encrypted string from the swarm; we merge it into conf2: - const unsigned char* merge_data[1]; - const char* merge_hash[1]; - size_t merge_size[1]; - merge_hash[0] = "fakehash1"; - merge_data[0] = exp_push1_encrypted.data(); - merge_size[0] = exp_push1_encrypted.size(); - config_string_list* accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash1"sv); - free(accepted); + std::vector> merge_configs; + merge_configs.emplace_back("fakehash1", exp_push1_encrypted); + auto accepted = conf2.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash1"sv); // Our state has changed, so we need to dump: - CHECK(config_needs_dump(conf2)); - unsigned char* dump2; - size_t dump2len; - config_dump(conf2, &dump2, &dump2len); + CHECK(conf2.needs_dump()); + auto dump2 = conf2.dump(); // (store in db) - free(dump2); - CHECK_FALSE(config_needs_dump(conf2)); + CHECK_FALSE(conf2.needs_dump()); // We *don't* need to push: even though we updated, all we did is update to the merged data (and // didn't have any sort of merge conflict needed): - REQUIRE_FALSE(config_needs_push(conf2)); + REQUIRE_FALSE(conf2.needs_push()); // Now let's create a conflicting update: // Change the name on both clients: - user_profile_set_name(conf, "Nibbler"); - user_profile_set_name(conf2, "Raz"); + conf.set_name("Nibbler"); + conf2.set_name("Raz"); // And, on conf2, we're also going to change some other things: - strcpy(p.url, "http://new.example.com/pic"); - memcpy(p.key, "qwert\0yuio1234567890123456789012", 32); - user_profile_set_pic(conf2, p); - - user_profile_set_nts_expiry(conf2, 86400); - CHECK(user_profile_get_nts_expiry(conf2) == 86400); - - CHECK(user_profile_get_blinded_msgreqs(conf2) == -1); - user_profile_set_blinded_msgreqs(conf2, 0); - CHECK(user_profile_get_blinded_msgreqs(conf2) == 0); - user_profile_set_blinded_msgreqs(conf2, -1); - CHECK(user_profile_get_blinded_msgreqs(conf2) == -1); - user_profile_set_blinded_msgreqs(conf2, 1); - CHECK(user_profile_get_blinded_msgreqs(conf2) == 1); + ustring key2 = "qwert\0yuio1234567890123456789012"_bytes; + std::string url2 = "http://new.example.com/pic"; + p.set_key(std::move(key2)); + p.url = std::move(url2); + conf2.set_profile_pic(p); + + conf2.set_nts_expiry(86400s); + CHECK(conf2.get_nts_expiry() == 86400s); + + CHECK(conf2.get_blinded_msgreqs() == std::nullopt); + conf2.set_blinded_msgreqs(false); + CHECK(conf2.get_blinded_msgreqs() == false); + conf2.set_blinded_msgreqs(std::nullopt); + CHECK(conf2.get_blinded_msgreqs() == std::nullopt); + conf2.set_blinded_msgreqs(true); + CHECK(conf2.get_blinded_msgreqs() == true); // Both have changes, so push need a push - CHECK(config_needs_push(conf)); - CHECK(config_needs_push(conf2)); - to_push = config_push(conf); - CHECK(to_push->seqno == 2); // incremented, since we made a field change - config_confirm_pushed(conf2, to_push->seqno, "fakehash2"); - - config_push_data* to_push2 = config_push(conf2); - CHECK(to_push2->seqno == 2); // incremented, since we made a field change - config_confirm_pushed(conf2, to_push2->seqno, "fakehash3"); - - config_dump(conf, &dump1, &dump1len); - config_dump(conf2, &dump2, &dump2len); + CHECK(conf.needs_push()); + CHECK(conf2.needs_push()); + std::tie(seqno, to_push, obs) = conf.push(); + CHECK(seqno == 2); // incremented, since we made a field change + conf.confirm_pushed(seqno, "fakehash2"); + + auto [seqno2, to_push2, obs2] = conf2.push(); + CHECK(seqno2 == 2); // incremented, since we made a field change + conf2.confirm_pushed(seqno2, "fakehash3"); + + dump1 = conf.dump(); + dump2 = conf2.dump(); // (store in db) - free(dump1); - free(dump2); // Since we set different things, we're going to get back different serialized data to be // pushed: - CHECK(printable(to_push->config, to_push->config_len) != - printable(to_push2->config, to_push2->config_len)); + CHECK(printable({to_push.data(), to_push.size()}) != + printable({to_push2.data(), to_push2.size()})); // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client // also fetches new messages and pulls down the other client's `seqno=2` value. // Feed the new config into each other. (This array could hold multiple configs if we pulled // down more than one). - merge_hash[0] = "fakehash2"; - merge_data[0] = to_push->config; - merge_size[0] = to_push->config_len; - accepted = config_merge(conf2, merge_hash, merge_data, merge_size, 1); - free(to_push); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash2"sv); - free(accepted); - merge_hash[0] = "fakehash3"; - merge_data[0] = to_push2->config; - merge_size[0] = to_push2->config_len; - accepted = config_merge(conf, merge_hash, merge_data, merge_size, 1); - REQUIRE(accepted->len == 1); - CHECK(accepted->value[0] == "fakehash3"sv); - free(accepted); - free(to_push2); + merge_configs[0] = {"fakehash2", to_push}; + accepted = conf2.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash2"sv); + + merge_configs[0] = {"fakehash3", to_push2}; + accepted = conf.merge(merge_configs); + REQUIRE(accepted.size() == 1); + CHECK(accepted[0] == "fakehash3"sv); // Now after the merge we *will* want to push from both client, since both will have generated a // merge conflict update (with seqno = 3). - to_push = config_push(conf); - to_push2 = config_push(conf2); + std::tie(seqno, to_push, obs) = conf.push(); + std::tie(seqno2, to_push2, obs2) = conf2.push(); - REQUIRE(to_push->seqno == 3); - REQUIRE(to_push2->seqno == 3); - REQUIRE(config_needs_push(conf)); - REQUIRE(config_needs_push(conf2)); + REQUIRE(seqno == 3); + REQUIRE(seqno2 == 3); + REQUIRE(conf.needs_push()); + REQUIRE(conf2.needs_push()); // They should have resolved the conflict to the same thing: - CHECK(user_profile_get_name(conf) == "Nibbler"sv); - CHECK(user_profile_get_name(conf2) == "Nibbler"sv); + CHECK(conf.get_name() == "Nibbler"sv); + CHECK(conf2.get_name() == "Nibbler"sv); // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized // message just happens to have a higher hash -- and thus gets priority -- for this particular // test). // Since only one of them set a profile pic there should be no conflict there: - pic = user_profile_get_pic(conf); - REQUIRE(pic.url); + pic = conf.get_profile_pic(); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(oxenc::to_hex(pic.key.begin(), pic.key.end()) == "7177657274007975696f31323334353637383930313233343536373839303132"); - pic = user_profile_get_pic(conf2); - REQUIRE(pic.url); + pic = conf2.get_profile_pic(); CHECK(pic.url == "http://new.example.com/pic"sv); - REQUIRE(pic.key); - CHECK(to_hex(ustring_view{pic.key, 32}) == + CHECK(oxenc::to_hex(pic.key.begin(), pic.key.end()) == "7177657274007975696f31323334353637383930313233343536373839303132"); - CHECK(user_profile_get_nts_priority(conf) == 9); - CHECK(user_profile_get_nts_priority(conf2) == 9); - CHECK(user_profile_get_nts_expiry(conf) == 86400); - CHECK(user_profile_get_nts_expiry(conf2) == 86400); - CHECK(user_profile_get_blinded_msgreqs(conf) == 1); - CHECK(user_profile_get_blinded_msgreqs(conf2) == 1); + CHECK(conf.get_nts_priority() == 9); + CHECK(conf2.get_nts_priority() == 9); + CHECK(conf.get_nts_expiry() == 86400s); + CHECK(conf2.get_nts_expiry() == 86400s); + CHECK(conf.get_blinded_msgreqs() == true); + CHECK(conf2.get_blinded_msgreqs() == true); - config_confirm_pushed(conf, to_push->seqno, "fakehash4"); - config_confirm_pushed(conf2, to_push2->seqno, "fakehash4"); + conf.confirm_pushed(seqno, "fakehash4"); + conf2.confirm_pushed(seqno2, "fakehash4"); - config_dump(conf, &dump1, &dump1len); - config_dump(conf2, &dump2, &dump2len); + dump1 = conf.dump(); + dump2 = conf2.dump(); // (store in db) - free(dump1); - free(dump2); - CHECK_FALSE(config_needs_dump(conf)); - CHECK_FALSE(config_needs_dump(conf2)); - CHECK_FALSE(config_needs_push(conf)); - CHECK_FALSE(config_needs_push(conf2)); + CHECK_FALSE(conf.needs_dump()); + CHECK_FALSE(conf2.needs_dump()); + CHECK_FALSE(conf.needs_push()); + CHECK_FALSE(conf2.needs_push()); } diff --git a/tests/test_group_info.cpp b/tests/test_group_info.cpp index 1896b47f..459fb294 100644 --- a/tests/test_group_info.cpp +++ b/tests/test_group_info.cpp @@ -8,6 +8,7 @@ #include #include +#include "session/errors.hpp" #include "utils.hpp" using namespace std::literals; @@ -161,10 +162,8 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { for (const auto& k : enc_keys1) // Just for testing, as above. ginfo.add_key(k, false); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group!"), "Unable to make changes to a read-only config object"); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group!"), session::Error::READ_ONLY_CONFIG); CHECK(!ginfo.is_dirty()); // This one is good and has the right signature: @@ -201,8 +200,7 @@ TEST_CASE("Verify-only Group Info", "[config][groups][verify-only]") { CHECK(ginfo.get_name() == "Super Group!!"); - REQUIRE_THROWS_WITH( - ginfo.set_name("Super Group11"), "Unable to make changes to a read-only config object"); + REQUIRE_THROWS_WITH(ginfo.set_name("Super Group11"), session::Error::READ_ONLY_CONFIG); // This shouldn't throw because it isn't *actually* changing a config value (i.e. re-setting the // same value does not dirty the config). It isn't clear why you'd need to do this, but still. ginfo.set_name("Super Group!!"); diff --git a/tests/test_group_keys.cpp b/tests/test_group_keys.cpp index 475ba217..25ac6178 100644 --- a/tests/test_group_keys.cpp +++ b/tests/test_group_keys.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -12,10 +14,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include "utils.hpp" @@ -25,6 +30,7 @@ using namespace oxenc::literals; static constexpr int64_t created_ts = 1680064059; +using namespace session::state; using namespace session::config; static std::array sk_from_seed(ustring_view seed) { @@ -557,47 +563,84 @@ TEST_CASE("Group Keys - C++ API", "[config][groups][keys][cpp]") { TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { struct pseudo_client { + std::string group_id; std::array secret_key; - const ustring_view public_key{secret_key.data() + 32, 32}; - std::string session_id{session_id_from_ed(public_key)}; + std::array user_secret_key; + const ustring_view user_public_key{user_secret_key.data() + 32, 32}; + std::string user_session_id{session_id_from_ed(user_public_key)}; - config_group_keys* keys; - config_object* info; - config_object* members; + state_object* state; + std::vector store_records; + std::vector send_records; pseudo_client( - ustring seed, - bool is_admin, - unsigned char* gpk, - std::optional gsk) : - secret_key{sk_from_seed(seed)} { - int rv = groups_members_init(&members, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL); - REQUIRE(rv == 0); - - rv = groups_info_init(&info, gpk, is_admin ? *gsk : NULL, NULL, 0, NULL); - REQUIRE(rv == 0); - - rv = groups_keys_init( - &keys, - secret_key.data(), - gpk, - is_admin ? *gsk : NULL, - info, - members, - NULL, + ustring user_seed, + std::optional group_id_ = std::nullopt, + std::optional> group_sk_ = std::nullopt) : + user_secret_key{sk_from_seed(user_seed)} { + char err[256]; + REQUIRE(state_init(&state, user_secret_key.data(), nullptr, 0, err)); + state_set_store_callback( + state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); + + // If we already have a group then just "approve" it + if (group_id_) { + auto gid = *group_id_; + group_id = gid; + + if (group_sk_) { + auto gsk = *group_sk_; + secret_key = gsk; + state_approve_group(state, gid.c_str()); + state_load_group_admin_key(state, gid.c_str(), gsk.data()); + return; + } + + state_approve_group(state, gid.c_str()); + return; + } + + pseudo_client* ctx = this; + state_create_group( + state, + "", 0, - NULL); - REQUIRE(rv == 0); + nullptr, + 0, + user_profile_pic(), + nullptr, + 0, + [](const char* group_id, + const unsigned char* group_sk, + const char* error, + const size_t error_len, + void* ctx) { + REQUIRE(error_len == 0); + + auto client = static_cast(ctx); + + // Now that the group is created store the values + client->group_id = group_id; + memcpy(client->secret_key.data(), group_sk, 64); + }, + ctx); + REQUIRE(send_records.size() == 1); + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":" + "\"fakehash3\"}}]}"); + send_records[0].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + send_records[0].callback_context); } - ~pseudo_client() { - config_free(info); - config_free(members); - } + ~pseudo_client() { state_free(state); } }; - const ustring group_seed = - "0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes; const ustring admin1_seed = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; const ustring admin2_seed = @@ -609,189 +652,268 @@ TEST_CASE("Group Keys - C API", "[config][groups][keys][c]") { "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"_hexbytes // member4 }; - std::array group_pk; - std::array group_sk; - - crypto_sign_ed25519_seed_keypair( - group_pk.data(), - group_sk.data(), - reinterpret_cast(group_seed.data())); - REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) == - oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32)); - hacky_list admins; hacky_list members; // Initialize admin and member objects - admins.emplace_back(admin1_seed, true, group_pk.data(), group_sk.data()); - admins.emplace_back(admin2_seed, true, group_pk.data(), group_sk.data()); + admins.emplace_back(admin1_seed); + + auto& admin1 = admins[0]; + admins.emplace_back(admin2_seed, admin1.group_id, admin1.secret_key); for (int i = 0; i < 4; ++i) - members.emplace_back(member_seeds[i], false, group_pk.data(), std::nullopt); + members.emplace_back(member_seeds[i], admin1.group_id); - REQUIRE(admins[0].session_id == + REQUIRE(admins[0].user_session_id == "05f1e8b64bbf761edf8f7b47e3a1f369985644cce0a62adb8e21604474bdd49627"); - REQUIRE(admins[1].session_id == + REQUIRE(admins[1].user_session_id == "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); - REQUIRE(members[0].session_id == + REQUIRE(members[0].user_session_id == "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628"); - REQUIRE(members[1].session_id == + REQUIRE(members[1].user_session_id == "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c"); - REQUIRE(members[2].session_id == + REQUIRE(members[2].user_session_id == "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"); - REQUIRE(members[3].session_id == + REQUIRE(members[3].user_session_id == "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"); - for (const auto& a : admins) - REQUIRE(groups_members_size(a.members) == 0); - for (const auto& m : members) - REQUIRE(groups_members_size(m.members) == 0); - - // add admin account, re-key, distribute - auto& admin1 = admins[0]; - config_group_member new_admin1; - - REQUIRE(groups_members_get_or_construct( - admin1.members, &new_admin1, admin1.session_id.c_str())); - - new_admin1.admin = true; - groups_members_set(admin1.members, &new_admin1); - - CHECK(config_needs_push(admin1.members)); - - const unsigned char* new_keys_config_1; - size_t key_len1; - REQUIRE(groups_keys_pending_config(admin1.keys, &new_keys_config_1, &key_len1)); - - config_push_data* new_info_config1 = config_push(admin1.info); - CHECK(new_info_config1->seqno == 1); - - config_push_data* new_mem_config1 = config_push(admin1.members); - CHECK(new_mem_config1->seqno == 1); + auto& admin2 = admins[1]; + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 1); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 1); - const char* merge_hash1[1]; - const unsigned char* merge_data1[2]; - size_t merge_size1[2]; - - merge_hash1[0] = "fakehash1"; - - merge_data1[0] = new_info_config1->config; - merge_size1[0] = new_info_config1->config_len; - - merge_data1[1] = new_mem_config1->config; - merge_size1[1] = new_mem_config1->config_len; + for (const auto& m : members) + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + + // Manually add member, re-key, distribute + auto& member1 = members[0]; + state_group_member new_member1; + REQUIRE(state_get_or_construct_group_member( + admin1.state, + admin1.group_id.c_str(), + &new_member1, + member1.user_session_id.c_str(), + nullptr)); + + state_mutate_group( + admin1.state, + admin1.group_id.c_str(), + [](mutable_group_state_object* state, void* ctx) { + state_set_group_member(state, static_cast(ctx)); + CHECK(state_rekey_group(state)); + }, + &new_member1); + + CHECK(session::state::unbox(admin1.state) + .config(admin1.group_id) + .needs_push()); + + REQUIRE(admin1.send_records.size() == 3); + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_INFO) == 2); + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 2); + ustring send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash1\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash2\"}},{\"code\":200,\"body\":{\"hash\":\"fakehash3\"}" + "}]}"); + admin1.send_records[2].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + admin1.send_records[2].callback_context); + + auto first_request_data = nlohmann::json::json_pointer("/params/requests/0/params/data"); + auto second_request_data = nlohmann::json::json_pointer("/params/requests/1/params/data"); + auto third_request_data = nlohmann::json::json_pointer("/params/requests/2/params/data"); + auto last_send_json = nlohmann::json::parse(admin1.send_records[2].payload); + REQUIRE(last_send_json.contains(first_request_data)); + REQUIRE(last_send_json.contains(second_request_data)); + REQUIRE(last_send_json.contains(third_request_data)); + auto last_send_data_0 = session::to_unsigned( + oxenc::from_base64(last_send_json[first_request_data].get())); + auto last_send_data_1 = session::to_unsigned( + oxenc::from_base64(last_send_json[second_request_data].get())); + auto last_send_data_2 = session::to_unsigned( + oxenc::from_base64(last_send_json[third_request_data].get())); + state_config_message* merge_data = new state_config_message[3]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash1", + created_ts, + last_send_data_0.data(), + last_send_data_0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash2", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash3", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; + state_config_message* merge_data_no_keys = new state_config_message[2]; + merge_data_no_keys[0] = { + NAMESPACE_GROUP_INFO, + "fakehash2", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + merge_data_no_keys[1] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash3", + created_ts, + last_send_data_2.data(), + last_send_data_2.size()}; /* Even though we have only added one admin, admin2 will still be able to see group info like group size and merge all configs. This is because they have loaded the key config message, which they can decrypt with the group secret key. + + We also need to merge for admin1 here because the Keys config won't update it's state + until it actually merges the updated keys */ for (auto& a : admins) { - REQUIRE(groups_keys_load_message( - a.keys, - "fakekeyshash1", - new_keys_config_1, - key_len1, - get_timestamp_ms(), - a.info, - a.members)); - config_string_list* hashes; - hashes = config_merge(a.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1); - REQUIRE(hashes->len); - free(hashes); - config_confirm_pushed(a.info, new_info_config1->seqno, "fakehash1"); - - hashes = config_merge(a.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1); - REQUIRE(hashes->len); - free(hashes); - config_confirm_pushed(a.members, new_mem_config1->seqno, "fakehash1"); - - REQUIRE(groups_members_size(a.members) == 1); + session_string_list* accepted; + REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash1"sv); + CHECK(accepted->value[1] == "fakehash2"sv); + CHECK(accepted->value[2] == "fakehash3"sv); + free(accepted); } - /* All attempts to merge non-admin members will throw, as none of the non admin members - will be able to decrypt the new info/member configs using the updated keys - */ - for (auto& m : members) { - // this will return true if the message was parsed successfully, NOT if the keys were - // decrypted - REQUIRE(groups_keys_load_message( - m.keys, - "fakekeyshash1", - new_keys_config_1, - key_len1, - get_timestamp_ms(), - m.info, - m.members)); - config_string_list* hashes; - REQUIRE_THROWS( - hashes = config_merge(m.info, merge_hash1, &merge_data1[0], &merge_size1[0], 1)); - REQUIRE_THROWS( - hashes = config_merge(m.members, merge_hash1, &merge_data1[1], &merge_size1[1], 1)); + // Due to the 'load_admin_key' behaviour admin2 will contain both admin1 and admin2 + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 2); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 3); - REQUIRE(groups_members_size(m.members) == 0); + /* Non-admins */ + for (auto& m : members) { + // Non-admin members cannot merge without the updated encryption keys + session_string_list* accepted; + REQUIRE_FALSE(state_merge(m.state, m.group_id.c_str(), merge_data_no_keys, 2, &accepted)); + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + m.state->last_error = nullptr; + + // The first member will be able to decrypt the keys (since they are a member), and + // info/member configs once they have the updated keys but the others aren't members so + // should fail + if (m.user_session_id == member1.user_session_id) { + REQUIRE(state_merge(m.state, m.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash1"sv); + CHECK(accepted->value[1] == "fakehash2"sv); + CHECK(accepted->value[2] == "fakehash3"sv); + free(accepted); + + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 2); + } else { + REQUIRE_FALSE(state_merge(m.state, m.group_id.c_str(), merge_data, 3, &accepted)); + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 0); + m.state->last_error = nullptr; + } } - free(new_info_config1); - free(new_mem_config1); + free(merge_data_no_keys); + free(merge_data); - for (int i = 0; i < members.size(); ++i) { - config_group_member new_mem; + CHECK_FALSE( + session::state::unbox(admin1.state).config(admin1.group_id).needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state) + .config(admin1.group_id) + .needs_push()); + CHECK_FALSE(session::state::unbox(admin1.state) + .config(admin1.group_id) + .pending_config() + .has_value()); + + std::vector new_members; + new_members.reserve(members.size()); - REQUIRE(groups_members_get_or_construct( - members[i].members, &new_mem, members[i].session_id.c_str())); + for (auto& m : members) { + auto new_mem = state_group_member(); + REQUIRE(state_get_or_construct_group_member( + admin1.state, + admin1.group_id.c_str(), + &new_mem, + m.user_session_id.c_str(), + nullptr)); new_mem.admin = false; - groups_members_set(admin1.members, &new_mem); + new_members.push_back(new_mem); } - CHECK(config_needs_push(admin1.members)); - - const unsigned char* new_keys_config_2; - size_t key_len2; - REQUIRE(groups_keys_rekey( - admin1.keys, admin1.info, admin1.members, &new_keys_config_2, &key_len2)); - - config_push_data* new_info_config2 = config_push(admin1.info); - CHECK(new_info_config2->seqno == 2); - - config_push_data* new_mem_config2 = config_push(admin1.members); - CHECK(new_mem_config2->seqno == 2); - - const char* merge_hash2[1]; - const unsigned char* merge_data2[2]; - size_t merge_size2[2]; - - merge_hash2[0] = "fakehash2"; - - merge_data2[0] = new_info_config2->config; - merge_size2[0] = new_info_config2->config_len; + // Add members via 'add_member' using supplemental rotation instead of manually + state_add_group_members( + admin1.state, + admin1.group_id.c_str(), + true, + new_members.data(), + new_members.size(), + [](const char* error, size_t error_len, void* ctx) { REQUIRE(error_len == 0); }, + nullptr); + + REQUIRE(admin1.send_records.size() == 4); + send_response = session::to_unsigned( + "{\"results\":[{\"code\":200,\"body\":{\"hash\":\"fakehash4\"}},{\"code\":200," + "\"body\":{\"hash\":\"fakehash5\"}}]}"); + admin1.send_records[3].response_cb( + true, + 200, + send_response.data(), + send_response.size(), + admin1.send_records[3].callback_context); + + CHECK(state_current_seqno(admin1.state, admin1.group_id.c_str(), NAMESPACE_GROUP_MEMBERS) == 3); + + last_send_json = nlohmann::json::parse(admin1.send_records[3].payload); + REQUIRE(last_send_json.contains(first_request_data)); + REQUIRE(last_send_json.contains(second_request_data)); + last_send_data_0 = session::to_unsigned( + oxenc::from_base64(last_send_json[first_request_data].get())); + last_send_data_1 = session::to_unsigned( + oxenc::from_base64(last_send_json[second_request_data].get())); + merge_data = new state_config_message[2]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash4", + created_ts, + last_send_data_0.data(), + last_send_data_0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash5", + created_ts, + last_send_data_1.data(), + last_send_data_1.size()}; + + /* Admins will store the hash for supplemental keys messages but won't actually consider them + * merged so the 'accepted' array won't contain the hash */ + for (auto& a : admins) { + session_string_list* accepted; + REQUIRE(state_merge(a.state, a.group_id.c_str(), merge_data, 2, &accepted)); + REQUIRE(accepted->len == 1); + CHECK(accepted->value[0] == "fakehash5"sv); + free(accepted); + } - merge_data2[1] = new_mem_config2->config; - merge_size2[1] = new_mem_config2->config_len; + // Due to the 'load_admin_key' behaviour admin2 will contain both admin1 and admin2 + REQUIRE(state_size_group_members(admin1.state, admin1.group_id.c_str()) == 5); + REQUIRE(state_size_group_members(admin2.state, admin2.group_id.c_str()) == 6); - for (auto& a : admins) { - REQUIRE(groups_keys_load_message( - a.keys, - "fakekeyshash2", - new_keys_config_2, - key_len2, - get_timestamp_ms(), - a.info, - a.members)); - config_string_list* hashes; - hashes = config_merge(a.info, merge_hash2, &merge_data2[0], &merge_size2[0], 1); - REQUIRE(hashes->len); - free(hashes); - config_confirm_pushed(a.info, new_info_config2->seqno, "fakehash2"); - hashes = config_merge(a.members, merge_hash2, &merge_data2[1], &merge_size2[1], 1); - REQUIRE(hashes->len); - free(hashes); - config_confirm_pushed(a.members, new_mem_config2->seqno, "fakehash2"); - - REQUIRE(groups_members_size(a.members) == 5); + /* Non-admins */ + for (auto& m : members) { + session_string_list* accepted; + REQUIRE(state_merge(m.state, m.group_id.c_str(), merge_data, 2, &accepted)); + REQUIRE(accepted->len == 2); + CHECK(accepted->value[0] == "fakehash4"sv); + CHECK(accepted->value[1] == "fakehash5"sv); + free(accepted); + + REQUIRE(state_size_group_members(m.state, m.group_id.c_str()) == 5); } - free(new_info_config2); - free(new_mem_config2); + free(merge_data); } TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]") { diff --git a/tests/test_state.cpp b/tests/test_state.cpp new file mode 100644 index 00000000..99d49cd3 --- /dev/null +++ b/tests/test_state.cpp @@ -0,0 +1,1142 @@ +#include +#include + +#include +#include + +#include "session/config/contacts.h" +#include "session/config/groups/members.h" +#include "session/config/namespaces.hpp" +#include "session/config/user_groups.h" +#include "session/config/user_profile.h" +#include "session/config/user_profile.hpp" +#include "session/state.h" +#include "session/state.hpp" +#include "session/state_groups.h" +#include "utils.hpp" + +using namespace std::literals; +using namespace oxenc::literals; +using namespace session; +using namespace session::state; +using namespace session::config; +using json_ptr = nlohmann::json::json_pointer; + +static constexpr int64_t created_ts = 1680064059; +using response_callback_t = + std::function; + +static std::array sk_from_seed(ustring_view seed) { + std::array ignore; + std::array sk; + crypto_sign_ed25519_seed_keypair(ignore.data(), sk.data(), seed.data()); + return sk; +} + +static ustring send_response(std::vector hashes) { + std::string result = "{\"results\":["; + + for (auto& hash : hashes) + result += "{\"code\":200,\"body\":{\"hash\":\"" + std::string(hash) + "\"}},"; + + if (!hashes.empty()) + result.pop_back(); // Remove last comma + + result += "]}"; + return to_unsigned(result); +} + +static void setup_store_hook(State& state, std::vector& store_records) { + state.on_store([&store_records]( + config::Namespace namespace_, + std::string pubkey, + uint64_t timestamp_ms, + ustring data) { + store_records.push_back({namespace_, pubkey, timestamp_ms, data}); + }); +} + +static void setup_send_hook(State& state, std::vector& send_records) { + state.on_send( + [&send_records]( + std::string pubkey, ustring payload, response_callback_t received_response) { + // Replicate the behaviour in the C wrapper + auto on_response = + std::make_unique(std::move(received_response)); + + send_records.push_back( + {pubkey, + payload, + [](bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context) { + try { + // Recapture the std::function callback here in a unique_ptr so + // that we clean it up at the end of this lambda. + std::unique_ptr cb{ + static_cast(callback_context)}; + (*cb)(success, status_code, {res, reslen}); + return true; + } catch (...) { + return false; + } + }, + nullptr, + on_response.release()}); + }); +} + +TEST_CASE("State", "[state][state]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); + + // Sanity check direct config access + CHECK_FALSE(state.config().get_name().has_value()); + state.mutable_config().user_profile.set_name("Test Name"); + CHECK(state.config().get_name() == "Test Name"); + REQUIRE(store_records.size() == 1); + REQUIRE(send_records.size() == 1); + CHECK(store_records[0].namespace_ == Namespace::UserProfile); + CHECK(store_records[0].pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(oxenc::to_hex(store_records[0].data.begin(), store_records[0].data.end()) == + "64313a21693165313a2438343a64313a23693165313a2664313a6e393a54657374204e616d6565313a3c6c6c" + "69306533323aea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c96564656565313a" + "3d64313a6e303a6565313a28303a313a296c6565"); + CHECK(send_records[0].pubkey == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + auto send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserProfile)); + CHECK(send_data.value(json_ptr("/params/requests/0/params/data"), "") == + "CAESqwMKABIAGqIDCAYoAUKbA02D9u45MzHN7luC80geUgdkpzPP8LNtakE7og80impxF++vn+" + "piV1rPki0Quo5Zp34MwwdZXqMFEwRpKGZJwpFPSre6jln5XlmH8tnq8djJo/" + "7QP8kH4m8uUfzsRNgZ1K6agbnGgRolBXgk86/" + "yFmmEsyC81rJF1dgqtkmOhA3nIFpk+yaPt5U5BzsELMQj3sydDB+" + "2iLQE4rIwH43lUtNj2S2YoQ27Mv2FDclbPMOdCOJyTENWt5k/" + "eo0Zovg012oOixj1Uq9I7M9fajgklO+GmE3I3LFGXkmDoDwLYyPavWe68FU8zV9OtFFfUKdIxRJUTZXgU8Kwxzc/" + "U3RzIm8Sc7APgIPkJsTmJr+ckYzLEdzbrqae4gxvzFB22lZYt62rg7KVoaBWUcB3NgFhTxMGc37ysti0pfoxO/" + "T+zkKertLqX+iWNZLRhy3kLaXhEkqafYQzikepvhzD8/" + "PZqc0ZOJ+vF35HSHh3zUMhDZZ4ZS4gcXRy7nLqEtoAUuRLB9GxB4+A2brXr95FWTj2QQE6NSt9tf7JqaOf/yAA"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(state.config().get_seqno() == 1); + + // Confirm the push + ustring send_res = send_response({"fakehash1"}); + REQUIRE(send_records[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); + CHECK(store_records.size() == 2); // Should call store after confirming the push + CHECK_FALSE(state.config().needs_push()); + + // Init with dumps + auto dump = state.dump(Namespace::UserProfile); + auto state2 = + State({ed_sk.data(), ed_sk.size()}, {{Namespace::UserProfile, std::nullopt, dump}}); + CHECK_FALSE(state2.config().needs_push()); + CHECK_FALSE(state2.config().needs_dump()); + CHECK(state2.config().get_name() == "Test Name"); + + // Explicit load + auto state3 = State({ed_sk.data(), ed_sk.size()}, {}); + CHECK_FALSE(state3.config().get_name().has_value()); + state3.load(Namespace::UserProfile, std::nullopt, dump); + CHECK(state3.config().get_name() == "Test Name"); + + // Creating a group works correctly + session::config::profile_pic p; + { + // These don't stay alive, so we use set_key/set_url to make a local copy: + ustring key = "qwerty78901234567890123456789012"_bytes; + std::string url = "http://example.com/huge.bmp"; + p.set_key(std::move(key)); + p.url = std::move(url); + } + const std::array member_seeds = { + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118", // member3 + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d", // member4 + "052222222222222222222222222222222222222222222222222222222222222222", // member5 + }; + std::vector members; + members.reserve(2); + members.emplace_back(groups::member(member_seeds[0])); + members.emplace_back(groups::member(member_seeds[1])); + members[0].set_name("Member 0"); + members[1].set_name("Member 1"); + + state.create_group( + "TestName", + "TestDesc", + std::move(p), + members, + [&state]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { + REQUIRE_FALSE(error.has_value()); + + auto g = state.config().get_group(group_id); + REQUIRE(g.has_value()); + CHECK(g->name == "TestName"); + CHECK(g->secretkey == group_sk); + CHECK(state.config(group_id).get_seqno() == 1); + CHECK(state.config(group_id).get_seqno() == 1); + CHECK(state.config(group_id).current_generation() == 0); + CHECK(state.config(group_id).admin()); + }); + + REQUIRE(send_records.size() == 2); + send_data = nlohmann::json::parse(send_records[1].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), uint64_t(0)) == 2592000000); + + CHECK_FALSE(state.config().needs_push()); + CHECK(store_records.size() == 2); // Not stored until we process a success response + send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); + REQUIRE(send_records[1].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[1].callback_context)); + CHECK(store_records.size() == 6); + CHECK(store_records[2].namespace_ == Namespace::UserGroups); + CHECK(store_records[3].namespace_ == Namespace::GroupKeys); + CHECK(store_records[4].namespace_ == Namespace::GroupInfo); + CHECK(store_records[5].namespace_ == Namespace::GroupMembers); + CHECK(state.config().get_seqno() == 1); + CHECK(state.config().needs_push()); + + // Prepare to merge the group data + std::vector to_merge; + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash2", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupInfo, + "fakehash3", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupMembers, + "fakehash4", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))); + + // Once the create group 'send' is confirm we add the group to UserGroups and also need to send + // that updated config + REQUIRE(send_records.size() == 3); + send_data = nlohmann::json::parse(send_records[2].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "0577cb6c50ed49a2c45e383ac3ca855375c68300f7ff0c803ea93cb18437d61f46"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + send_res = send_response({"fakehash5"}); + REQUIRE(send_records[2].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[2].callback_context)); + + REQUIRE(state.config().size_groups() == 1); + auto member4_sid = "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d"; + auto group = *state.config().begin_groups(); + CHECK(state.config(group.id).get_seqno() == 1); + CHECK(state.config(group.id).get_seqno() == 1); + CHECK(state.config(group.id).current_generation() == 0); + CHECK_FALSE(state.config().needs_push()); + + // Keys only get loaded when merging so we need to trigger the merge + auto merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash2"); + CHECK(merge_result[1] == "fakehash3"); + CHECK(merge_result[2] == "fakehash4"); + CHECK(send_records.size() == 3); + + // Check that the supplemental rotation calls everything correctly + std::vector supplemental_members; + supplemental_members.emplace_back(member_seeds[2]); + state.add_group_members( + group.id, true, supplemental_members, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 4); + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/2/params/messages/0"), "") == "fakehash4"); + + send_res = send_response({"fakehash5", "fakehash6"}); + REQUIRE(send_records[3].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[3].callback_context)); +} + +TEST_CASE("State", "[state][state][merge failure behaviour]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + auto state2 = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); + setup_store_hook(state2, store_records_2); + setup_send_hook(state2, send_records_2); + + // Setup an initial state + state.mutable_config().user_profile.set_name("Test Name1"); + REQUIRE(send_records.size() == 1); + CHECK(store_records.size() == 1); + ustring send_res = send_response({"fakehash1"}); + REQUIRE(send_records[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); + CHECK(store_records.size() == 2); + + // Merge into state2 so they are consistent + std::vector to_merge; + auto send_data = nlohmann::json::parse(send_records[0].payload); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash1", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + auto merge_result = state2.merge(std::nullopt, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash1"); + + // Modify state2 to generate valid data to merge + state2.mutable_config().user_profile.set_name("Test Name"); + REQUIRE(send_records_2.size() == 1); + CHECK(store_records_2.size() == 2); + send_res = send_response({"fakehash2"}); + REQUIRE(send_records_2[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); + CHECK(store_records.size() == 2); + + state2.mutable_config().user_profile.set_name("Test Name2"); + REQUIRE(send_records_2.size() == 2); + CHECK(store_records_2.size() == 4); + send_res = send_response({"fakehash3"}); + REQUIRE(send_records_2[1].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); + CHECK(store_records_2.size() == 5); + REQUIRE(state2.config().get_name().has_value()); + CHECK(*state2.config().get_name() == "Test Name2"); + + // Generate a valid and an invalid config for the merge (only the valid one should be merged) + send_data = nlohmann::json::parse(send_records_2[0].payload); + auto valid_timestamp = send_data[json_ptr("/params/requests/0/params/timestamp")].get(); + auto invalid_send_data = nlohmann::json::parse(send_records_2[1].payload); + auto invalid_payload = oxenc::from_base64( + invalid_send_data[json_ptr("/params/requests/0/params/data")].get()); + invalid_payload.replace(invalid_payload.begin(), invalid_payload.begin() + 4, "RAWR"); + to_merge.clear(); + to_merge.emplace_back( + Namespace::UserProfile, + "fakehash2", + valid_timestamp, + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + to_merge.emplace_back( + Namespace::UserProfile, "fakehash3", 3000000000000, to_unsigned(invalid_payload)); + merge_result = state.merge(std::nullopt, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash2"); + CHECK(send_records.size() == 1); // Unchanged + REQUIRE(state.config().get_name().has_value()); + CHECK(*state.config().get_name() == "Test Name"); + REQUIRE(store_records.size() == 3); + CHECK(store_records[2].timestamp == valid_timestamp); + + // Now try to merge solely invalid data, the send/store hooks shouldn't get called + CHECK(send_records.size() == 1); + CHECK(store_records.size() == 3); + to_merge.clear(); + to_merge.emplace_back( + Namespace::UserProfile, "fakehash3", 3000000000000, to_unsigned(invalid_payload)); + merge_result = state.merge(std::nullopt, to_merge); + CHECK(merge_result.size() == 0); + CHECK(send_records.size() == 1); // Unchanged + CHECK(store_records.size() == 3); // Unchanged +} + +TEST_CASE("State", "[state][state][merge key conflict]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + const std::string admin2_sid = + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"; + const std::array member_seeds = { + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", // member1 + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", // member2 + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118", // member3 + "050a41669a06c098f22633aee2eba03764ef6813bd4f770a3a2b9033b868ca470d", // member4 + }; + + auto state = State({ed_sk.data(), ed_sk.size()}, {}); + std::vector store_records; + std::vector send_records; + setup_store_hook(state, store_records); + setup_send_hook(state, send_records); + + auto admin2_sk = sk_from_seed({admin2_seed.data(), admin2_seed.size()}); + auto state_admin_2 = State({admin2_sk.data(), admin2_sk.size()}, {}); + std::vector store_records_2; + std::vector send_records_2; + setup_store_hook(state_admin_2, store_records_2); + setup_send_hook(state_admin_2, send_records_2); + + // Create the initial group + std::vector members; + members.reserve(3); + members.emplace_back(groups::member(member_seeds[0])); + members.emplace_back(groups::member(member_seeds[1])); + members.emplace_back(groups::member{admin2_sid}); + members[0].set_name("Member 0"); + members[1].set_name("Member 1"); + members[2].set_name("Admin 2"); + + state.create_group( + "TestName", + "TestDesc", + profile_pic(), + members, + [&state]( + std::string_view group_id, + ustring_view group_sk, + std::optional error) { REQUIRE_FALSE(error.has_value()); }); + + REQUIRE(send_records.size() == 1); + auto send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + ustring send_res = send_response({"fakehash1", "fakehash2", "fakehash3"}); + REQUIRE(send_records[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[0].callback_context)); + REQUIRE(send_records.size() == 2); // Group added to UserGroups + send_data = nlohmann::json::parse(send_records[1].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + send_res = send_response({"fakehash4"}); + REQUIRE(send_records[1].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[1].callback_context)); + REQUIRE(state.config().size_groups() == 1); + auto group = *state.config().begin_groups(); + + // Group keys aren't finalised until they have been retrieved and merged in + std::vector to_merge; + send_data = nlohmann::json::parse(send_records[0].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash1", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + auto merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash1"); + CHECK(send_records.size() == 2); // Unchanged + + // Load the group for admin2 + state_admin_2.approve_group(group.id); + REQUIRE(send_records_2.size() == 1); + send_data = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + send_res = send_response({"fakehash5"}); + REQUIRE(send_records_2[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); + REQUIRE(send_records_2.size() == 1); // Unchanged + + send_data = nlohmann::json::parse(send_records[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + to_merge.clear(); + to_merge.emplace_back(config_message{ + Namespace::GroupKeys, + "fakehash1", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}); + to_merge.emplace_back(config_message{ + Namespace::GroupInfo, + "fakehash2", + send_data.value(json_ptr("/params/requests/1/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))}); + to_merge.emplace_back(config_message{ + Namespace::GroupMembers, + "fakehash3", + send_data.value(json_ptr("/params/requests/2/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))}); + merge_result = state_admin_2.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash1"); + CHECK(merge_result[1] == "fakehash2"); + CHECK(merge_result[2] == "fakehash3"); + + // Promote to admin + state_admin_2.load_group_admin_key(group.id, group.secretkey); + REQUIRE(send_records_2.size() == 3); + + // UserGroups gets the admin key + send_data = nlohmann::json::parse(send_records_2[1].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); + send_res = send_response({"fakehash6"}); + REQUIRE(send_records_2[1].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); + + // Member flagged as an admin + send_data = nlohmann::json::parse(send_records_2[2].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == group.id); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash3"); + send_res = send_response({"fakehash7"}); + REQUIRE(send_records_2[2].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[2].callback_context)); + REQUIRE(send_records_2.size() == 3); // Unchanged + REQUIRE(state_admin_2.config(group.id).admin()); + + // Merge the member change into admin1 + to_merge.clear(); + to_merge.emplace_back(config_message{ + Namespace::GroupMembers, + "fakehash7", + send_data.value(json_ptr("/params/requests/0/params/timestamp"), uint64_t(0)), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))}); + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash7"); + REQUIRE(send_records.size() == 2); // Unchanged + + // Create a conflict between the members/keys + std::vector conflict_members_1, conflict_members_2; + conflict_members_1.emplace_back(member_seeds[2]); + conflict_members_2.emplace_back(member_seeds[3]); + state.add_group_members( + group.id, false, conflict_members_1, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + state_admin_2.add_group_members( + group.id, false, conflict_members_2, [](std::optional error) { + REQUIRE_FALSE(error.has_value()); + }); + + REQUIRE(send_records.size() == 3); + send_data = nlohmann::json::parse(send_records[2].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 3); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash7"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/2"), "") == "fakehash3"); + send_res = send_response({"fakehash8", "fakehash9", "fakehash10"}); + REQUIRE(send_records[2].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[2].callback_context)); + + // Group keys aren't finalised until they have been retrieved and merged in + to_merge.clear(); + send_data = nlohmann::json::parse(send_records[2].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash8", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash8"); + CHECK(send_records.size() == 3); // Unchanged + + REQUIRE(send_records_2.size() == 4); + send_data = nlohmann::json::parse(send_records_2[3].payload); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 2); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash2"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash7"); + send_res = send_response({"fakehash11", "fakehash12", "fakehash13"}); + REQUIRE(send_records_2[3].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[3].callback_context)); + + // Group keys aren't finalised until they have been retrieved and merged in + to_merge.clear(); + send_data = nlohmann::json::parse(send_records_2[3].payload); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash11", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + merge_result = state_admin_2.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 1); + CHECK(merge_result[0] == "fakehash11"); + CHECK(send_records_2.size() == 4); // Unchanged + + // Both configs are one the same generation (with a conflict) + REQUIRE(state.config(group.id).current_generation() == 1); + REQUIRE(state_admin_2.config(group.id).current_generation() == 1); + + // Merge the changes from admin2 across to admin1 (the merge function should handle the + // conflict) + send_data = nlohmann::json::parse(send_records_2[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + to_merge.clear(); + to_merge.emplace_back( + Namespace::GroupKeys, + "fakehash11", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupInfo, + "fakehash12", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get()))); + to_merge.emplace_back( + Namespace::GroupMembers, + "fakehash13", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get()))); + + merge_result = state.merge(group.id, to_merge); + REQUIRE(merge_result.size() == 3); + CHECK(merge_result[0] == "fakehash11"); + CHECK(merge_result[1] == "fakehash12"); + CHECK(merge_result[2] == "fakehash13"); + + // Admin1 should have performed a rekey as part of the merge (updating each of the group + // configs) + REQUIRE(send_records.size() == 4); + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/3/params/messages")].size() == 4); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/0"), "") == "fakehash9"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/1"), "") == "fakehash12"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/2"), "") == "fakehash13"); + CHECK(send_data.value(json_ptr("/params/requests/3/params/messages/3"), "") == "fakehash10"); +} + +TEST_CASE("State c API", "[state][state][c]") { + auto ed_sk = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + const ustring admin2_seed = + "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"_hexbytes; + + char err[256]; + state_object* state; + state_object* state2; + auto admin2_sk = sk_from_seed({admin2_seed.data(), admin2_seed.size()}); + REQUIRE(state_init(&state, ed_sk.data(), nullptr, 0, err)); + REQUIRE(state_init(&state2, admin2_sk.data(), nullptr, 0, err)); + std::vector store_records; + std::vector send_records; + std::vector store_records_2; + std::vector send_records_2; + state_set_store_callback(state, c_store_callback, reinterpret_cast(&store_records)); + state_set_send_callback(state, c_send_callback, reinterpret_cast(&send_records)); + state_set_store_callback(state2, c_store_callback, reinterpret_cast(&store_records_2)); + state_set_send_callback(state2, c_send_callback, reinterpret_cast(&send_records_2)); + + // User Profile forwarding + CHECK(state_get_profile_name(state) == nullptr); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_profile_name(mutable_state, "Test Name"); + }, + nullptr); + CHECK(state_get_profile_name(state) == "Test Name"sv); + + CHECK(strlen(state_get_profile_pic(state).url) == 0); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + auto p = user_profile_pic(); + strcpy(p.url, "http://example.org/omg-pic-123.bmp"); // NB: length must be < + // sizeof(p.url)! + + memcpy(p.key, "secret78901234567890123456789012", 32); + state_set_profile_pic(mutable_state, p); + }, + nullptr); + + auto stored_pic = state_get_profile_pic(state); + CHECK(stored_pic.url == "http://example.org/omg-pic-123.bmp"sv); + CHECK(ustring_view{stored_pic.key, 32} == "secret78901234567890123456789012"_bytes); + + CHECK(state_get_profile_blinded_msgreqs(state) == -1); + state_mutate_user( + state, + [](mutable_user_state_object* mutable_state, void* ctx) { + state_set_profile_blinded_msgreqs(mutable_state, 1); + }, + nullptr); + CHECK(state_get_profile_blinded_msgreqs(state) == 1); + + unsigned char* dump1; + size_t dump1len; + state_dump_namespace(state, NAMESPACE_USER_PROFILE, nullptr, &dump1, &dump1len); + state_object* state3; + REQUIRE(state_init(&state3, ed_sk.data(), nullptr, 0, err)); + CHECK(state_get_profile_name(state3) == nullptr); + CHECK(state_load(state3, NAMESPACE_USER_PROFILE, nullptr, dump1, dump1len)); + CHECK(state_get_profile_name(state3) == "Test Name"sv); + + // Creating a group works correctly + auto pic = user_profile_pic(); + strcpy(pic.url, "http://example.com/huge.bmp"); + memcpy(pic.key, "qwerty78901234567890123456789012", 32); + auto members = new state_group_member[3]; + members[0] = state_group_member{ + "05ece06dd8e02fb2f7d9497f956a1996e199953c651f4016a2f79a3b3e38d55628", "Member 0"}; + members[1] = state_group_member{ + "053ac269b71512776b0bd4a1234aaf93e67b4e9068a2c252f3b93a20acb590ae3c", "Member 1"}; + members[2] = state_group_member{ + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e", "Admin 2"}; + + state_create_group( + state, + "TestName", + 8, + "TestDesc", + 8, + pic, + members, + 3, + [](const char* group_id, + unsigned const char* group_sk, + const char* error, + const size_t error_len, + void* ctx) { REQUIRE(error_len == 0); }, + nullptr); + free(members); + + REQUIRE(send_records.size() == 4); + auto send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupInfo)); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "").substr(0, 2) == "03"); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/2/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/ttl"), uint64_t(0)) == 2592000000); + + CHECK_FALSE(unbox(state).config().needs_push()); + CHECK(store_records.size() == 3); // Not stored until we process a success response + auto send_res = send_response({"fakehash2", "fakehash3", "fakehash4"}); + REQUIRE(send_records[3].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[3].callback_context)); + CHECK(store_records.size() == 7); + CHECK(store_records[3].namespace_ == Namespace::UserGroups); + CHECK(store_records[4].namespace_ == Namespace::GroupKeys); + CHECK(store_records[5].namespace_ == Namespace::GroupInfo); + CHECK(store_records[6].namespace_ == Namespace::GroupMembers); + CHECK(unbox(state).config().get_seqno() == 1); + CHECK(unbox(state).config().needs_push()); + + auto gid = send_data.value(json_ptr("/params/requests/0/params/pubkey"), ""); + auto g = ugroups_group_info(); + REQUIRE(state_size_ugroups(state) == 1); + REQUIRE(state_get_ugroups_group(state, &g, gid.c_str(), nullptr)); + CHECK(g.name == "TestName"sv); + CHECK(unbox(state).config(gid).get_seqno() == 1); + CHECK(unbox(state).config(gid).get_seqno() == 1); + CHECK(unbox(state).config(gid).current_generation() == 0); + CHECK(unbox(state).config(gid).admin()); + + // Keys only get loaded when merging so we need to trigger the merge + state_config_message* merge_data = new state_config_message[3]; + session_string_list* accepted; + auto payload0 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/0/params/data")].get())); + auto payload1 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/1/params/data")].get())); + auto payload2 = to_unsigned(oxenc::from_base64( + send_data[json_ptr("/params/requests/2/params/data")].get())); + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash2", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + payload0.data(), + payload0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash3", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + payload1.data(), + payload1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash4", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + payload2.data(), + payload2.size()}; + REQUIRE(state_merge(state, gid.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash2"sv); + CHECK(accepted->value[1] == "fakehash3"sv); + CHECK(accepted->value[2] == "fakehash4"sv); + CHECK(send_records.size() == 5); + free(accepted); + free(merge_data); + + // Check that the supplemental rotation calls everything correctly + members = new state_group_member[1]; + members[0] = state_group_member{ + "05a2b03abdda4df8316f9d7aed5d2d1e483e9af269d0b39191b08321b8495bc118"}; + state_add_group_members( + state, + gid.c_str(), + true, + members, + 1, + [](const char* error, size_t error_len, void* ctx) { REQUIRE(error_len == 0); }, + nullptr); + free(members); + + REQUIRE(send_records.size() == 6); + send_data = nlohmann::json::parse(send_records[5].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupKeys)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/1/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/1/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/1/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/1/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/1/params/ttl"), uint64_t(0)) == 2592000000); + CHECK(send_data.value(json_ptr("/params/requests/2/method"), "") == "delete"); + CHECK(send_data.value(json_ptr("/params/requests/2/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/2/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/2/params/signature"), "").size() == 88); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/2/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/2/params/messages/0"), "") == "fakehash4"); + + send_res = send_response({"fakehash5", "fakehash6"}); + REQUIRE(send_records[5].response_cb( + true, 200, send_res.data(), send_res.size(), send_records[5].callback_context)); + + // Load the group for admin2 + state_approve_group(state2, gid.c_str()); + REQUIRE(send_records_2.size() == 1); + send_data = nlohmann::json::parse(send_records_2[0].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 1); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + send_res = send_response({"fakehash5"}); + REQUIRE(send_records_2[0].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[0].callback_context)); + REQUIRE(send_records_2.size() == 1); // Unchanged + + send_data = nlohmann::json::parse(send_records[3].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 3); + merge_data = new state_config_message[3]; + merge_data[0] = { + NAMESPACE_GROUP_KEYS, + "fakehash2", + send_data[json_ptr("/params/requests/0/params/timestamp")].get(), + payload0.data(), + payload0.size()}; + merge_data[1] = { + NAMESPACE_GROUP_INFO, + "fakehash3", + send_data[json_ptr("/params/requests/1/params/timestamp")].get(), + payload1.data(), + payload1.size()}; + merge_data[2] = { + NAMESPACE_GROUP_MEMBERS, + "fakehash4", + send_data[json_ptr("/params/requests/2/params/timestamp")].get(), + payload2.data(), + payload2.size()}; + REQUIRE(state_merge(state2, gid.c_str(), merge_data, 3, &accepted)); + REQUIRE(accepted->len == 3); + CHECK(accepted->value[0] == "fakehash2"sv); + CHECK(accepted->value[1] == "fakehash3"sv); + CHECK(accepted->value[2] == "fakehash4"sv); + CHECK(send_records_2.size() == 1); + free(accepted); + free(merge_data); + + // Promote to admin + state_load_group_admin_key(state2, gid.c_str(), g.secretkey); + REQUIRE(send_records_2.size() == 3); + + // UserGroups gets the admin key + send_data = nlohmann::json::parse(send_records_2[1].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == + "05c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey_ed25519"), "") == + "3ccd241cffc9b3618044b97d036d8614593d8b017c340f1dee8773385517654b"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::UserGroups)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash5"); + send_res = send_response({"fakehash6"}); + REQUIRE(send_records_2[1].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[1].callback_context)); + + // Member flagged as an admin + send_data = nlohmann::json::parse(send_records_2[2].payload); + REQUIRE(send_data.contains(json_ptr("/params/requests"))); + REQUIRE(send_data[json_ptr("/params/requests")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests")].size() == 2); + CHECK(send_data.value("method", "") == "sequence"); + CHECK(send_data.value(json_ptr("/params/requests/0/method"), "") == "store"); + CHECK(send_data.value(json_ptr("/params/requests/0/params/pubkey"), "") == gid); + CHECK_FALSE(send_data.contains(json_ptr("/params/requests/0/params/pubkey_ed25519"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/namespace"), 0) == + static_cast(Namespace::GroupMembers)); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/data"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/signature"), "").size() == 88); + CHECK(send_data.contains(json_ptr("/params/requests/0/params/timestamp"))); + CHECK(send_data.value(json_ptr("/params/requests/0/params/ttl"), uint64_t(0)) == 2592000000); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].is_array()); + REQUIRE(send_data[json_ptr("/params/requests/1/params/messages")].size() == 1); + CHECK(send_data.value(json_ptr("/params/requests/1/params/messages/0"), "") == "fakehash4"); + send_res = send_response({"fakehash7"}); + REQUIRE(send_records_2[2].response_cb( + true, 200, send_res.data(), send_res.size(), send_records_2[2].callback_context)); + REQUIRE(send_records_2.size() == 3); // Unchanged + REQUIRE(unbox(state2).config(gid).admin()); +} \ No newline at end of file diff --git a/tests/utils.hpp b/tests/utils.hpp index 76b145c1..423f00c2 100644 --- a/tests/utils.hpp +++ b/tests/utils.hpp @@ -10,7 +10,8 @@ #include #include -#include "session/config/base.h" +#include "session/config/namespaces.h" +#include "session/config/namespaces.hpp" using ustring = std::basic_string; using ustring_view = std::basic_string_view; @@ -88,3 +89,52 @@ std::vector> view_vec(const std::vector*>(ctx)->emplace_back(last_store_data{ + static_cast(namespace_), + {pubkey, 66}, + timestamp_ms, + {data, data_len}}); +} + +inline void c_send_callback( + const char* pubkey, + const unsigned char* data, + size_t data_len, + bool (*response_cb)( + bool success, + int16_t status_code, + const unsigned char* res, + size_t reslen, + void* callback_context), + void* app_ctx, + void* callback_context) { + static_cast*>(app_ctx)->emplace_back( + last_send_data{{pubkey, 66}, {data, data_len}, response_cb, app_ctx, callback_context}); +}