diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt index 59cb8ede08..03a1441d8f 100644 --- a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt @@ -95,7 +95,7 @@ class LibSessionTests { fakePollNewConfig(contacts, newContactMerge) verify(storageSpy).addLibSessionContacts(argThat { first().let { it.id == newContactId && it.approved } && size == 1 - }) + }, 0) verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index e4be27f24b..93eb8ceafd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -194,12 +194,12 @@ public class ApplicationContext extends Application implements DefaultLifecycleO } @Override - public void notifyUpdates(@NonNull ConfigBase forConfigObject) { + public void notifyUpdates(@NonNull ConfigBase forConfigObject, long messageTimestamp) { // forward to the config factory / storage ig if (forConfigObject instanceof UserProfile && !textSecurePreferences.getConfigurationMessageSynced()) { textSecurePreferences.setConfigurationMessageSynced(true); } - storage.notifyConfigUpdates(forConfigObject); + storage.notifyConfigUpdates(forConfigObject, messageTimestamp); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt index c47fbc6ea8..a65c22545b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ExpirationConfigurationDatabase.kt @@ -27,7 +27,7 @@ class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHel @JvmField val MIGRATE_GROUP_CONVERSATION_EXPIRY_TYPE_COMMAND = """ - INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}, ${RecipientDatabase.EXPIRE_MESSAGES}, 1 + INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID} FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} LIKE '$CLOSED_GROUP_PREFIX%' AND EXISTS (SELECT ${RecipientDatabase.EXPIRE_MESSAGES} FROM ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} = ${RecipientDatabase.TABLE_NAME}.${RecipientDatabase.ADDRESS} AND ${RecipientDatabase.EXPIRE_MESSAGES} > 0) @@ -35,7 +35,7 @@ class ExpirationConfigurationDatabase(context: Context, helper: SQLCipherOpenHel @JvmField val MIGRATE_ONE_TO_ONE_CONVERSATION_EXPIRY_TYPE_COMMAND = """ - INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID}, ${RecipientDatabase.EXPIRE_MESSAGES}, 2 + INSERT INTO $TABLE_NAME ($THREAD_ID) SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ID} FROM ${ThreadDatabase.TABLE_NAME}, ${RecipientDatabase.TABLE_NAME} WHERE ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$CLOSED_GROUP_PREFIX%' AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ADDRESS} NOT LIKE '$OPEN_GROUP_PREFIX%' diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index b8b480d553..f00ffca096 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -434,8 +434,8 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id) } - override fun notifyConfigUpdates(forConfigObject: ConfigBase) { - notifyUpdates(forConfigObject) + override fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) { + notifyUpdates(forConfigObject, messageTimestamp) } override fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean { @@ -446,16 +446,16 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co return configFactory.canPerformChange(variant, publicKey, changeTimestampMs) } - fun notifyUpdates(forConfigObject: ConfigBase) { + private fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) { when (forConfigObject) { - is UserProfile -> updateUser(forConfigObject) - is Contacts -> updateContacts(forConfigObject) - is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject) - is UserGroupsConfig -> updateUserGroups(forConfigObject) + is UserProfile -> updateUser(forConfigObject, messageTimestamp) + is Contacts -> updateContacts(forConfigObject, messageTimestamp) + is ConversationVolatileConfig -> updateConvoVolatile(forConfigObject, messageTimestamp) + is UserGroupsConfig -> updateUserGroups(forConfigObject, messageTimestamp) } } - private fun updateUser(userProfile: UserProfile) { + private fun updateUser(userProfile: UserProfile, messageTimestamp: Long) { val userPublicKey = getUserPublicKey() ?: return // would love to get rid of recipient and context from this val recipient = Recipient.from(context, fromSerialized(userPublicKey), false) @@ -486,11 +486,16 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co setPinned(ourThread, userProfile.getNtsPriority() > 0) } + getThreadId(recipient)?.let { ourThread -> + val expiration = ExpirationConfiguration(ourThread, userProfile.getNtsExpiry(), messageTimestamp) + DatabaseComponent.get(context).expirationConfigurationDatabase().setExpirationConfiguration(expiration) + } + } - private fun updateContacts(contacts: Contacts) { + private fun updateContacts(contacts: Contacts, messageTimestamp: Long) { val extracted = contacts.all().toList() - addLibSessionContacts(extracted) + addLibSessionContacts(extracted, messageTimestamp) } override fun clearUserPic() { @@ -510,7 +515,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context) } - private fun updateConvoVolatile(convos: ConversationVolatileConfig) { + private fun updateConvoVolatile(convos: ConversationVolatileConfig, messageTimestamp: Long) { val extracted = convos.all() for (conversation in extracted) { val threadId = when (conversation) { @@ -527,7 +532,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co } } - private fun updateUserGroups(userGroups: UserGroupsConfig) { + private fun updateUserGroups(userGroups: UserGroupsConfig, messageTimestamp: Long) { val threadDb = DatabaseComponent.get(context).threadDatabase() val localUserPublicKey = getUserPublicKey() ?: return Log.w( "Loki", @@ -579,6 +584,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co } for (group in lgc) { + val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId) val existingGroup = existingClosedGroups.firstOrNull { GroupUtil.doubleDecodeGroupId(it.encodedId) == group.sessionId } val existingThread = existingGroup?.let { getThreadId(existingGroup.encodedId) } if (existingGroup != null) { @@ -593,7 +599,6 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co } else { val members = group.members.keys.map { Address.fromSerialized(it) } val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) } - val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId) val title = group.name val formationTimestamp = (group.joinedAt * 1000L) createGroup(groupId, title, admins + members, null, null, admins, formationTimestamp) @@ -616,6 +621,17 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co // Start polling ClosedGroupPollerV2.shared.startPolling(group.sessionId) } + getThreadId(Address.fromSerialized(groupId))?.let { conversationThreadId -> + val mode = + if (group.disappearingTimer == 0L) ExpiryMode.NONE + else ExpiryMode.AfterRead(group.disappearingTimer) + val newConfig = ExpirationConfiguration( + conversationThreadId, mode, messageTimestamp + ) + DatabaseComponent.get(context) + .expirationConfigurationDatabase() + .setExpirationConfiguration(newConfig) + } } } @@ -1163,7 +1179,7 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co return if (recipientSettings.isPresent) { recipientSettings.get() } else null } - override fun addLibSessionContacts(contacts: List) { + override fun addLibSessionContacts(contacts: List, timestamp: Long) { val mappingDb = DatabaseComponent.get(context).blindedIdMappingDatabase() val moreContacts = contacts.filter { contact -> val id = SessionId(contact.id) @@ -1204,6 +1220,15 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co setPinned(conversationThreadId, contact.priority == PRIORITY_PINNED) } } + getThreadId(recipient)?.let { conversationThreadId -> + val expiration = ExpirationConfiguration( + conversationThreadId, + contact.expiryMode, + timestamp + ) + DatabaseComponent.get(context).expirationConfigurationDatabase() + .setExpirationConfiguration(expiration) + } setRecipientHash(recipient, contact.hashCode().toString()) } } @@ -1676,7 +1701,15 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co override fun getExpirationConfiguration(threadId: Long): ExpirationConfiguration? { val recipient = getRecipientForThread(threadId) ?: return null val dbExpirationMetadata = DatabaseComponent.get(context).expirationConfigurationDatabase().getExpirationConfiguration(threadId) ?: return null - return if (recipient.isContactRecipient && recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) { + return if (recipient.isLocalNumber) { + configFactory.user?.getNtsExpiry()?.let { mode -> + ExpirationConfiguration( + threadId, + mode, + dbExpirationMetadata.updatedTimestampMs + ) + } + } else if (recipient.isContactRecipient && recipient.address.serialize().startsWith(IdPrefix.STANDARD.value)) { // read it from contacts config if exists configFactory.contacts?.get(recipient.address.serialize())?.let { contact -> val mode = contact.expiryMode @@ -1703,11 +1736,26 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co override fun setExpirationConfiguration(config: ExpirationConfiguration) { val recipient = getRecipientForThread(config.threadId) ?: return if (recipient.isClosedGroupRecipient) { - configFactory.userGroups?.getLegacyGroupInfo() + val userGroups = configFactory.userGroups ?: return + val groupPublicKey = GroupUtil.addressToGroupSessionId(recipient.address) + val expiryMode = config.expiryMode + val groupInfo = userGroups.getLegacyGroupInfo(groupPublicKey)?.let { info -> + info.copy(disappearingTimer = when (expiryMode) { + null, ExpiryMode.NONE -> 0 + else -> expiryMode.expirySeconds + }) + } ?: return + userGroups.set(groupInfo) } else if (recipient.isLocalNumber) { - + val user = configFactory.user ?: return + user.setNtsExpiry(config.expiryMode ?: ExpiryMode.NONE) } else if (recipient.isContactRecipient) { - + val contacts = configFactory.contacts ?: return + val expiry = config.expiryMode + val contact = contacts.get(recipient.address.serialize())?.copy( + expiryMode = expiry ?: ExpiryMode.NONE + ) ?: return + contacts.set(contact) } DatabaseComponent.get(context).expirationConfigurationDatabase().setExpirationConfiguration(config) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt index d664ffedb2..8379e1a23b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ConfigFactory.kt @@ -187,7 +187,7 @@ class ConfigFactory( override fun persist(forConfigObject: ConfigBase, timestamp: Long) { try { listeners.forEach { listener -> - listener.notifyUpdates(forConfigObject) + listener.notifyUpdates(forConfigObject, timestamp) } when (forConfigObject) { is UserProfile -> persistUserConfigDump(timestamp) diff --git a/libsession-util/src/main/cpp/user_profile.cpp b/libsession-util/src/main/cpp/user_profile.cpp index 78b671ef0d..e765cb1827 100644 --- a/libsession-util/src/main/cpp/user_profile.cpp +++ b/libsession-util/src/main/cpp/user_profile.cpp @@ -95,4 +95,28 @@ Java_network_loki_messenger_libsession_1util_UserProfile_getNtsPriority(JNIEnv * std::lock_guard lock{util::util_mutex_}; auto profile = ptrToProfile(env, thiz); return profile->get_nts_priority(); -} \ No newline at end of file +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_setNtsExpiry(JNIEnv *env, jobject thiz, + jobject expiry_mode) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto expiry = util::deserialize_expiry(env, expiry_mode); + profile->set_nts_expiry(std::chrono::seconds (expiry.second)); +} + +extern "C" +JNIEXPORT jobject JNICALL +Java_network_loki_messenger_libsession_1util_UserProfile_getNtsExpiry(JNIEnv *env, jobject thiz) { + std::lock_guard lock{util::util_mutex_}; + auto profile = ptrToProfile(env, thiz); + auto nts_expiry = profile->get_nts_expiry(); + if (nts_expiry == std::nullopt) { + auto expiry = util::serialize_expiry(env, session::config::expiration_mode::none, std::chrono::seconds(0)); + return expiry; + } + auto expiry = util::serialize_expiry(env, session::config::expiration_mode::after_send, std::chrono::seconds(*nts_expiry)); + return expiry; +} diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 52fb541d7d..2ed11adbb3 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -4,6 +4,7 @@ import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.ConfigPush import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.Conversation +import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.UserPic import org.session.libsignal.protos.SignalServiceProtos.SharedConfigMessage.Kind @@ -126,6 +127,8 @@ class UserProfile(pointer: Long) : ConfigBase(pointer) { external fun setPic(userPic: UserPic) external fun setNtsPriority(priority: Int) external fun getNtsPriority(): Int + external fun setNtsExpiry(expiryMode: ExpiryMode) + external fun getNtsExpiry(): ExpiryMode } class ConversationVolatileConfig(pointer: Long): ConfigBase(pointer) { diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index a7aab4e72c..56ccf7e84d 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -182,7 +182,7 @@ interface StorageProtocol { fun setContact(contact: Contact) fun getRecipientForThread(threadId: Long): Recipient? fun getRecipientSettings(address: Address): RecipientSettings? - fun addLibSessionContacts(contacts: List) + fun addLibSessionContacts(contacts: List, timestamp: Long) fun addContacts(contacts: List) // Attachments @@ -229,7 +229,7 @@ interface StorageProtocol { fun updateDisappearingState(threadID: Long, disappearingState: Recipient.DisappearingState) // Shared configs - fun notifyConfigUpdates(forConfigObject: ConfigBase) + fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index cb8233b1c2..c82eb2d128 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -65,11 +65,13 @@ abstract class Message { if (config.isEnabled && config.expiryMode != null) { expirationTimer = config.expiryMode.expirySeconds.toInt() lastDisappearingMessageChangeTimestamp = config.updatedTimestampMs - config.expiryMode.let { expiryMode -> - when (expiryMode) { - is ExpiryMode.AfterSend -> expirationType = ExpirationType.DELETE_AFTER_SEND - is ExpiryMode.AfterRead -> expirationType = ExpirationType.DELETE_AFTER_READ - ExpiryMode.NONE -> { /* do nothing */ } + if (ExpirationConfiguration.isNewConfigEnabled) { + config.expiryMode.let { expiryMode -> + when (expiryMode) { + is ExpiryMode.AfterSend -> expirationType = ExpirationType.DELETE_AFTER_SEND + is ExpiryMode.AfterRead -> expirationType = ExpirationType.DELETE_AFTER_READ + ExpiryMode.NONE -> { /* do nothing */ } + } } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index b79de9a11c..64c74d497c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -3,8 +3,8 @@ package org.session.libsession.messaging.messages.control import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.visible.VisibleMessage -import org.session.libsignal.utilities.Log import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.utilities.Log class ExpirationTimerUpdate() : ControlMessage() { /** In the case of a sync message, the public key of the person the message was targeted at. @@ -26,7 +26,9 @@ class ExpirationTimerUpdate() : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { val dataMessageProto = if (proto.hasDataMessage()) proto.dataMessage else return null - val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 + val isExpirationTimerUpdate = dataMessageProto.flags.and( + SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE + ) != 0 if (!isExpirationTimerUpdate) return null val syncTarget = dataMessageProto.syncTarget val duration = if (proto.hasExpirationTimer()) proto.expirationTimer else dataMessageProto.expireTimer diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 93fad9d2b0..06ddead859 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -69,7 +69,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, // Do nothing if the message was outdated if (MessageReceiver.messageIsOutdated(message, threadId, openGroupID)) { return } - MessageReceiver.updateExpiryIfNeeded(message, proto, openGroupID, ) + MessageReceiver.updateExpiryIfNeeded(message, proto, openGroupID) when (message) { is ReadReceipt -> handleReadReceipt(message) is TypingIndicator -> handleTypingIndicator(message) diff --git a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt index a45fcd6761..8add1da849 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ConfigFactoryProtocol.kt @@ -19,5 +19,5 @@ interface ConfigFactoryProtocol { } interface ConfigFactoryUpdateListener { - fun notifyUpdates(forConfigObject: ConfigBase) + fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index bfab2585de..45104a43a8 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -1,6 +1,5 @@ package org.session.libsession.utilities -import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.utilities.Hex import java.io.IOException @@ -104,6 +103,11 @@ object GroupUtil { return Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID))) } + @JvmStatic + fun addressToGroupSessionId(address: Address): String { + return doubleDecodeGroupId(address.toGroupString()) + } + fun createConfigMemberMap( members: Collection, admins: Collection