Skip to content
Closed
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
22 changes: 13 additions & 9 deletions include/session/config/convo_info_volatile.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ extern "C" {
typedef struct convo_info_volatile_1to1 {
char session_id[67]; // in hex; 66 hex chars + null terminator.

int64_t last_read; // milliseconds since unix epoch
bool unread; // true if the conversation is explicitly marked unread
int64_t last_read; // milliseconds since unix epoch
int64_t last_active; // ms since unix epoch
bool unread; // true if the conversation is explicitly marked unread
} convo_info_volatile_1to1;

typedef struct convo_info_volatile_community {
Expand All @@ -20,22 +21,25 @@ typedef struct convo_info_volatile_community {
char room[65]; // null-terminated (max length 64), normalized (always lower-case)
unsigned char pubkey[32]; // 32 bytes (not terminated, can contain nulls)

int64_t last_read; // ms since unix epoch
bool unread; // true if marked unread
int64_t last_read; // ms since unix epoch
int64_t last_active; // ms since unix epoch
bool unread; // true if marked unread
} convo_info_volatile_community;

typedef struct convo_info_volatile_group {
char group_id[67]; // in hex; 66 hex chars + null terminator. Begins with "03".
int64_t last_read; // ms since unix epoch
bool unread; // true if marked unread
char group_id[67]; // in hex; 66 hex chars + null terminator. Begins with "03".
int64_t last_read; // ms since unix epoch
int64_t last_active; // ms since unix epoch
bool unread; // true if marked unread
} convo_info_volatile_group;

typedef struct convo_info_volatile_legacy_group {
char group_id[67]; // in hex; 66 hex chars + null terminator. Looks just like a Session ID,
// though isn't really one.

int64_t last_read; // ms since unix epoch
bool unread; // true if marked unread
int64_t last_read; // ms since unix epoch
int64_t last_active; // ms since unix epoch
bool unread; // true if marked unread
} convo_info_volatile_legacy_group;

/// API: convo_info_volatile/convo_info_volatile_init
Expand Down
48 changes: 9 additions & 39 deletions include/session/config/convo_info_volatile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class val_loader;
/// Values are dicts with keys:
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
/// included, but will be 0 if no messages are read.
/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always
/// included, but will be 0 for empty conversations.
Copy link
Member

@jagerman jagerman Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make just one of these two always included and omit the other when 0. (The reason to always include something is that we have to ensure there is always one value, because empty dicts get pruned from the serialized config).

/// u - will be present and set to 1 if this conversation is specifically marked unread.
///
/// o - community conversations. This is a nested dict where the outer keys are the BASE_URL of the
Expand All @@ -41,25 +43,32 @@ class val_loader;
/// containing keys:
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
/// included, but will be 0 if no messages are read.
/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always
/// included, but will be 0 for empty conversations.
/// u - will be present and set to 1 if this conversation is specifically marked unread.
///
/// g - group conversations (aka new, non-legacy closed groups). The key is the group identifier
/// (beginning with 03). Values are dicts with keys:
/// r - the unix timestamp (in integer milliseconds) of the last-read message. Always
/// included, but will be 0 if no messages are read.
/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always
/// included, but will be 0 for empty conversations.
/// u - will be present and set to 1 if this conversation is specifically marked unread.
///
/// C - legacy group conversations (aka closed groups). The key is the group identifier (which
/// looks indistinguishable from a Session ID, but isn't really a proper Session ID). Values
/// are dicts with keys:
/// r - the unix timestamp (integer milliseconds) of the last-read message. Always included,
/// but will be 0 if no messages are read.
/// l - the unix timestamp (in integer milliseconds) the conversation was last active. Always
/// included, but will be 0 for empty conversations.
/// u - will be present and set to 1 if this conversation is specifically marked unread.

namespace convo {

struct base {
int64_t last_read = 0;
int64_t last_active = 0;
bool unread = false;

protected:
Expand Down Expand Up @@ -195,45 +204,6 @@ class ConvoInfoVolatile : public ConfigBase {
/// - `const char*` - Will return "ConvoInfoVolatile"
const char* encryption_domain() const override { return "ConvoInfoVolatile"; }

/// Our pruning ages. We ignore added conversations that are more than PRUNE_LOW before now,
/// and we actively remove (when doing a new push) any conversations that are more than
/// PRUNE_HIGH before now. Clients can mostly ignore these and just add all conversations; the
/// class just transparently ignores (or removes) pruned values.
static constexpr auto PRUNE_LOW = 30 * 24h;
static constexpr auto PRUNE_HIGH = 45 * 24h;

/// API: convo_info_volatile/ConvoInfoVolatile::prune_stale
///
/// Prunes any "stale" conversations: that is, ones with a last read more than `prune` ago that
/// are not specifically "marked as unread" by the client.
///
/// This method is called automatically by `push()` and does not typically need to be invoked
/// directly.
///
/// Inputs:
/// - `prune` the "too old" time; any conversations with a last_read time more than this
/// duration ago will be removed (unless they have the explicit `unread` flag set). If
/// omitted, defaults to the PRUNE_HIGH constant (45 days).
///
/// Outputs:
/// - returns nothing.
void prune_stale(std::chrono::milliseconds prune = PRUNE_HIGH);

/// API: convo_info_volatile/ConvoInfoVolatile::push
///
/// Overrides push() to prune stale last-read values before we do the push.
///
/// Inputs: None
///
/// Outputs:
/// - `std::tuple<seqno_t, std::vector<unsigned char>, std::vector<std::string>>` - Returns a
/// tuple containing
/// - `seqno_t` -- sequence number
/// - `std::vector<std::vector<unsigned char>>` -- data message(s) to push to the server
/// - `std::vector<std::string>` -- list of known message hashes
std::tuple<seqno_t, std::vector<std::vector<unsigned char>>, std::vector<std::string>> push()
override;

/// API: convo_info_volatile/ConvoInfoVolatile::get_1to1
///
/// Looks up and returns a contact by session ID (hex). Returns nullopt if the session ID was
Expand Down
61 changes: 10 additions & 51 deletions src/config/convo_info_volatile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,18 @@ namespace convo {
check_session_id(session_id);
}
one_to_one::one_to_one(const convo_info_volatile_1to1& c) :
base{c.last_read, c.unread}, session_id{c.session_id, 66} {}
base{c.last_read, c.last_active, c.unread}, session_id{c.session_id, 66} {}

void one_to_one::into(convo_info_volatile_1to1& c) const {
std::memcpy(c.session_id, session_id.data(), 67);
c.last_read = last_read;
c.last_active = last_active;
c.unread = unread;
}

community::community(const convo_info_volatile_community& c) :
config::community{c.base_url, c.room, std::span<const unsigned char>{c.pubkey, 32}},
base{c.last_read, c.unread} {}
base{c.last_read, c.last_active, c.unread} {}

void community::into(convo_info_volatile_community& c) const {
static_assert(sizeof(c.base_url) == BASE_URL_MAX_LENGTH + 1);
Expand All @@ -49,6 +50,7 @@ namespace convo {
copy_c_str(c.room, room_norm());
std::memcpy(c.pubkey, pubkey().data(), 32);
c.last_read = last_read;
c.last_active = last_active;
c.unread = unread;
}

Expand All @@ -64,6 +66,7 @@ namespace convo {
void group::into(convo_info_volatile_group& c) const {
std::memcpy(c.group_id, id.c_str(), 67);
c.last_read = last_read;
c.last_active = last_active;
c.unread = unread;
}

Expand All @@ -74,16 +77,18 @@ namespace convo {
check_session_id(id);
}
legacy_group::legacy_group(const convo_info_volatile_legacy_group& c) :
base{c.last_read, c.unread}, id{c.group_id, 66} {}
base{c.last_read, c.last_active, c.unread}, id{c.group_id, 66} {}

void legacy_group::into(convo_info_volatile_legacy_group& c) const {
std::memcpy(c.group_id, id.data(), 67);
c.last_read = last_read;
c.last_active = last_active;
c.unread = unread;
}

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

Expand Down Expand Up @@ -219,57 +224,11 @@ void ConvoInfoVolatile::set(const convo::one_to_one& c) {
}

void ConvoInfoVolatile::set_base(const convo::base& c, DictFieldProxy& info) {
auto r = info["r"];

// If we're making the last_read value *older* for some reason then ignore the prune cutoff
// (because we might be intentionally resetting the value after a deletion, for instance).
if (auto* val = r.integer(); val && c.last_read < *val)
r = c.last_read;
else {
std::chrono::system_clock::time_point last_read{std::chrono::milliseconds{c.last_read}};
if (last_read > std::chrono::system_clock::now() - PRUNE_LOW)
info["r"] = c.last_read;
}

set_nonzero_int(info["r"], c.last_read);
set_nonzero_int(info["l"], c.last_active);
set_flag(info["u"], c.unread);
}

void ConvoInfoVolatile::prune_stale(std::chrono::milliseconds prune) {
const int64_t cutoff = std::chrono::duration_cast<std::chrono::milliseconds>(
(std::chrono::system_clock::now() - prune).time_since_epoch())
.count();

std::vector<std::string> stale;
for (auto it = begin_1to1(); it != end(); ++it)
if (!it->unread && it->last_read < cutoff)
stale.push_back(it->session_id);
for (const auto& sid : stale)
erase_1to1(sid);

stale.clear();
for (auto it = begin_legacy_groups(); it != end(); ++it)
if (!it->unread && it->last_read < cutoff)
stale.push_back(it->id);
for (const auto& id : stale)
erase_legacy_group(id);

std::vector<std::pair<std::string, std::string>> stale_comms;
for (auto it = begin_communities(); it != end(); ++it)
if (!it->unread && it->last_read < cutoff)
stale_comms.emplace_back(it->base_url(), it->room());
for (const auto& [base, room] : stale_comms)
erase_community(base, room);
}

std::tuple<seqno_t, std::vector<std::vector<unsigned char>>, std::vector<std::string>>
ConvoInfoVolatile::push() {
// Prune off any conversations with last_read timestamps more than PRUNE_HIGH ago (unless they
// also have a `unread` flag set, in which case we keep them indefinitely).
prune_stale();

return ConfigBase::push();
}

void ConvoInfoVolatile::set(const convo::community& c) {
auto info = community_field(c);
data["o"][c.base_url()]["#"] = c.pubkey();
Expand Down
Loading