Compare commits

...

1 Commits

Author SHA1 Message Date
iequidoo
974e32dd76 fix: Don't decrease member add/remove timestamps if they aren't far away in the future
We shouldn't decrease `add_timestamp` and `remove_timestamp` in the `chats_contacts` table normally,
even if remote changes arrive reordered. This particularly makes sense for ad-hoc groups (see
`chat::update_chat_contacts_table()` in `apply_group_changes()`) and in case if we join an encrypted
group which we were a member of before (see `chat::add_to_chat_contacts_table()` call).

Still, limit already stored timestamps in case local clock was in the future and is set back
now. But our clock may be slow, so limit stored timestamps with a remote timestamp if it's
bigger.

NB: `receive_imf::update_chats_contacts_timestamps()` already only increases timestamps, but it's
used only for handling of the "Chat-Group-Member-Timestamps" header, i.e. for encrypted groups.
2026-06-07 21:45:47 -03:00

View File

@@ -3738,17 +3738,19 @@ pub(crate) async fn update_chat_contacts_table(
id: ChatId, id: ChatId,
contacts: &BTreeSet<ContactId>, contacts: &BTreeSet<ContactId>,
) -> Result<()> { ) -> Result<()> {
// See add_to_chat_contacts_table() for reasoning.
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
// Bump `remove_timestamp` to at least `now` // Bump `remove_timestamp` even for members from `contacts`.
// even for members from `contacts`.
// We add members from `contacts` back below. // We add members from `contacts` back below.
transaction.execute( transaction.execute(
"UPDATE chats_contacts "UPDATE chats_contacts SET
SET remove_timestamp=MAX(add_timestamp+1, ?) add_timestamp=MIN(add_timestamp, ?1),
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
WHERE chat_id=?", WHERE chat_id=?",
(timestamp, id), (limit, timestamp, id),
)?; )?;
if !contacts.is_empty() { if !contacts.is_empty() {
@@ -3760,9 +3762,8 @@ pub(crate) async fn update_chat_contacts_table(
)?; )?;
for contact_id in contacts { for contact_id in contacts {
// We bumped `add_timestamp` for existing rows above, // We bumped `remove_timestamp` for existing rows above,
// so on conflict it is enough to set `add_timestamp = remove_timestamp` // so on conflict it is enough to set `add_timestamp = remove_timestamp`.
// and this guarantees that `add_timestamp` is no less than `timestamp`.
statement.execute((id, contact_id, timestamp))?; statement.execute((id, contact_id, timestamp))?;
} }
} }
@@ -3779,17 +3780,24 @@ pub(crate) async fn add_to_chat_contacts_table(
chat_id: ChatId, chat_id: ChatId,
contact_ids: &[ContactId], contact_ids: &[ContactId],
) -> Result<()> { ) -> Result<()> {
// Our clock may be slow, so limit stored timestamps with `timestamp` if it's bigger. This way
// we only cap remote timestamps if, in addition, remote changes arrive reordered or we do local
// changes. Also allow some tolerance, moreover, previous removals might lend time from the
// future.
let limit = cmp::max(time().saturating_add(TIMESTAMP_SENT_TOLERANCE), timestamp);
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
let mut add_statement = transaction.prepare( let mut add_statement = transaction.prepare(
"INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3) "INSERT INTO chats_contacts (chat_id, contact_id, add_timestamp) VALUES(?1, ?2, ?3)
ON CONFLICT (chat_id, contact_id) ON CONFLICT (chat_id, contact_id)
DO UPDATE SET add_timestamp=MAX(remove_timestamp, ?3)", DO UPDATE SET
remove_timestamp=MIN(remove_timestamp, ?4),
add_timestamp=MIN(MAX(add_timestamp,remove_timestamp,?3), ?4)",
)?; )?;
for contact_id in contact_ids { for contact_id in contact_ids {
add_statement.execute((chat_id, contact_id, timestamp))?; add_statement.execute((chat_id, contact_id, timestamp, limit))?;
} }
Ok(()) Ok(())
}) })
@@ -3808,13 +3816,16 @@ pub(crate) async fn remove_from_chat_contacts_table(
contact_id: ContactId, contact_id: ContactId,
) -> Result<bool> { ) -> Result<bool> {
let now = time(); let now = time();
// See add_to_chat_contacts_table() for reasoning.
let limit = now.saturating_add(TIMESTAMP_SENT_TOLERANCE);
let is_past_member = context let is_past_member = context
.sql .sql
.execute( .execute(
"UPDATE chats_contacts "UPDATE chats_contacts SET
SET remove_timestamp=MAX(add_timestamp+1, ?) add_timestamp=MIN(add_timestamp, ?1),
remove_timestamp=MAX(MIN(remove_timestamp,?1), MIN(add_timestamp,?1)+1, ?)
WHERE chat_id=? AND contact_id=?", WHERE chat_id=? AND contact_id=?",
(now, chat_id, contact_id), (limit, now, chat_id, contact_id),
) )
.await? .await?
> 0; > 0;