Skip to content

Added 'profile_updated' to contact_info & member to version profile info #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
7 commits merged into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ local static_build(name,
clang(17),
full_llvm(17),
debian_build('Debian stable (i386)', docker_base + 'debian-stable/i386'),
debian_build('Debian 12', docker_base + 'debian-bookworm', extra_setup=debian_backports('bookworm', ['cmake'])),
debian_build('Debian 12', docker_base + 'debian-bookworm'),
debian_build('Ubuntu latest', docker_base + 'ubuntu-rolling'),
debian_build('Ubuntu LTS', docker_base + 'ubuntu-lts'),

Expand Down
1 change: 1 addition & 0 deletions include/session/config/contacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ typedef struct contacts_contact {
char name[101];
char nickname[101];
user_profile_pic profile_pic;
int64_t profile_updated; // unix timestamp (seconds)

bool approved;
bool approved_me;
Expand Down
14 changes: 14 additions & 0 deletions include/session/config/contacts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ namespace session::config {
/// E - Disappearing message timer, in seconds. Omitted when `e` is omitted.
/// j - Unix timestamp (seconds) when the contact was created ("j" to match user_groups
/// equivalent "j"oined field). Omitted if 0.
/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information.

/// Struct containing contact info.
struct contact_info {
Expand All @@ -53,6 +54,8 @@ struct contact_info {
std::string name;
std::string nickname;
profile_pic profile_picture;
std::chrono::sys_seconds profile_updated{}; /// The unix timestamp (seconds) that this
/// profile information was last updated.
bool approved = false;
bool approved_me = false;
bool blocked = false;
Expand Down Expand Up @@ -230,6 +233,17 @@ class Contacts : public ConfigBase {
/// - `profile_pic` -- profile pic of the contact
void set_profile_pic(std::string_view session_id, profile_pic pic);

/// API: contacts/contacts::set_profile_updated
///
/// Alternative to `set()` for setting a single field. (If setting multiple fields at once you
/// should use `set()` instead).
///
/// Inputs:
/// - `session_id` -- hex string of the session id
/// - `profile_updated` -- profile updated unix timestamp (seconds) of the contact. (To convert
/// a raw s/ms/µs integer value, use session::to_sys_seconds).
void set_profile_updated(std::string_view session_id, std::chrono::sys_seconds profile_updated);

/// API: contacts/contacts::set_approved
///
/// Alternative to `set()` for setting a single field. (If setting multiple fields at once you
Expand Down
1 change: 1 addition & 0 deletions include/session/config/groups/members.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ typedef struct config_group_member {
// These two will be 0-length strings when unset:
char name[101];
user_profile_pic profile_pic;
int64_t profile_updated; // unix timestamp (seconds)

bool admin;
int invited; // 0 == unset, STATUS_SENT = invited, STATUS_FAILED = invite failed to send,
Expand Down
8 changes: 8 additions & 0 deletions include/session/config/groups/members.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ using namespace std::literals;
/// resent)
/// - 3 if a member has been marked for promotion but the promotion hasn't been sent yet.
/// - omitted once the promotion is accepted (i.e. once `A` gets set).
/// t - The `profile_updated` unix timestamp (seconds) for this contacts profile information.

constexpr int STATUS_SENT = 1, STATUS_FAILED = 2, STATUS_NOT_SENT = 3;
constexpr int REMOVED_MEMBER = 1, REMOVED_MEMBER_AND_MESSAGES = 2;
Expand Down Expand Up @@ -100,6 +101,13 @@ struct member {
/// member.
profile_pic profile_picture;

/// API: groups/member::profile_updated
///
/// Member variable
///
/// The unix timestamp (seconds) that this profile information was last updated.
std::chrono::sys_seconds profile_updated{};

/// API: groups/member::admin
///
/// Member variable
Expand Down
19 changes: 19 additions & 0 deletions include/session/util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,23 @@ inline int64_t to_epoch_seconds(int64_t timestamp) {
: timestamp;
}

// Takes a timestamp as unix epoch seconds (not ms, µs) and wraps it in a sys_seconds containing it.
inline std::chrono::sys_seconds as_sys_seconds(int64_t timestamp) {
return std::chrono::sys_seconds{std::chrono::seconds{timestamp}};
}

// Helper function to transform a timestamp integer that might be seconds, milliseconds or
// microseconds to typesafe system clock seconds unix timestamp.
inline std::chrono::sys_seconds to_sys_seconds(int64_t timestamp) {
if (timestamp > 9'000'000'000'000)
timestamp /= 1'000'000;
else if (timestamp > 9'000'000'000)
timestamp /= 1'000;
return as_sys_seconds(timestamp);
}

static_assert(std::is_same_v<
std::chrono::seconds,
decltype(std::declval<std::chrono::sys_seconds>().time_since_epoch())>);

} // namespace session
33 changes: 22 additions & 11 deletions src/config/contacts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ LIBSESSION_C_API int contacts_init(
}

void contact_info::load(const dict& info_dict) {
name = maybe_string(info_dict, "n").value_or("");
nickname = maybe_string(info_dict, "N").value_or("");
name = string_or_empty(info_dict, "n");
nickname = string_or_empty(info_dict, "N");

auto url = maybe_string(info_dict, "p");
auto key = maybe_vector(info_dict, "q");
Expand All @@ -83,23 +83,24 @@ void contact_info::load(const dict& info_dict) {
profile_picture.clear();
}

approved = maybe_int(info_dict, "a").value_or(0);
approved_me = maybe_int(info_dict, "A").value_or(0);
blocked = maybe_int(info_dict, "b").value_or(0);
profile_updated = ts_or_epoch(info_dict, "t");
approved = int_or_0(info_dict, "a");
approved_me = int_or_0(info_dict, "A");
blocked = int_or_0(info_dict, "b");

priority = maybe_int(info_dict, "+").value_or(0);
priority = int_or_0(info_dict, "+");

int notify = maybe_int(info_dict, "@").value_or(0);
int notify = int_or_0(info_dict, "@");
if (notify >= 0 && notify <= 3) {
notifications = static_cast<notify_mode>(notify);
if (notifications == notify_mode::mentions_only)
notifications = notify_mode::all;
} else {
notifications = notify_mode::defaulted;
}
mute_until = to_epoch_seconds(maybe_int(info_dict, "!").value_or(0));
mute_until = to_epoch_seconds(int_or_0(info_dict, "!"));

int exp_mode_ = maybe_int(info_dict, "e").value_or(0);
int exp_mode_ = int_or_0(info_dict, "e");
if (exp_mode_ >= static_cast<int>(expiration_mode::none) &&
exp_mode_ <= static_cast<int>(expiration_mode::after_read))
exp_mode = static_cast<expiration_mode>(exp_mode_);
Expand All @@ -109,7 +110,7 @@ void contact_info::load(const dict& info_dict) {
if (exp_mode == expiration_mode::none)
exp_timer = 0s;
else {
int secs = maybe_int(info_dict, "E").value_or(0);
int secs = int_or_0(info_dict, "E");
if (secs <= 0) {
exp_mode = expiration_mode::none;
exp_timer = 0s;
Expand All @@ -118,7 +119,7 @@ void contact_info::load(const dict& info_dict) {
}
}

created = to_epoch_seconds(maybe_int(info_dict, "j").value_or(0));
created = to_epoch_seconds(int_or_0(info_dict, "j"));
}

void contact_info::into(contacts_contact& c) const {
Expand All @@ -131,6 +132,7 @@ void contact_info::into(contacts_contact& c) const {
} else {
copy_c_str(c.profile_pic.url, "");
}
c.profile_updated = profile_updated.time_since_epoch().count();
c.approved = approved;
c.approved_me = approved_me;
c.blocked = blocked;
Expand All @@ -154,6 +156,7 @@ contact_info::contact_info(const contacts_contact& c) : session_id{c.session_id,
profile_picture.url = c.profile_pic.url;
profile_picture.key.assign(c.profile_pic.key, c.profile_pic.key + 32);
}
profile_updated = to_sys_seconds(c.profile_updated);
approved = c.approved;
approved_me = c.approved_me;
blocked = c.blocked;
Expand Down Expand Up @@ -227,6 +230,8 @@ void Contacts::set(const contact_info& contact) {
info["q"],
contact.profile_picture.key);

set_ts(info["t"], contact.profile_updated);

set_flag(info["a"], contact.approved);
set_flag(info["A"], contact.approved_me);
set_flag(info["b"], contact.blocked);
Expand Down Expand Up @@ -279,6 +284,12 @@ void Contacts::set_profile_pic(std::string_view session_id, profile_pic pic) {
c.profile_picture = std::move(pic);
set(c);
}
void Contacts::set_profile_updated(
std::string_view session_id, std::chrono::sys_seconds profile_updated) {
auto c = get_or_construct(session_id);
c.profile_updated = profile_updated;
set(c);
}
void Contacts::set_approved(std::string_view session_id, bool approved) {
auto c = get_or_construct(session_id);
c.approved = approved;
Expand Down
4 changes: 2 additions & 2 deletions src/config/convo_info_volatile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ namespace convo {
}

void base::load(const dict& info_dict) {
last_read = maybe_int(info_dict, "r").value_or(0);
unread = (bool)maybe_int(info_dict, "u").value_or(0);
last_read = int_or_0(info_dict, "r");
unread = (bool)int_or_0(info_dict, "u");
}

} // namespace convo
Expand Down
19 changes: 11 additions & 8 deletions src/config/groups/members.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ void Members::set(const member& mem) {
info["q"],
mem.profile_picture.key);

set_ts(info["t"], mem.profile_updated);
set_flag(info["A"], mem.admin);
set_positive_int(info["P"], mem.promotion_status);
set_positive_int(info["I"], mem.admin ? 0 : mem.invite_status);
Expand All @@ -84,7 +85,7 @@ void Members::set(const member& mem) {
}

void member::load(const dict& info_dict) {
name = maybe_string(info_dict, "n").value_or("");
name = string_or_empty(info_dict, "n");

auto url = maybe_string(info_dict, "p");
auto key = maybe_vector(info_dict, "q");
Expand All @@ -95,13 +96,13 @@ void member::load(const dict& info_dict) {
profile_picture.clear();
}

admin = maybe_int(info_dict, "A").value_or(0);
invite_status = admin ? 0 : maybe_int(info_dict, "I").value_or(0);
promotion_status = maybe_int(info_dict, "P").value_or(0);
removed_status = maybe_int(info_dict, "R").value_or(0);
supplement = invite_status > 0 && !(admin || promotion_status > 0)
? maybe_int(info_dict, "s").value_or(0)
: 0;
profile_updated = ts_or_epoch(info_dict, "t");
admin = int_or_0(info_dict, "A");
invite_status = admin ? 0 : int_or_0(info_dict, "I");
promotion_status = int_or_0(info_dict, "P");
removed_status = int_or_0(info_dict, "R");
supplement =
invite_status > 0 && !(admin || promotion_status > 0) ? int_or_0(info_dict, "s") : 0;
}

/// Load _val from the current iterator position; if it is invalid, skip to the next key until we
Expand Down Expand Up @@ -187,6 +188,7 @@ member::member(const config_group_member& m) : session_id{m.session_id, 66} {
profile_picture.url = m.profile_pic.url;
profile_picture.key.assign(m.profile_pic.key, m.profile_pic.key + 32);
}
profile_updated = to_sys_seconds(m.profile_updated);
admin = m.admin;
invite_status =
(m.invited == STATUS_SENT || m.invited == STATUS_FAILED || m.invited == STATUS_NOT_SENT)
Expand All @@ -211,6 +213,7 @@ void member::into(config_group_member& m) const {
} else {
copy_c_str(m.profile_pic.url, "");
}
m.profile_updated = profile_updated.time_since_epoch().count();
m.admin = admin;
static_assert(groups::STATUS_SENT == ::STATUS_SENT);
static_assert(groups::STATUS_FAILED == ::STATUS_FAILED);
Expand Down
31 changes: 31 additions & 0 deletions src/config/internal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,49 @@ std::optional<int64_t> maybe_int(const session::config::dict& d, const char* key
return std::nullopt;
}

int64_t int_or_0(const session::config::dict& d, const char* key) {
if (auto* i = maybe_scalar<int64_t>(d, key))
return *i;
return 0;
}

std::optional<std::chrono::sys_seconds> maybe_ts(const session::config::dict& d, const char* key) {
std::optional<std::chrono::sys_seconds> result;
if (auto* i = maybe_scalar<int64_t>(d, key))
result.emplace(std::chrono::seconds{*i});
return result;
}

std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key) {
if (auto* i = maybe_scalar<int64_t>(d, key))
return std::chrono::sys_seconds{std::chrono::seconds{*i}};
return std::chrono::sys_seconds{};
}

std::optional<std::string> maybe_string(const session::config::dict& d, const char* key) {
if (auto* s = maybe_scalar<std::string>(d, key))
return *s;
return std::nullopt;
}

std::string string_or_empty(const session::config::dict& d, const char* key) {
if (auto* s = maybe_scalar<std::string>(d, key))
return *s;
return ""s;
}

std::optional<std::string_view> maybe_sv(const session::config::dict& d, const char* key) {
if (auto* s = maybe_scalar<std::string>(d, key))
return *s;
return std::nullopt;
}

std::string_view sv_or_empty(const session::config::dict& d, const char* key) {
if (auto* s = maybe_scalar<std::string>(d, key))
return *s;
return ""sv;
}

std::optional<std::vector<unsigned char>> maybe_vector(
const session::config::dict& d, const char* key) {
std::optional<std::vector<unsigned char>> result;
Expand Down
33 changes: 29 additions & 4 deletions src/config/internal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,38 @@ const config::set* maybe_set(const session::config::dict& d, const char* key);
// Digs into a config `dict` to get out an int64_t; nullopt if not there (or not int)
std::optional<int64_t> maybe_int(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out an int64_t; returns 0 if the value is not there or not an
// int. Equivalent to `maybe_int(d, key).value_or(0)`.
int64_t int_or_0(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out an int64_t containing unix timestamp seconds, returns it
// wrapped in a std::chrono::sys_seconds. Returns nullopt if not there (or not int).
std::optional<std::chrono::sys_seconds> maybe_ts(const session::config::dict& d, const char* key);

// Works like maybe_ts, except that if the value isn't present it returns a default-constructed
// sys_seconds (i.e. unix timestamp 0). Equivalent to `maybe_ts(d,
// key).value_or(std::chrono::sys_seconds{})`.
std::chrono::sys_seconds ts_or_epoch(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out a string; nullopt if not there (or not string)
std::optional<std::string> maybe_string(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out a std::vector<unsigned char>; nullopt if not there (or not
// string)
std::optional<std::vector<unsigned char>> maybe_vector(
const session::config::dict& d, const char* key);
// Digs into a config `dict` to get out a string; ""s if not there (or not string)
std::string string_or_empty(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out a string view; nullopt if not there (or not string). The
// string view is only valid as long as the dict stays unchanged.
std::optional<std::string_view> maybe_sv(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out a string view; ""sv if not there (or not string). The
// string view is only valid as long as the dict stays unchanged.
std::string_view sv_or_empty(const session::config::dict& d, const char* key);

// Digs into a config `dict` to get out a std::vector<unsigned char>; nullopt if not there (or not
// string)
std::optional<std::vector<unsigned char>> maybe_vector(
const session::config::dict& d, const char* key);

/// Sets a value to 1 if true, removes it if false.
void set_flag(ConfigBase::DictFieldProxy&& field, bool val);

Expand All @@ -172,6 +192,11 @@ void set_nonzero_int(ConfigBase::DictFieldProxy&& field, int64_t val);
/// Sets an integer value, if positive; removes it if <= 0.
void set_positive_int(ConfigBase::DictFieldProxy&& field, int64_t val);

/// Sets a unix timestamp as an integer, if positive; removes it if <= 0.
inline void set_ts(ConfigBase::DictFieldProxy&& field, std::chrono::sys_seconds val) {
set_positive_int(std::move(field), val.time_since_epoch().count());
}

/// Sets a pair of values if the given condition is satisfied, clears both values otherwise.
template <typename Condition, typename T1, typename T2>
void set_pair_if(
Expand Down
Loading