Fix Message#expiryMode de/serialisation

pull/1313/head
Andrew 5 months ago
parent cb0327ecb2
commit 4c7485f53d

@ -14,6 +14,7 @@ import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.SSKEnvironment.MessageExpirationManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -25,11 +26,11 @@ class DisappearingMessages @Inject constructor(
private val textSecurePreferences: TextSecurePreferences,
private val messageExpirationManager: MessageExpirationManagerProtocol,
) {
fun set(threadId: Long, address: Address, mode: ExpiryMode) {
fun set(threadId: Long, address: Address, mode: ExpiryMode, isGroup: Boolean) {
val expiryChangeTimestampMs = SnodeAPI.nowWithOffset
MessagingModuleConfiguration.shared.storage.setExpirationConfiguration(ExpirationConfiguration(threadId, mode, expiryChangeTimestampMs))
val message = ExpirationTimerUpdate().apply {
val message = ExpirationTimerUpdate(isGroup = isGroup).apply {
expiryMode = mode
sender = textSecurePreferences.getLocalNumber()
isSenderSelf = true
@ -62,7 +63,7 @@ class DisappearingMessages @Inject constructor(
text = if (message.expiresIn == 0L) R.string.dialog_disappearing_messages_follow_setting_confirm else R.string.dialog_disappearing_messages_follow_setting_set,
contentDescription = if (message.expiresIn == 0L) R.string.AccessibilityId_confirm else R.string.AccessibilityId_set_button
) {
set(message.threadId, message.recipient.address, message.expiryMode)
set(message.threadId, message.recipient.address, message.expiryMode, message.recipient.isClosedGroupRecipient)
}
cancelButton()
}

@ -87,7 +87,7 @@ class DisappearingMessagesViewModel(
return@launch
}
disappearingMessages.set(threadId, address, mode)
disappearingMessages.set(threadId, address, mode, state.isGroup)
_event.send(Event.SUCCESS)
}

@ -72,6 +72,7 @@ import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.DataExtractionNotification
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
@ -1574,7 +1575,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
return null
}
// Create the message
val message = VisibleMessage()
val message = VisibleMessage().applyExpiryMode(viewModel.threadId)
message.sentTimestamp = sentTimestamp
message.text = text
val expiresInMillis = viewModel.expirationConfiguration?.expiryMode?.expiryMillis ?: 0
@ -1609,7 +1610,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val sentTimestamp = SnodeAPI.nowWithOffset
processMessageRequestApproval()
// Create the message
val message = VisibleMessage()
val message = VisibleMessage().applyExpiryMode()
message.sentTimestamp = sentTimestamp
message.text = body
val quote = quotedMessage?.let {

@ -30,6 +30,10 @@ class ExpirationTimerView @JvmOverloads constructor(
R.drawable.timer60
)
fun setTimerIcon() {
setExpirationTime(0L, 0L)
}
fun setExpirationTime(startedAt: Long, expiresIn: Long) {
if (expiresIn == 0L) {
setImageResource(R.drawable.timer55)

@ -53,12 +53,19 @@ class ControlMessageView : LinearLayout {
Log.d(TAG, "bind() called, messageBody = $messageBody")
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
val threadRecipient = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId)
if (threadRecipient?.isClosedGroupRecipient == true) {
expirationTimerView.setTimerIcon()
} else {
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
}
followSetting.isVisible = ExpirationConfiguration.isNewConfigEnabled
&& !message.isOutgoing
&& message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
&& DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(message.threadId)?.isGroupRecipient != true
&& threadRecipient?.isGroupRecipient != true
followSetting.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
}

@ -4,11 +4,11 @@ import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId
data class MarkedMessageInfo(val syncMessageId: SyncMessageId, val expirationInfo: ExpirationInfo) {
fun guessExpiryType(): ExpiryType = expirationInfo.run {
when {
syncMessageId.timetamp == expireStarted -> ExpiryType.AFTER_SEND
expiresIn > 0 -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
}
val expiryType get() = when {
syncMessageId.timetamp == expirationInfo.expireStarted -> ExpiryType.AFTER_SEND
expirationInfo.expiresIn > 0 -> ExpiryType.AFTER_READ
else -> ExpiryType.NONE
}
val expiryMode get() = expiryType.mode(expirationInfo.expiresIn)
}

@ -382,9 +382,6 @@ open class Storage(
DatabaseComponent.get(context).lokiMessageDatabase().setMessageServerHash(id, message.isMediaMessage(), serverHash)
}
}
if (expiryMode is ExpiryMode.AfterSend) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender!!, expireStartedAt)
}
return messageID
}
@ -972,26 +969,24 @@ open class Storage(
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
val expirationConfig = getExpirationConfiguration(threadId)
val expiryMode = expirationConfig?.expiryMode
val expiresInMillis = expiryMode?.expiryMillis ?: 0
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
val expiresInMillis = expiryMode.expiryMillis
val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0
val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), expiresInMillis, expireStartedAt, true, false)
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseComponent.get(context).smsDatabase()
smsDB.insertMessageInbox(infoMessage, true)
if (expiryMode is ExpiryMode.AfterSend) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
}
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
val userPublicKey = getUserPublicKey()
val userPublicKey = getUserPublicKey()!!
val recipient = Recipient.from(context, fromSerialized(groupID), false)
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
val expirationConfig = getExpirationConfiguration(threadId)
val expiryMode = expirationConfig?.expiryMode
val expiresInMillis = expiryMode?.expiryMillis ?: 0
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
val expiresInMillis = expiryMode.expiryMillis
val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0
val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: ""
val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, expiresInMillis, expireStartedAt, true, null, listOf(), listOf())
@ -1000,9 +995,7 @@ open class Storage(
if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, runThreadUpdate = true)
mmsDB.markAsSent(infoMessageID, true)
if (expiryMode is ExpiryMode.AfterSend) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, userPublicKey!!, expireStartedAt)
}
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, userPublicKey, expiryMode)
}
override fun isClosedGroup(publicKey: String): Boolean {
@ -1403,8 +1396,8 @@ open class Storage(
if (recipient.isBlocked) return
val threadId = getThreadId(recipient) ?: return
val expirationConfig = getExpirationConfiguration(threadId)
val expiryMode = expirationConfig?.expiryMode
val expiresInMillis = expiryMode?.expiryMillis ?: 0
val expiryMode = expirationConfig?.expiryMode ?: ExpiryMode.NONE
val expiresInMillis = expiryMode.expiryMillis
val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0
val mediaMessage = IncomingMediaMessage(
address,
@ -1426,9 +1419,8 @@ open class Storage(
)
database.insertSecureDecryptedMessageInbox(mediaMessage, threadId, runThreadUpdate = true)
if (expiryMode is ExpiryMode.AfterSend) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
}
override fun insertMessageRequestResponse(response: MessageRequestResponse) {
@ -1557,14 +1549,12 @@ open class Storage(
val recipient = Recipient.from(context, address, false)
val threadId = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
val expirationConfig = getExpirationConfiguration(threadId)
val expiryMode = expirationConfig?.expiryMode
val expiresInMillis = expiryMode?.expiryMillis ?: 0
val expiryMode = expirationConfig?.expiryMode?.coerceSendToRead() ?: ExpiryMode.NONE
val expiresInMillis = expiryMode.expiryMillis
val expireStartedAt = if (expiryMode is ExpiryMode.AfterSend) sentTimestamp else 0
val callMessage = IncomingTextMessage.fromCallInfo(callMessageType, address, Optional.absent(), sentTimestamp, expiresInMillis, expireStartedAt)
database.insertCallMessage(callMessage)
if (expiryMode is ExpiryMode.AfterSend) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(sentTimestamp, senderPublicKey, expireStartedAt)
}
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(sentTimestamp, senderPublicKey, expiryMode)
}
override fun conversationHasOutgoing(userPublicKey: String): Boolean {

@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.sending_receiving.MessageSender.send
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeAPI.nowWithOffset
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled
import org.session.libsession.utilities.associateByNotNull
@ -62,6 +63,18 @@ class MarkReadReceiver : BroadcastReceiver() {
sendReadReceipts(context, markedReadMessages)
markedReadMessages
.filter { it.expiryType == ExpiryType.AFTER_READ }
.forEach { info ->
DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(info.syncMessageId.timetamp)
?.takeUnless { it.isExpirationTimerUpdate && it.recipient.isClosedGroupRecipient }
?.run {
SSKEnvironment.shared.messageExpirationManager.startDisappearAfterRead(
info.syncMessageId.timetamp,
info.syncMessageId.address.serialize()
)
}
}
markedReadMessages.forEach { scheduleDeletion(context, it.expirationInfo) }
hashToDisappearAfterReadMessage(context, markedReadMessages)?.let {
@ -77,7 +90,7 @@ class MarkReadReceiver : BroadcastReceiver() {
val loki = DatabaseComponent.get(context).lokiMessageDatabase()
return markedReadMessages
.filter { it.guessExpiryType() == ExpiryType.AFTER_READ }
.filter { it.expiryType == ExpiryType.AFTER_READ }
.associateByNotNull { it.expirationInfo.run { loki.getMessageServerHash(id, isMms) } }
.takeIf { it.isNotEmpty() }
}

@ -49,6 +49,9 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
fun scheduleDeletion(id: Long, mms: Boolean, startedAtTimestamp: Long, expiresInMillis: Long) {
Log.d(TAG, "scheduleDeletion() called with: id = $id, mms = $mms, startedAtTimestamp = $startedAtTimestamp, expiresInMillis = $expiresInMillis")
if (startedAtTimestamp <= 0) return
val expiresAtMillis = startedAtTimestamp + expiresInMillis
synchronized(expiringMessageReferences) {
expiringMessageReferences += ExpiringMessageReference(id, mms, expiresAtMillis)
@ -164,16 +167,16 @@ class ExpiringMessageManager(context: Context) : MessageExpirationManagerProtoco
insertIncomingExpirationTimerMessage(message, expireStartedAt)
}
startAnyExpiration(message)
maybeStartExpiration(message)
}
override fun startAnyExpiration(timestamp: Long, author: String, expireStartedAt: Long) {
Log.d(TAG, "startAnyExpiration() called with: timestamp = $timestamp, author = $author, expireStartedAt = $expireStartedAt")
val messageRecord = mmsSmsDatabase.getMessageFor(timestamp, author) ?: throw Exception("no message record!!!")
Log.d(TAG, "startAnyExpiration() $messageRecord")
val mms = messageRecord.isMms()
getDatabase(mms).markExpireStarted(messageRecord.getId(), expireStartedAt)
scheduleDeletion(messageRecord.getId(), mms, expireStartedAt, messageRecord.expiresIn)
mmsSmsDatabase.getMessageFor(timestamp, author)?.run {
getDatabase(isMms()).markExpireStarted(getId(), expireStartedAt)
scheduleDeletion(getId(), isMms(), expireStartedAt, expiresIn)
} ?: Log.e(TAG, "no message record!!!")
}
private inner class LoadTask : Runnable {

@ -16,6 +16,7 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
@ -57,8 +58,12 @@ import java.util.ArrayDeque
import java.util.UUID
import org.thoughtcrime.securesms.webrtc.data.State as CallState
class CallManager(context: Context, audioManager: AudioManagerCompat, private val storage: StorageProtocol): PeerConnection.Observer,
SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer {
class CallManager(
private val context: Context,
audioManager: AudioManagerCompat,
private val storage: StorageProtocol
): PeerConnection.Observer,
SignalAudioManager.EventListener, CameraEventListener, DataChannel.Observer {
sealed class StateEvent {
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
@ -293,17 +298,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
while (pendingOutgoingIceUpdates.isNotEmpty()) {
currentPendings.add(pendingOutgoingIceUpdates.pop())
}
val sdps = currentPendings.map { it.sdp }
val sdpMLineIndexes = currentPendings.map { it.sdpMLineIndex }
val sdpMids = currentPendings.map { it.sdpMid }
MessageSender.sendNonDurably(CallMessage(
ICE_CANDIDATES,
sdps = sdps,
sdpMLineIndexes = sdpMLineIndexes,
sdpMids = sdpMids,
currentCallId
), currentRecipient.address, isSyncMessage = currentRecipient.isLocalNumber)
CallMessage(
ICE_CANDIDATES,
sdps = currentPendings.map(IceCandidate::sdp),
sdpMLineIndexes = currentPendings.map(IceCandidate::sdpMLineIndex),
sdpMids = currentPendings.map(IceCandidate::sdpMid),
currentCallId
)
.applyExpiryMode()
.also { MessageSender.sendNonDurably(it, currentRecipient.address, isSyncMessage = currentRecipient.isLocalNumber) }
}
}
}

@ -4,11 +4,13 @@ import kotlin.time.Duration.Companion.seconds
sealed class ExpiryMode(val expirySeconds: Long) {
object NONE: ExpiryMode(0)
data class Legacy(private val seconds: Long): ExpiryMode(seconds) // after read
data class Legacy(private val seconds: Long): ExpiryMode(seconds)
data class AfterSend(private val seconds: Long): ExpiryMode(seconds)
data class AfterRead(private val seconds: Long): ExpiryMode(seconds)
val duration get() = expirySeconds.seconds
val expiryMillis get() = expirySeconds * 1000L
fun coerceSendToRead(coerce: Boolean = true) = if (coerce && this is AfterSend) AfterRead(expirySeconds) else this
}

@ -6,6 +6,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
@ -25,6 +26,8 @@ abstract class Message {
var expiryMode: ExpiryMode = ExpiryMode.NONE
open val coerceDisappearAfterSendToRead = false
open val defaultTtl: Long = 14 * 24 * 60 * 60 * 1000
open val ttl: Long get() = specifiedTtl ?: defaultTtl
open val isSelfSendValid: Boolean = false
@ -51,26 +54,16 @@ abstract class Message {
abstract fun toProto(): SignalServiceProtos.Content?
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
dataMessage.group = groupProto.build()
dataMessage.group = SignalServiceProtos.GroupContext.newBuilder().apply {
id = GroupUtil.doubleEncodeGroupID(recipient!!).let(GroupUtil::getDecodedGroupIDAsData).let(ByteString::copyFrom)
type = SignalServiceProtos.GroupContext.Type.DELIVER
}.build()
}
fun SignalServiceProtos.Content.Builder.setExpirationConfigurationIfNeeded(
threadId: Long?,
coerceDisappearAfterSendToRead: Boolean = false
): SignalServiceProtos.Content.Builder {
val config = threadId?.let(MessagingModuleConfiguration.shared.storage::getExpirationConfiguration)
?: run {
expirationTimer = 0
return this
}
expirationTimer = config.expiryMode.expirySeconds.toInt()
lastDisappearingMessageChangeTimestamp = config.updatedTimestampMs
expirationType = when (config.expiryMode) {
is ExpiryMode.AfterSend -> if (coerceDisappearAfterSendToRead) ExpirationType.DELETE_AFTER_READ else ExpirationType.DELETE_AFTER_SEND
fun SignalServiceProtos.Content.Builder.applyExpiryMode(): SignalServiceProtos.Content.Builder {
expirationTimer = expiryMode.expirySeconds.toInt()
expirationType = when (expiryMode) {
is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
else -> ExpirationType.UNKNOWN
}
@ -79,13 +72,36 @@ abstract class Message {
}
inline fun <reified M: Message> M.copyExpiration(proto: SignalServiceProtos.Content): M {
val duration: Int = (if (proto.hasExpirationTimer()) proto.expirationTimer else if (proto.hasDataMessage()) proto.dataMessage?.expireTimer else null) ?: return this
expiryMode = when (proto.expirationType.takeIf { duration > 0 }) {
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
else -> ExpiryMode.NONE
(proto.takeIf { it.hasExpirationTimer() }?.expirationTimer ?: proto.dataMessage?.expireTimer)?.let { duration ->
expiryMode = when (proto.expirationType.takeIf { duration > 0 }) {
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
else -> ExpiryMode.NONE
}
}
return this
}
fun SignalServiceProtos.Content.expiryMode(): ExpiryMode =
(takeIf { it.hasExpirationTimer() }?.expirationTimer ?: dataMessage?.expireTimer)?.let { duration ->
when (expirationType.takeIf { duration > 0 }) {
ExpirationType.DELETE_AFTER_SEND -> ExpiryMode.AfterSend(duration.toLong())
ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(duration.toLong())
else -> ExpiryMode.NONE
}
} ?: ExpiryMode.NONE
/**
* Apply ExpiryMode from the current setting.
*/
inline fun <reified M: Message> M.applyExpiryMode(): M {
val address = Address.fromSerialized(sender ?: return this)
MessagingModuleConfiguration.shared.storage.getThreadId(address)?.let(::applyExpiryMode)
return this
}
inline fun <reified M: Message> M.applyExpiryMode(thread: Long): M {
val storage = MessagingModuleConfiguration.shared.storage
expiryMode = storage.getExpirationConfiguration(thread)?.expiryMode?.coerceSendToRead(coerceDisappearAfterSendToRead) ?: ExpiryMode.NONE
return this
}

@ -1,5 +1,6 @@
package org.session.libsession.messaging.messages.control
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.*
@ -13,6 +14,7 @@ class CallMessage(): ControlMessage() {
var sdpMids: List<String> = listOf()
var callId: UUID? = null
override val coerceDisappearAfterSendToRead = true
override val isSelfSendValid: Boolean get() = type in arrayOf(ANSWER, END_CALL)
override val defaultTtl: Long = 300000L // 5m
@ -40,21 +42,21 @@ class CallMessage(): ControlMessage() {
listOf(),
listOf(),
callId
)
).applyExpiryMode()
fun preOffer(callId: UUID) = CallMessage(PRE_OFFER,
listOf(),
listOf(),
listOf(),
callId
)
).applyExpiryMode()
fun offer(sdp: String, callId: UUID) = CallMessage(OFFER,
listOf(sdp),
listOf(),
listOf(),
callId
)
).applyExpiryMode()
fun endCall(callId: UUID) = CallMessage(END_CALL, emptyList(), emptyList(), emptyList(), callId)
@ -83,9 +85,8 @@ class CallMessage(): ControlMessage() {
.addAllSdpMids(sdpMids)
.setUuid(callId!!.toString())
val content = SignalServiceProtos.Content.newBuilder()
content.setExpirationConfigurationIfNeeded(threadID, true)
return content
return SignalServiceProtos.Content.newBuilder()
.applyExpiryMode()
.setCallMessage(callMessage)
.build()
}

@ -178,7 +178,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
contentProto.dataMessage = dataMessageProto.build()
// Expiration timer
val threadId = groupID?.let { MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(Address.fromSerialized(it)) }
contentProto.setExpirationConfigurationIfNeeded(threadId)
contentProto.applyExpiryMode()
return contentProto.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")

@ -20,10 +20,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
override val isSelfSendValid: Boolean = true
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>, var expirationTimer: Int) {
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
internal constructor() : this("", "", null, listOf(), listOf(), 0)
internal constructor() : this("", "", null, listOf(), listOf())
override fun toString(): String {
return name
@ -40,8 +40,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
val members = proto.membersList.map { it.toByteArray().toHexString() }
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
val expirationTimer = proto.expirationTimer
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins, expirationTimer)
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
}
}
@ -55,7 +54,6 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.expirationTimer = expirationTimer
return result.build()
}
}
@ -128,15 +126,12 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(group.encodedId))
val expiryConfig = storage.getExpirationConfiguration(threadID)
val closedGroup = ClosedGroup(
groupPublicKey,
group.title,
encryptionKeyPair,
group.members.map { it.serialize() },
group.admins.map { it.serialize() },
expiryConfig?.expiryMode?.expirySeconds?.toInt() ?: 0
group.admins.map { it.serialize() }
)
closedGroups.add(closedGroup)
}

@ -7,6 +7,8 @@ import org.session.libsignal.utilities.Log
class DataExtractionNotification() : ControlMessage() {
var kind: Kind? = null
override val coerceDisappearAfterSendToRead = true
sealed class Kind {
class Screenshot() : Kind()
class MediaSaved(val timestamp: Long) : Kind()
@ -64,10 +66,10 @@ class DataExtractionNotification() : ControlMessage() {
dataExtractionNotification.timestamp = kind.timestamp
}
}
val contentProto = SignalServiceProtos.Content.newBuilder()
contentProto.dataExtractionNotification = dataExtractionNotification.build()
contentProto.setExpirationConfigurationIfNeeded(threadID, true)
return contentProto.build()
return SignalServiceProtos.Content.newBuilder()
.setDataExtractionNotification(dataExtractionNotification.build())
.applyExpiryMode()
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
return null

@ -1,59 +1,53 @@
package org.session.libsession.messaging.messages.control
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.copyExpiration
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
import org.session.libsignal.utilities.Log
/** In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
data class ExpirationTimerUpdate(var syncTarget: String? = null) : ControlMessage() {
data class ExpirationTimerUpdate(var syncTarget: String? = null, val isGroup: Boolean = false) : ControlMessage() {
override val isSelfSendValid: Boolean = true
companion object {
const val TAG = "ExpirationTimerUpdate"
private val storage = MessagingModuleConfiguration.shared.storage
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
if (!isExpirationTimerUpdate) return null
return ExpirationTimerUpdate(dataMessageProto.syncTarget)
.copyExpiration(proto)
}
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? =
proto.dataMessage?.takeIf { it.flags and EXPIRATION_TIMER_UPDATE_VALUE != 0 }?.run {
ExpirationTimerUpdate(syncTarget, hasGroup()).copyExpiration(proto)
}
}
override fun toProto(): SignalServiceProtos.Content? {
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
dataMessageProto.expireTimer = expiryMode.expirySeconds.toInt()
// Sync target
if (syncTarget != null) {
dataMessageProto.syncTarget = syncTarget
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder().apply {
flags = EXPIRATION_TIMER_UPDATE_VALUE
expireTimer = expiryMode.expirySeconds.toInt()
}
// Sync target
syncTarget?.let { dataMessageProto.syncTarget = it }
// Group context
if (MessagingModuleConfiguration.shared.storage.isClosedGroup(recipient!!)) {
if (storage.isClosedGroup(recipient!!)) {
try {
setGroupContext(dataMessageProto)
} catch(e: Exception) {
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
Log.w(TAG, "Couldn't construct visible message proto from: $this", e)
return null
}
}
return try {
SignalServiceProtos.Content.newBuilder().apply {
dataMessage = dataMessageProto.build()
setExpirationConfigurationIfNeeded(threadID)
}.build()
SignalServiceProtos.Content.newBuilder()
.setDataMessage(dataMessageProto)
.applyExpiryMode()
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this")
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this", e)
null
}
}
}
}

@ -20,7 +20,7 @@ class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = nu
profile?.profileKey?.let { messageRequestResponseProto.profileKey = ByteString.copyFrom(it) }
return try {
SignalServiceProtos.Content.newBuilder()
.setExpirationConfigurationIfNeeded(threadID)
.applyExpiryMode()
.setMessageRequestResponse(messageRequestResponseProto.build())
.build()
} catch (e: Exception) {

@ -37,14 +37,15 @@ class ReadReceipt() : ControlMessage() {
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
return null
}
val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder()
receiptProto.type = SignalServiceProtos.ReceiptMessage.Type.READ
receiptProto.addAllTimestamp(timestamps.asIterable())
val contentProto = SignalServiceProtos.Content.newBuilder()
return try {
contentProto.receiptMessage = receiptProto.build()
contentProto.setExpirationConfigurationIfNeeded(threadID)
contentProto.build()
SignalServiceProtos.Content.newBuilder()
.setReceiptMessage(
SignalServiceProtos.ReceiptMessage.newBuilder()
.setType(SignalServiceProtos.ReceiptMessage.Type.READ)
.addAllTimestamp(timestamps.asIterable()).build()
).applyExpiryMode()
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
null

@ -56,14 +56,11 @@ class TypingIndicator() : ControlMessage() {
Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
return null
}
val typingIndicatorProto = SignalServiceProtos.TypingMessage.newBuilder()
typingIndicatorProto.timestamp = timestamp
typingIndicatorProto.action = kind.toProto()
val contentProto = SignalServiceProtos.Content.newBuilder()
return try {
contentProto.typingMessage = typingIndicatorProto.build()
contentProto.setExpirationConfigurationIfNeeded(threadID)
contentProto.build()
SignalServiceProtos.Content.newBuilder()
.setTypingMessage(SignalServiceProtos.TypingMessage.newBuilder().setTimestamp(timestamp).setAction(kind.toProto()).build())
.applyExpiryMode()
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
null

@ -41,14 +41,11 @@ class UnsendRequest(): ControlMessage() {
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
return null
}
val unsendRequestProto = SignalServiceProtos.UnsendRequest.newBuilder()
unsendRequestProto.timestamp = timestamp
unsendRequestProto.author = author
val contentProto = SignalServiceProtos.Content.newBuilder()
return try {
contentProto.unsendRequest = unsendRequestProto.build()
contentProto.setExpirationConfigurationIfNeeded(threadID)
contentProto.build()
SignalServiceProtos.Content.newBuilder()
.setUnsendRequest(SignalServiceProtos.UnsendRequest.newBuilder().setTimestamp(timestamp).setAuthor(author).build())
.applyExpiryMode()
.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
null

@ -44,52 +44,26 @@ data class VisibleMessage(
companion object {
const val TAG = "VisibleMessage"
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
val dataMessage = proto.dataMessage ?: return null
val result = VisibleMessage()
if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget }
result.text = dataMessage.body
// Attachments are handled in MessageReceiver
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
if (quoteProto != null) {
val quote = Quote.fromProto(quoteProto)
result.quote = quote
}
val linkPreviewProto = dataMessage.previewList.firstOrNull()
if (linkPreviewProto != null) {
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
result.linkPreview = linkPreview
}
val openGroupInvitationProto = if (dataMessage.hasOpenGroupInvitation()) dataMessage.openGroupInvitation else null
if (openGroupInvitationProto != null) {
val openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto)
result.openGroupInvitation = openGroupInvitation
}
// TODO Contact
val profile = Profile.fromProto(dataMessage)
if (profile != null) { result.profile = profile }
val reactionProto = if (dataMessage.hasReaction()) dataMessage.reaction else null
if (reactionProto != null) {
val reaction = Reaction.fromProto(reactionProto)
result.reaction = reaction
}
result.blocksMessageRequests = with (dataMessage) { hasBlocksCommunityMessageRequests() && blocksCommunityMessageRequests }
return result.copyExpiration(proto)
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? =
proto.dataMessage?.let { VisibleMessage().apply {
if (it.hasSyncTarget()) syncTarget = it.syncTarget
text = it.body
// Attachments are handled in MessageReceiver
if (it.hasQuote()) quote = Quote.fromProto(it.quote)
linkPreview = it.previewList.firstOrNull()?.let(LinkPreview::fromProto)
if (it.hasOpenGroupInvitation()) openGroupInvitation = it.openGroupInvitation?.let(OpenGroupInvitation::fromProto)
// TODO Contact
profile = Profile.fromProto(it)
if (it.hasReaction()) reaction = it.reaction?.let(Reaction::fromProto)
blocksMessageRequests = it.hasBlocksCommunityMessageRequests() && it.blocksCommunityMessageRequests
}.copyExpiration(proto)
}
}
override fun toProto(): SignalServiceProtos.Content? {
val proto = SignalServiceProtos.Content.newBuilder()
val dataMessage: SignalServiceProtos.DataMessage.Builder
// Profile
val profileProto = profile?.toProto()
dataMessage = if (profileProto != null) {
profileProto.toBuilder()
} else {
SignalServiceProtos.DataMessage.newBuilder()
}
val dataMessage = profile?.toProto()?.toBuilder() ?: SignalServiceProtos.DataMessage.newBuilder()
// Text
if (text != null) { dataMessage.body = text }
// Quote
@ -124,7 +98,7 @@ data class VisibleMessage(
dataMessage.addAllAttachments(pointers)
// TODO: Contact
// Expiration timer
proto.setExpirationConfigurationIfNeeded(threadID)
proto.applyExpiryMode()
// Group context
val storage = MessagingModuleConfiguration.shared.storage
if (storage.isClosedGroup(recipient!!)) {
@ -163,4 +137,4 @@ data class VisibleMessage(
fun isMediaMessage(): Boolean {
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || reaction != null
}
}
}

@ -145,9 +145,7 @@ object MessageReceiver {
MessageRequestResponse.fromProto(proto) ?:
CallMessage.fromProto(proto) ?:
SharedConfigurationMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: run {
throw Error.UnknownMessage
}
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
val isUserBlindedSender = sender == openGroupPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString }
val isUserSender = sender == userPublicKey

@ -9,6 +9,7 @@ import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.jobs.NotifyPNServerJob
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.applyExpiryMode
import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
@ -421,12 +422,7 @@ object MessageSender {
storage.markUnidentified(timestamp, userPublicKey)
// Start the disappearing messages timer if needed
Log.d("MessageSender", "Start the disappearing messages timer if needed message.recipient = ${message.recipient}, userPublicKey = $userPublicKey, isSyncMessage = $isSyncMessage")
message.threadID?.let(storage::getExpirationConfiguration)?.expiryMode?.takeIf { it.expirySeconds > 0 }?.let { mode ->
if (message.recipient == userPublicKey || !isSyncMessage) {
val expireStartedAt = if (mode is ExpiryMode.AfterRead) timestamp + 1 else timestamp
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(timestamp, userPublicKey, expireStartedAt)
}
}
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
} ?: run {
storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp)
}
@ -475,6 +471,7 @@ object MessageSender {
@JvmStatic
fun send(message: Message, address: Address) {
message.applyExpiryMode()
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
message.threadID = threadID
val destination = Destination.from(address)

@ -124,7 +124,6 @@ private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
private fun MessageReceiver.handleCallMessage(message: CallMessage) {
// TODO: refactor this out to persistence, just to help debug the flow and send/receive in synchronous testing
WebRtcUtils.SIGNAL_QUEUE.trySend(message)
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message, coerceToDisappearAfterRead = true)
}
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
@ -192,7 +191,6 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac
else -> return
}
storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!)
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message, coerceToDisappearAfterRead = true)
}
private fun handleConfigurationMessage(message: ConfigurationMessage) {
@ -221,7 +219,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
} else {
// only handle new closed group if it's first time sync
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!, closedGroup.expirationTimer)
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
}
}
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
@ -431,7 +429,7 @@ fun MessageReceiver.handleVisibleMessage(
val isSms = !message.isMediaMessage() && attachments.isEmpty()
storage.setOpenGroupServerMessageID(messageID, it, threadID, isSms)
}
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message)
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message)
return messageID
}
return null
@ -550,10 +548,10 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
val expirationTimer = kind.expirationTimer
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expirationTimer)
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
}
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long, expireTimer: Int) {
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!

@ -3,11 +3,12 @@ package org.session.libsession.utilities
import android.content.Context
import android.util.Log
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeAPI.nowWithOffset
import org.session.libsession.utilities.recipients.Recipient
class SSKEnvironment(
@ -43,18 +44,41 @@ class SSKEnvironment(
interface MessageExpirationManagerProtocol {
fun insertExpirationTimerMessage(message: ExpirationTimerUpdate)
fun startAnyExpiration(timestamp: Long, author: String, expireStartedAt: Long)
fun startAnyExpiration(message: Message, coerceToDisappearAfterRead: Boolean = false) {
Log.d("MessageExpirationManagerProtocol", "startAnyExpiration() called with: message = $message, coerceToDisappearAfterRead = $coerceToDisappearAfterRead")
val timestamp = message.sentTimestamp ?: return
fun maybeStartExpiration(message: Message, startDisappearAfterRead: Boolean = false) {
Log.d("MessageExpirationManagerProtocol", "maybeStartExpiration() called with: message = $message, startDisappearAfterRead = $startDisappearAfterRead")
if (message is ExpirationTimerUpdate && message.isGroup) return
maybeStartExpiration(
message.sentTimestamp ?: return,
message.sender ?: return,
message.expiryMode,
startDisappearAfterRead || message.isSenderSelf
)
}
fun startDisappearAfterRead(timestamp: Long, sender: String) {
Log.d("MessageExpirationManagerProtocol", "startDisappearAfterRead() called with: timestamp = $timestamp, sender = $sender")
startAnyExpiration(
timestamp = timestamp,
author = message.sender ?: return,
expireStartedAt = if (message.expiryMode is ExpiryMode.AfterRead || coerceToDisappearAfterRead && message.expiryMode.expiryMillis > 0) SnodeAPI.nowWithOffset.coerceAtLeast(timestamp + 1)
else if (message.expiryMode is ExpiryMode.AfterSend) timestamp
else return
timestamp,
sender,
expireStartedAt = nowWithOffset.coerceAtLeast(timestamp + 1)
)
}
fun maybeStartExpiration(timestamp: Long, sender: String, mode: ExpiryMode, startDisappearAfterRead: Boolean = false) {
Log.d("MessageExpirationManagerProtocol", "maybeStartExpiration() called with: timestamp = $timestamp, sender = $sender, mode = $mode, startDisappearAfterRead = $startDisappearAfterRead")
val expireStartedAt = when (mode) {
is ExpiryMode.AfterSend -> timestamp
is ExpiryMode.AfterRead -> if (startDisappearAfterRead) nowWithOffset.coerceAtLeast(timestamp + 1) else return
else -> return
}
startAnyExpiration(timestamp, sender, expireStartedAt)
}
}
companion object {

Loading…
Cancel
Save