Implement message expiration timer version

Fixes #1605
This commit is contained in:
AsamK 2024-10-26 13:08:21 +02:00
parent 5171107a29
commit 5a4f4ba6db
11 changed files with 91 additions and 31 deletions

View file

@ -11,6 +11,7 @@ public record Contact(
String note, String note,
String color, String color,
int messageExpirationTime, int messageExpirationTime,
int messageExpirationTimeVersion,
long muteUntil, long muteUntil,
boolean hideStory, boolean hideStory,
boolean isBlocked, boolean isBlocked,
@ -29,6 +30,7 @@ public record Contact(
builder.note, builder.note,
builder.color, builder.color,
builder.messageExpirationTime, builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil, builder.muteUntil,
builder.hideStory, builder.hideStory,
builder.isBlocked, builder.isBlocked,
@ -84,6 +86,7 @@ public record Contact(
private String note; private String note;
private String color; private String color;
private int messageExpirationTime; private int messageExpirationTime;
private int messageExpirationTimeVersion = 1;
private long muteUntil; private long muteUntil;
private boolean hideStory; private boolean hideStory;
private boolean isBlocked; private boolean isBlocked;
@ -139,6 +142,11 @@ public record Contact(
return this; return this;
} }
public Builder withMessageExpirationTimeVersion(final int val) {
messageExpirationTimeVersion = val;
return this;
}
public Builder withMuteUntil(final long val) { public Builder withMuteUntil(final long val) {
muteUntil = val; muteUntil = val;
return this; return this;

View file

@ -29,8 +29,7 @@ public class ServiceConfig {
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var deleteSync = !isPrimaryDevice; final var deleteSync = !isPrimaryDevice;
final var versionedExpirationTimer = !isPrimaryDevice; return new AccountAttributes.Capabilities(true, deleteSync, true);
return new AccountAttributes.Capabilities(true, deleteSync, versionedExpirationTimer);
} }
public static ServiceEnvironmentConfig getServiceEnvironmentConfig( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(

View file

@ -36,8 +36,34 @@ public class ContactHelper {
return; return;
} }
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
final var version = contact == null
? 1
: contact.messageExpirationTimeVersion() == Integer.MAX_VALUE
? Integer.MAX_VALUE
: contact.messageExpirationTimeVersion() + 1;
account.getContactStore() account.getContactStore()
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); .storeContact(recipientId,
builder.withMessageExpirationTime(messageExpirationTimer)
.withMessageExpirationTimeVersion(version)
.build());
}
public void setExpirationTimer(
RecipientId recipientId, int messageExpirationTimer, int messageExpirationTimerVersion
) {
var contact = account.getContactStore().getContact(recipientId);
if (contact != null && (
contact.messageExpirationTime() == messageExpirationTimer
|| contact.messageExpirationTimeVersion() >= messageExpirationTimerVersion
)) {
return;
}
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
account.getContactStore()
.storeContact(recipientId,
builder.withMessageExpirationTime(messageExpirationTimer)
.withMessageExpirationTimeVersion(messageExpirationTimerVersion)
.build());
} }
public void setContactBlocked(RecipientId recipientId, boolean blocked) { public void setContactBlocked(RecipientId recipientId, boolean blocked) {

View file

@ -802,7 +802,9 @@ public final class IncomingMessageHandler {
} }
} else if (conversationPartnerAddress != null) { } else if (conversationPartnerAddress != null) {
context.getContactHelper() context.getContactHelper()
.setExpirationTimer(conversationPartnerAddress.recipientId(), message.getExpiresInSeconds()); .setExpirationTimer(conversationPartnerAddress.recipientId(),
message.getExpiresInSeconds(),
message.getExpireTimerVersion());
} }
} }
if (!ignoreAttachments) { if (!ignoreAttachments) {

View file

@ -86,8 +86,8 @@ public class SendHelper {
account.getContactStore().storeContact(recipientId, contact); account.getContactStore().storeContact(recipientId, contact);
} }
final var expirationTime = contact.messageExpirationTime(); messageBuilder.withExpiration(contact.messageExpirationTime());
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpireTimerVersion(contact.messageExpirationTimeVersion());
if (!contact.isBlocked()) { if (!contact.isBlocked()) {
final var profileKey = account.getProfileKey().serialize(); final var profileKey = account.getProfileKey().serialize();
@ -187,8 +187,8 @@ public class SendHelper {
) { ) {
final var recipientId = account.getSelfRecipientId(); final var recipientId = account.getSelfRecipientId();
final var contact = account.getContactStore().getContact(recipientId); final var contact = account.getContactStore().getContact(recipientId);
final var expirationTime = contact != null ? contact.messageExpirationTime() : 0; messageBuilder.withExpiration(contact != null ? contact.messageExpirationTime() : 0);
messageBuilder.withExpiration(expirationTime); messageBuilder.withExpireTimerVersion(contact != null ? contact.messageExpirationTimeVersion() : 1);
var message = messageBuilder.build(); var message = messageBuilder.build();
return sendSelfMessage(message, editTargetTimestamp); return sendSelfMessage(message, editTargetTimestamp);

View file

@ -239,7 +239,7 @@ public class SyncHelper {
Optional.ofNullable(verifiedMessage), Optional.ofNullable(verifiedMessage),
Optional.ofNullable(profileKey), Optional.ofNullable(profileKey),
Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()), Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
Optional.empty(), Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
Optional.empty(), Optional.empty(),
contact != null && contact.isArchived()); contact != null && contact.isArchived());
} }
@ -392,7 +392,18 @@ public class SyncHelper {
TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
} }
if (c.getExpirationTimer().isPresent()) { if (c.getExpirationTimer().isPresent()) {
builder.withMessageExpirationTime(c.getExpirationTimer().get()); if (c.getExpirationTimerVersion().isPresent() && (
contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
)) {
builder.withMessageExpirationTime(c.getExpirationTimer().get());
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
} else {
logger.debug(
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}",
recipientId,
c.getExpirationTimerVersion(),
contact == null ? 1 : contact.messageExpirationTimeVersion());
}
} }
builder.withIsArchived(c.isArchived()); builder.withIsArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build()); account.getContactStore().storeContact(recipientId, builder.build());

