diff --git a/include/session/config/user_profile.h b/include/session/config/user_profile.h index 87d2c0c..d3c2cec 100644 --- a/include/session/config/user_profile.h +++ b/include/session/config/user_profile.h @@ -92,7 +92,8 @@ LIBSESSION_EXPORT int user_profile_set_name(config_object* conf, const char* nam /// /// 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). +/// beyond other API calls on this config object). The returned value will be the latest profile pic +/// between when the user last set their profile and when it was last re-uploaded. /// /// Declaration: /// ```cpp @@ -110,7 +111,7 @@ LIBSESSION_EXPORT user_profile_pic user_profile_get_pic(const config_object* con /// API: user_profile/user_profile_set_pic /// -/// Sets a user profile +/// Sets a user profile pic /// /// Declaration: /// ```cpp @@ -128,6 +129,26 @@ LIBSESSION_EXPORT user_profile_pic user_profile_get_pic(const config_object* con /// - `int` -- Returns 0 on success, non-zero on error LIBSESSION_EXPORT int user_profile_set_pic(config_object* conf, user_profile_pic pic); +/// API: user_profile/user_profile_set_reupload_pic +/// +/// Sets a user profile pic when reuploading +/// +/// Declaration: +/// ```cpp +/// INT user_profile_set_reupload_pic( +/// [in] config_object* conf, +/// [in] user_profile_pic pic +/// ); +/// ``` +/// +/// 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_EXPORT int user_profile_set_reupload_pic(config_object* conf, user_profile_pic pic); + /// API: user_profile/user_profile_get_nts_priority /// /// Gets the current note-to-self priority level. Will be negative for hidden, 0 for unpinned, and > @@ -245,6 +266,19 @@ LIBSESSION_EXPORT int user_profile_get_blinded_msgreqs(const config_object* conf /// - `void` -- Returns Nothing LIBSESSION_EXPORT void user_profile_set_blinded_msgreqs(config_object* conf, int enabled); +/// API: user_profile/user_profile_get_profile_updated +/// +/// Returns the timestamp that the user last updated their profile information; or `0` if it's +/// never been updated. This value will return the latest timestamp between when the user last +/// set their profile and when it was last re-uploaded. +/// +/// Inputs: None +/// +/// Outputs: +/// - `int64_t` - timestamp (unix seconds) that the user last updated their public profile +/// information. Will be `0` if it's never been updated. +LIBSESSION_EXPORT int64_t user_profile_get_profile_updated(config_object* conf); + #ifdef __cplusplus } // extern "C" #endif diff --git a/include/session/config/user_profile.hpp b/include/session/config/user_profile.hpp index 6b9dfa8..e2140fd 100644 --- a/include/session/config/user_profile.hpp +++ b/include/session/config/user_profile.hpp @@ -24,6 +24,13 @@ using namespace std::literals; /// M - set to 1 if blinded message request retrieval is enabled, 0 if retrieval is *disabled*, and /// omitted if the setting has not been explicitly set (or has been explicitly cleared for some /// reason). +/// t - The unix timestamp (seconds) that the user last explicitly updated their profile information +/// (automatically updates when changing `name`, `profile_pic` or `set_blinded_msgreqs`). +/// P - user profile url after re-uploading (should take precedence over `p` when `T > t`). +/// Q - user profile decryption key (binary) after re-uploading (should take precedence over `q` +/// when `T > t`). +/// T - The unix timestamp (seconds) that the user last re-uploaded their profile information +/// (automatically updates when calling `set_reupload_profile_pic`). class UserProfile : public ConfigBase { @@ -101,7 +108,8 @@ class UserProfile : public ConfigBase { /// API: user_profile/UserProfile::get_profile_pic /// /// Gets the user's current profile pic URL and decryption key. The returned object will - /// evaluate as false if the URL and/or key are not set. + /// evaluate as false if the URL and/or key are not set. The returned value will be the latest + /// profile pic between when the user last set their profile and when it was last re-uploaded. /// /// Inputs: None /// @@ -111,8 +119,8 @@ class UserProfile : public ConfigBase { /// API: user_profile/UserProfile::set_profile_pic /// - /// Sets the user's current profile pic to a new URL and decryption key. Clears both if either - /// one is empty. + /// Sets the user's current profile pic to a new URL and decryption key. Clears both as well as + /// the reupload values if either one is empty. /// /// Declaration: /// ```cpp @@ -129,6 +137,25 @@ class UserProfile : public ConfigBase { void set_profile_pic(std::string_view url, std::span key); void set_profile_pic(profile_pic pic); + /// API: user_profile/UserProfile::set_reupload_profile_pic + /// + /// Sets the user's profile pic to a new URL and decryption key after reuploading. + /// + /// Declaration: + /// ```cpp + /// void set_reupload_profile_pic(std::string_view url, std::span key); + /// void set_reupload_profile_pic(profile_pic pic); + /// ``` + /// + /// Inputs: + /// - First function: + /// - `url` -- URL pointing to the profile pic + /// - `key` -- Decryption key + /// - Second function: + /// - `pic` -- Profile pic object + void set_reupload_profile_pic(std::string_view url, std::span key); + void set_reupload_profile_pic(profile_pic pic); + /// API: user_profile/UserProfile::get_nts_priority /// /// Gets the Note-to-self conversation priority. Negative means hidden; 0 means unpinned; @@ -199,6 +226,19 @@ class UserProfile : public ConfigBase { /// default). void set_blinded_msgreqs(std::optional enabled); + /// API: user_profile/UserProfile::get_profile_updated + /// + /// Returns the timestamp that the user last updated their profile information; or `0` if it's + /// never been updated. This value will return the latest timestamp between when the user last + /// set their profile and when it was last re-uploaded. + /// + /// Inputs: None + /// + /// Outputs: + /// - `std::chrono::sys_seconds` - timestamp that the user last updated their profile + /// information. Will be `0` if it's never been updated. + std::chrono::sys_seconds get_profile_updated() const; + bool accepts_protobuf() const override { return true; } }; diff --git a/src/config.cpp b/src/config.cpp index ee23291..a8e2cf1 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -679,6 +679,7 @@ ConfigMessage::ConfigMessage( for (const auto& [seqno_hash, ptrs] : replay) { const auto& [data, diff] = ptrs; apply_diff(data_, *diff, *data); + lagged_diffs_.emplace_hint(lagged_diffs_.end(), seqno_hash, *diff); } diff --git a/src/config/user_profile.cpp b/src/config/user_profile.cpp index f14b925..611156b 100644 --- a/src/config/user_profile.cpp +++ b/src/config/user_profile.cpp @@ -28,6 +28,9 @@ void UserProfile::set_name(std::string_view new_name) { if (new_name.size() > contact_info::MAX_NAME_LENGTH) throw std::invalid_argument{"Invalid profile name: exceeds maximum length"}; set_nonempty_str(data["n"], new_name); + + const auto target_timestamp = (data["T"].integer_or(0) > data["t"].integer_or(0) ? "T" : "t"); + data[target_timestamp] = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); } void UserProfile::set_name_truncated(std::string new_name) { set_name(utf8_truncate(std::move(new_name), contact_info::MAX_NAME_LENGTH)); @@ -35,9 +38,14 @@ void UserProfile::set_name_truncated(std::string new_name) { profile_pic UserProfile::get_profile_pic() const { profile_pic pic{}; - if (auto* url = data["p"].string(); url && !url->empty()) + + const bool use_primary_keys = (data["T"].integer_or(0) > data["t"].integer_or(0)); + const auto url_key = (use_primary_keys ? "p" : "P"); + const auto key_key = (use_primary_keys ? "q" : "Q"); + + if (auto* url = data[url_key].string(); url && !url->empty()) pic.url = *url; - if (auto* key = data["q"].string(); key && key->size() == 32) + if (auto* key = data[key_key].string(); key && key->size() == 32) pic.key.assign( reinterpret_cast(key->data()), reinterpret_cast(key->data()) + 32); @@ -46,12 +54,28 @@ profile_pic UserProfile::get_profile_pic() const { void UserProfile::set_profile_pic(std::string_view url, std::span key) { set_pair_if(!url.empty() && key.size() == 32, data["p"], url, data["q"], key); + + // If the profile was removed then we should remove the "reupload" version as well + if (url.empty() || key.size() != 32) { + set_reupload_profile_pic({}); + } + + data["t"] = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); } void UserProfile::set_profile_pic(profile_pic pic) { set_profile_pic(pic.url, pic.key); } +void UserProfile::set_reupload_profile_pic(std::string_view url, std::span key) { + set_pair_if(!url.empty() && key.size() == 32, data["P"], url, data["Q"], key); + data["T"] = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); +} + +void UserProfile::set_reupload_profile_pic(profile_pic pic) { + set_reupload_profile_pic(pic.url, pic.key); +} + void UserProfile::set_nts_priority(int priority) { set_nonzero_int(data["+"], priority); } @@ -75,6 +99,9 @@ void UserProfile::set_blinded_msgreqs(std::optional value) { data["M"].erase(); else data["M"] = static_cast(*value); + + const auto target_timestamp = (data["T"].integer_or(0) > data["t"].integer_or(0) ? "T" : "t"); + data[target_timestamp] = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); } std::optional UserProfile::get_blinded_msgreqs() const { @@ -83,6 +110,15 @@ std::optional UserProfile::get_blinded_msgreqs() const { return std::nullopt; } +std::chrono::sys_seconds UserProfile::get_profile_updated() const { + if (auto* t = data["t"].integer(); t) { + if (auto* T = data["T"].integer(); T && T > t) + return std::chrono::sys_seconds{std::chrono::seconds{*T}}; + return std::chrono::sys_seconds{std::chrono::seconds{*t}}; + } + return std::chrono::sys_seconds{}; +} + extern "C" { using namespace session; @@ -141,6 +177,21 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic static_cast(SESSION_ERR_BAD_VALUE)); } +LIBSESSION_C_API int user_profile_set_reupload_pic(config_object* conf, user_profile_pic pic) { + std::string_view url{pic.url}; + std::span key; + if (!url.empty()) + key = {pic.key, 32}; + + return wrap_exceptions( + conf, + [&] { + unbox(conf)->set_reupload_profile_pic(url, key); + return 0; + }, + static_cast(SESSION_ERR_BAD_VALUE)); +} + LIBSESSION_C_API int user_profile_get_nts_priority(const config_object* conf) { return unbox(conf)->get_nts_priority(); } @@ -170,4 +221,8 @@ LIBSESSION_C_API void user_profile_set_blinded_msgreqs(config_object* conf, int unbox(conf)->set_blinded_msgreqs(std::move(val)); } +LIBSESSION_C_API int64_t user_profile_get_profile_updated(config_object* conf) { + return unbox(conf)->get_profile_updated().time_since_epoch().count(); +} + } // extern "C" \ No newline at end of file