Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/session/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class ConfigMessage {
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
std::function<void(dict&, oxenc::bt_dict&, const dict&)> custom_conflict_resolver =
nullptr,
std::function<void(size_t, const config_error&)> error_handler = nullptr);

/// Returns a read-only reference to the contained data. (To get a mutable config object use
Expand Down Expand Up @@ -301,6 +303,8 @@ class MutableConfigMessage : public ConfigMessage {
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
std::function<void(dict&, oxenc::bt_dict&, const dict&)> custom_conflict_resolver =
nullptr,
std::function<void(size_t, const config_error&)> error_handler = nullptr);

/// Wrapper around the above that takes a single string view to load a single message, doesn't
Expand Down
14 changes: 14 additions & 0 deletions include/session/config/base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,20 @@ class ConfigBase : public ConfigSig {
std::unordered_set<std::string> _merge(
std::span<const std::pair<std::string, std::span<const unsigned char>>> configs);

/// API: base/ConfigBase::resolve_conflicts
///
/// Subclasses can override this to add custom logic in order to resolve conflicts when merging
/// multiple configs that have the same seq_no. This function will remove any entries from
/// `diff` which have been handled by the custom resolution logic.
///
/// Inputs:
/// - `data` -- The config data to be updated to the resolved state.
/// - `diff` -- The diffs from the conflicting config update.
/// - `source` -- The config data that the diffs conflicted with.
///
/// Outputs: None
virtual void resolve_conflicts(dict& data, oxenc::bt_dict& diff, const dict& source) {};

/// API: base/ConfigBase::extra_data
///
/// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
Expand Down
50 changes: 50 additions & 0 deletions include/session/config/user_profile.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,31 @@ 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_profile_pic_content_version
///
/// Returns the version of the profile picture content; or `0` if it's never been set.
///
/// Inputs: None
///
/// Outputs:
/// - `uint32_t` - version of the profile picture content. Will be `0` if it's never been set.
LIBSESSION_EXPORT uint32_t user_profile_get_profile_pic_content_version(const config_object* conf);

/// API: user_profile/user_profile_profile_pic_content_version
///
/// Sets the version for the profile picture content. This should be updated when a user sets a
/// new profile picture (or removes the current one), but now when re-uploading the current
/// profile picture.
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
/// - `version` -- [in] version for the profile picture content.
///
/// Outputs:
/// - `int` -- Returns 0 on success, non-zero on error
LIBSESSION_EXPORT int user_profile_set_profile_pic_content_version(
config_object* conf, uint32_t version);

/// 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 >
Expand Down Expand Up @@ -245,6 +270,31 @@ 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 public profile information; or `0` if
/// it's never been updated.
///
/// 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);

/// API: user_profile/user_profile_set_profile_updated
///
/// Sets the timestamp that the user last updated their public profile information (should be
/// updated by the clients when modifying public profile information via a user action, eg:
/// `name`, `profile_pic`, `set_blinded_msgreqs`) but not when an "automated" change occurs (eg.
/// re-uploading the display picture due to expiration).
///
/// Inputs:
/// - `conf` -- [in] Pointer to the config object
/// - `updated` -- [in] timestamp (unix seconds) that the user last updated their public profile
/// information.
LIBSESSION_EXPORT void user_profile_set_profile_updated(config_object* conf, int64_t updated);

#ifdef __cplusplus
} // extern "C"
#endif
63 changes: 63 additions & 0 deletions include/session/config/user_profile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ using namespace std::literals;
/// n - user profile name
/// p - user profile url
/// q - user profile decryption key (binary)
/// V - The version of the content of the profile picture, should be updated when the user uploads a
/// new profile picture (but not when re-uploading the current one).
/// + - the priority value for the "Note to Self" pseudo-conversation (higher = higher in the
/// conversation list). Omitted when 0. -1 means hidden.
/// e - the expiry timer (in seconds) for the "Note to Self" pseudo-conversation. Omitted when 0.
/// 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
/// (should be updated when changing `name`, `profile_pic` or `set_blinded_msgreqs`).