View file

@ -33,7 +33,7 @@ import java.util.UUID;
public class AccountDatabase extends Database { public class AccountDatabase extends Database {
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class); private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
private static final long DATABASE_VERSION = 26; private static final long DATABASE_VERSION = 27;
private AccountDatabase(final HikariDataSource dataSource) { private AccountDatabase(final HikariDataSource dataSource) {
super(logger, DATABASE_VERSION, dataSource); super(logger, DATABASE_VERSION, dataSource);
@ -600,6 +600,14 @@ public class AccountDatabase extends Database {
"""); """);
} }
} }
if (oldVersion < 27) {
logger.debug("Updating database: Create expiration_time_version column");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
ALTER TABLE recipient ADD expiration_time_version INTEGER DEFAULT 1 NOT NULL;
""");
}
}
} }
private static void createUuidMappingTable( private static void createUuidMappingTable(

View file

@ -864,6 +864,7 @@ public class SignalAccount implements Closeable {
null, null,
contact.color, contact.color,
contact.messageExpirationTime, contact.messageExpirationTime,
1,
0, 0,
false, false,
contact.blocked, contact.blocked,
@ -939,6 +940,7 @@ public class SignalAccount implements Closeable {
getContactStore().storeContact(recipientId, getContactStore().storeContact(recipientId,
Contact.newBuilder(contact) Contact.newBuilder(contact)
.withMessageExpirationTime(thread.messageExpirationTime) .withMessageExpirationTime(thread.messageExpirationTime)
.withMessageExpirationTimeVersion(1)
.build()); .build());
} }
} else { } else {

View file

@ -46,6 +46,7 @@ public class LegacyRecipientStore2 {
null, null,
r.contact.color, r.contact.color,
r.contact.messageExpirationTime, r.contact.messageExpirationTime,
1,
0, 0,
false, false,
r.contact.blocked, r.contact.blocked,

View file

@ -79,6 +79,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
color TEXT, color TEXT,
expiration_time INTEGER NOT NULL DEFAULT 0, expiration_time INTEGER NOT NULL DEFAULT 0,
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
mute_until INTEGER NOT NULL DEFAULT 0, mute_until INTEGER NOT NULL DEFAULT 0,
blocked INTEGER NOT NULL DEFAULT FALSE, blocked INTEGER NOT NULL DEFAULT FALSE,
archived INTEGER NOT NULL DEFAULT FALSE, archived INTEGER NOT NULL DEFAULT FALSE,
@ -332,7 +333,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
public List<Pair<RecipientId, Contact>> getContacts() { public List<Pair<RecipientId, Contact>> getContacts() {
final var sql = ( final var sql = (
""" """
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
FROM %s r FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
""" """
@ -356,7 +357,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
SELECT r._id, SELECT r._id,
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing, r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
r.discoverable, r.discoverable,
r.storage_record r.storage_record
@ -376,7 +377,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
SELECT r._id, SELECT r._id,
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing, r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
r.discoverable, r.discoverable,
r.storage_record r.storage_record
@ -413,7 +414,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
SELECT r._id, SELECT r._id,
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing, r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
r.discoverable, r.discoverable,
r.storage_record r.storage_record
@ -817,7 +818,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
final var sql = ( final var sql = (
""" """
UPDATE %s UPDATE %s
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ? SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?
WHERE _id = ? WHERE _id = ?
""" """
).formatted(TABLE_RECIPIENT); ).formatted(TABLE_RECIPIENT);
@ -826,21 +827,22 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
statement.setString(2, contact == null ? null : contact.familyName()); statement.setString(2, contact == null ? null : contact.familyName());
statement.setString(3, contact == null ? null : contact.nickName()); statement.setString(3, contact == null ? null : contact.nickName());
statement.setInt(4, contact == null ? 0 : contact.messageExpirationTime()); statement.setInt(4, contact == null ? 0 : contact.messageExpirationTime());
statement.setLong(5, contact == null ? 0 : contact.muteUntil()); statement.setInt(5, contact == null ? 0 : Math.max(1, contact.messageExpirationTimeVersion()));
statement.setBoolean(6, contact != null && contact.hideStory()); statement.setLong(6, contact == null ? 0 : contact.muteUntil());
statement.setBoolean(7, contact != null && contact.isProfileSharingEnabled()); statement.setBoolean(7, contact != null && contact.hideStory());
statement.setString(8, contact == null ? null : contact.color()); statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
statement.setBoolean(9, contact != null && contact.isBlocked()); statement.setString(9, contact == null ? null : contact.color());
statement.setBoolean(10, contact != null && contact.isArchived()); statement.setBoolean(10, contact != null && contact.isBlocked());
statement.setBoolean(11, contact != null && contact.isArchived());
if (contact == null || contact.unregisteredTimestamp() == null) { if (contact == null || contact.unregisteredTimestamp() == null) {
statement.setNull(11, Types.INTEGER); statement.setNull(12, Types.INTEGER);
} else { } else {
statement.setLong(11, contact.unregisteredTimestamp()); statement.setLong(12, contact.unregisteredTimestamp());
} }
statement.setString(12, contact == null ? null : contact.nickNameGivenName()); statement.setString(13, contact == null ? null : contact.nickNameGivenName());
statement.setString(13, contact == null ? null : contact.nickNameFamilyName()); statement.setString(14, contact == null ? null : contact.nickNameFamilyName());
statement.setString(14, contact == null ? null : contact.note()); statement.setString(15, contact == null ? null : contact.note());
statement.setLong(15, recipientId.id()); statement.setLong(16, recipientId.id());
statement.executeUpdate(); statement.executeUpdate();
} }
if (contact != null && contact.unregisteredTimestamp() != null) { if (contact != null && contact.unregisteredTimestamp() != null) {
@ -918,8 +920,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
markDiscoverable(connection, recipientId, false); markDiscoverable(connection, recipientId, false);
final var contact = getContact(connection, recipientId); final var contact = getContact(connection, recipientId);
if (recipientAddress.get().address().aci().isEmpty() || ( if (recipientAddress.get().address().aci().isEmpty() || (
contact != null contact != null && contact.unregisteredTimestamp() != null
&& contact.unregisteredTimestamp() != null
)) { )) {
markUnregisteredAndSplitIfNecessary(connection, recipientId); markUnregisteredAndSplitIfNecessary(connection, recipientId);
} }
@ -1416,7 +1417,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException { private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = ( final var sql = (
""" """
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
FROM %s r FROM %s r
WHERE r._id = ? AND (%s) WHERE r._id = ? AND (%s)
""" """
@ -1514,6 +1515,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
resultSet.getString("note"), resultSet.getString("note"),
resultSet.getString("color"), resultSet.getString("color"),
resultSet.getInt("expiration_time"), resultSet.getInt("expiration_time"),
resultSet.getInt("expiration_time_version"),
resultSet.getLong("mute_until"), resultSet.getLong("mute_until"),
resultSet.getBoolean("hide_story"), resultSet.getBoolean("hide_story"),
resultSet.getBoolean("blocked"), resultSet.getBoolean("blocked"),

View file

@ -699,6 +699,7 @@ public class DbusManagerImpl implements Manager {
null, null,
null, null,
0, 0,
1,
0, 0,
false, false,
contactBlocked, contactBlocked,