class UserProfile : public ConfigBase {

Expand Down Expand Up @@ -71,6 +75,22 @@ class UserProfile : public ConfigBase {
/// - `const char*` - Will return "UserProfile"
const char* encryption_domain() const override { return "UserProfile"; }

/// API: user_profile/UserProfile::resolve_conflicts
///
/// The UserProfile config stores a timestamp indicating when the user explicitly updated their
/// profile information but there are other situations where the profile information can be
/// "automatically" updated by the clients (eg. re-uploading a display picture). If these
/// actions happen on two devices at the same time it can result in a conflict, in which case we
/// want the "explicit" update to to win the conflict resolution.
///
/// Inputs:
/// - `data` -- The config data to be updated to the resolved state.
/// - `diff` -- The diffs from the conflicting config update.
/// - `source` -- The config data that the diffs conflicted with.
///
/// Outputs: None
void resolve_conflicts(dict& data, oxenc::bt_dict& diff, const dict& source) override;

/// API: user_profile/UserProfile::get_name
///
/// Returns the user profile name, or std::nullopt if there is no profile name set.
Expand Down Expand Up @@ -129,6 +149,26 @@ class UserProfile : public ConfigBase {
void set_profile_pic(std::string_view url, std::span<const unsigned char> key);
void set_profile_pic(profile_pic pic);

/// API: user_profile/UserProfile::profile_pic_content_version
///
/// Returns the version of the profile picture content; or `0` if it's never been set.
///
/// Inputs: None
///
/// Outputs:
/// - `uint32_t` - version of the profile picture content. Will be `0` if it's never been set.
uint32_t get_profile_pic_content_version() const;

/// API: user_profile/UserProfile::profile_pic_content_version
///
/// Sets the version for the profile picture content. This should be updated when a user sets a
/// new profile picture (or removes the current one), but now when re-uploading the current
/// profile picture.
///
/// Inputs:
/// - `version` -- version for the profile picture content.
void set_profile_pic_content_version(uint32_t version);

/// API: user_profile/UserProfile::get_nts_priority
///
/// Gets the Note-to-self conversation priority. Negative means hidden; 0 means unpinned;
Expand Down Expand Up @@ -199,6 +239,29 @@ class UserProfile : public ConfigBase {
/// default).
void set_blinded_msgreqs(std::optional<bool> enabled);

/// API: user_profile/UserProfile::get_profile_updated
///
/// returns the timestamp that the user last updated their public profile information; or `0` if
/// it's never been updated.
///
/// Inputs: None
///
/// Outputs:
/// - `std::chrono::sys_seconds` - timestamp that the user last updated their public profile
/// information. Will be `0` if it's never been updated.
std::chrono::sys_seconds get_profile_updated() const;

/// API: user_profile/UserProfile::set_profile_updated
///
/// Sets the timestamp that the user last updated their public profile information (should be
/// updated by the clients when modifying public profile information via a user action, eg:
/// `name`, `profile_pic`, `set_blinded_msgreqs`) but not when an "automated" change occurs (eg.
/// re-uploading the display picture due to expiration).
///
/// Inputs:
/// - `updated` -- timestamp that the user last updated their public profile information.
void set_profile_updated(std::chrono::sys_seconds updated);

bool accepts_protobuf() const override { return true; }
};

Expand Down
12 changes: 11 additions & 1 deletion src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ ConfigMessage::ConfigMessage(
verify_callable verifier_,
sign_callable signer_,
int lag,
std::function<void(dict&, oxenc::bt_dict&, const dict&)> custom_conflict_resolver,
std::function<void(size_t, const config_error&)> error_handler) :
verifier{std::move(verifier_)}, signer{std::move(signer_)}, lag{lag} {

Expand Down Expand Up @@ -678,7 +679,13 @@ ConfigMessage::ConfigMessage(
// ones
for (const auto& [seqno_hash, ptrs] : replay) {
const auto& [data, diff] = ptrs;
apply_diff(data_, *diff, *data);
auto mutable_diff = *diff;

if (custom_conflict_resolver)
custom_conflict_resolver(data_, mutable_diff, *data);

apply_diff(data_, mutable_diff, *data);

lagged_diffs_.emplace_hint(lagged_diffs_.end(), seqno_hash, *diff);
}

Expand All @@ -694,12 +701,14 @@ MutableConfigMessage::MutableConfigMessage(
verify_callable verifier,
sign_callable signer,
int lag,
std::function<void(dict&, oxenc::bt_dict&, const dict&)> custom_conflict_resolver,
std::function<void(size_t, const config_error&)> error_handler) :
ConfigMessage{
serialized_confs,
std::move(verifier),
std::move(signer),
lag,
std::move(custom_conflict_resolver),
std::move(error_handler)} {
if (!merged())
increment_impl();
Expand All @@ -715,6 +724,7 @@ MutableConfigMessage::MutableConfigMessage(
std::move(verifier),
std::move(signer),
lag,
[](dict&, const oxenc::bt_dict&, const dict&) { return false; },
[](size_t, const config_error& e) { throw e; }} {}

const oxenc::bt_dict& ConfigMessage::diff() {
Expand Down
3 changes: 3 additions & 0 deletions src/config/base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ std::unordered_set<std::string> ConfigBase::_merge(
_config->verifier,
_config->signer,
config_lags(),
[&](dict& data, oxenc::bt_dict& diff, const dict& source) {
resolve_conflicts(data, diff, source);
},
[&](size_t i, const config_error& e) {
log::warning(cat, "{}", e.what());
assert(i > 0); // i == 0 means we can't deserialize our own serialization
Expand Down
83 changes: 83 additions & 0 deletions src/config/user_profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ void UserProfile::set_profile_pic(profile_pic pic) {
set_profile_pic(pic.url, pic.key);
}

uint32_t UserProfile::get_profile_pic_content_version() const {
return data["V"].integer_or(0);
}

void UserProfile::set_profile_pic_content_version(uint32_t version) {
set_nonzero_int(data["V"], version);
}

void UserProfile::set_nts_priority(int priority) {
set_nonzero_int(data["+"], priority);
}
Expand Down Expand Up @@ -83,6 +91,58 @@ std::optional<bool> UserProfile::get_blinded_msgreqs() const {
return std::nullopt;
}

std::chrono::sys_seconds UserProfile::get_profile_updated() const {
if (auto* t = data["t"].integer(); t)
return std::chrono::sys_seconds{std::chrono::seconds{*t}};
return std::chrono::sys_seconds{};
}

void UserProfile::set_profile_updated(std::chrono::sys_seconds updated) {
if (updated.time_since_epoch().count() == 0)
data["t"].erase();
else
data["t"] = static_cast<int>(updated.time_since_epoch().count());
}

void UserProfile::resolve_conflicts(dict& data, oxenc::bt_dict& diff, const dict& source) {
// The UserProfile config stores a timestamp indicating when the user explicitly updated their
// profile information but there are other situations where the profile information can be
// "automatically" updated by the clients (eg. re-uploading a display picture). This hook
// pre-processes a conflict between these public profile values and removes any keys from the
// diff that should be ignored.
static const std::set<std::string> relevant_keys = {"n", "p", "q", "M", "t", "V"};

// No need to do anything if none of the relevant keys were modified
bool has_public_keys = false;
for (const auto& [key, _] : diff) {
if (relevant_keys.count(key)) {
has_public_keys = true;
break;
}
}

if (!has_public_keys)
return;

// A higher content version should win, in the case that they both match then we should only
// keep the local state if it has a higher timestamp
const auto local_content_version = int_or_0(data, "V");
const auto local_timestamp = ts_or_epoch(data, "t");
const auto incoming_full_content_version = int_or_0(source, "V");
const auto incoming_full_timestamp = ts_or_epoch(source, "t");
auto local_state_wins =
((local_content_version > incoming_full_content_version) ||
(local_content_version == incoming_full_content_version &&
local_timestamp > incoming_full_timestamp));

// If the local state wins then we should remove the `relevant_keys` from the diff (ie. keep the
// local state), otherwise the standard `apply_diff` logic should result in the incoming values
// overriding the local ones
if (local_state_wins)
for (const auto& key : relevant_keys)
diff.erase(key);
};

extern "C" {

using namespace session;
Expand Down Expand Up @@ -141,6 +201,21 @@ LIBSESSION_C_API int user_profile_set_pic(config_object* conf, user_profile_pic
static_cast<int>(SESSION_ERR_BAD_VALUE));
}

LIBSESSION_C_API uint32_t user_profile_get_profile_pic_content_version(const config_object* conf) {
return unbox<UserProfile>(conf)->get_profile_pic_content_version();
}

LIBSESSION_C_API int user_profile_set_profile_pic_content_version(
config_object* conf, uint32_t version) {
return wrap_exceptions(
conf,
[&] {
unbox<UserProfile>(conf)->set_profile_pic_content_version(version);
return 0;
},
static_cast<int>(SESSION_ERR_BAD_VALUE));
}

LIBSESSION_C_API int user_profile_get_nts_priority(const config_object* conf) {
return unbox<UserProfile>(conf)->get_nts_priority();
}
Expand Down Expand Up @@ -170,4 +245,12 @@ LIBSESSION_C_API void user_profile_set_blinded_msgreqs(config_object* conf, int
unbox<UserProfile>(conf)->set_blinded_msgreqs(std::move(val));
}

LIBSESSION_C_API int64_t user_profile_get_profile_updated(config_object* conf) {
return unbox<UserProfile>(conf)->get_profile_updated().time_since_epoch().count();
}

LIBSESSION_C_API void user_profile_set_profile_updated(config_object* conf, int64_t updated) {
unbox<UserProfile>(conf)->set_profile_updated(to_sys_seconds(updated));
}

} // extern "C"
1 change: 1 addition & 0 deletions tests/test_configdata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ TEST_CASE("config message signature", "[config][signing]") {
verifier,
nullptr,
ConfigMessage::DEFAULT_DIFF_LAGS,
[](config::dict&, oxenc::bt_dict&, const config::dict&) {},
[](size_t, const auto& exc) { throw exc; }),
config::config_error,
Message("Config signature failed verification"));
Expand Down