Update expiry configuration

This commit is contained in:
charles 2022-11-16 10:45:58 +11:00
parent b529d6d341
commit 6eba3ac8af
24 changed files with 2623 additions and 388 deletions

View File

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityExpirationSettingsBinding import network.loki.messenger.databinding.ActivityExpirationSettingsBinding
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
@ -41,7 +42,15 @@ class ExpirationSettingsActivity: PassphraseRequiredActionBarActivity() {
.zip(resources.getStringArray(R.array.read_expiration_time_names)) { value, name -> RadioOption(value, name)} .zip(resources.getStringArray(R.array.read_expiration_time_names)) { value, name -> RadioOption(value, name)}
val afterSendOptions = resources.getIntArray(R.array.send_expiration_time_values).map(Int::toString) val afterSendOptions = resources.getIntArray(R.array.send_expiration_time_values).map(Int::toString)
.zip(resources.getStringArray(R.array.send_expiration_time_names)) { value, name -> RadioOption(value, name)} .zip(resources.getStringArray(R.array.send_expiration_time_names)) { value, name -> RadioOption(value, name)}
viewModelFactory.create(threadId, afterReadOptions, afterSendOptions) viewModelFactory.create(threadId, mayAddTestExpiryOption(afterReadOptions), mayAddTestExpiryOption(afterSendOptions))
}
private fun mayAddTestExpiryOption(expiryOptions: List<RadioOption>): List<RadioOption> {
return if (BuildConfig.DEBUG) {
val options = expiryOptions.toMutableList()
options.add(1, RadioOption("60", "1 Minute"))
options
} else expiryOptions
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {

View File

@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.ExpirationSettingsConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
@ -961,4 +962,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return recipientDb.blockedContacts return recipientDb.blockedContacts
} }
override fun getExpirationSettingsConfiguration(threadId: Long): ExpirationSettingsConfiguration? {
return null
}
override fun addExpirationSettingsConfiguration(config: ExpirationSettingsConfiguration) {
}
override fun getExpiringMessages(messageIds: LongArray): List<Pair<String, Int>> {
return emptyList()
}
} }

View File

@ -6,7 +6,6 @@
<item <item
android:id="@+id/menu_overflow" android:id="@+id/menu_overflow"
android:icon="@drawable/ic_outline_settings_24" android:icon="@drawable/ic_outline_settings_24"
android:title="@string/conversation_context__menu_call"
app:showAsAction="always"> app:showAsAction="always">
<menu> <menu>

View File

@ -8,6 +8,7 @@ import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.ExpirationSettingsConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
@ -144,7 +145,7 @@ interface StorageProtocol {
// Thread // Thread
fun getOrCreateThreadIdFor(address: Address): Long fun getOrCreateThreadIdFor(address: Address): Long
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String? = null, openGroupID: String? = null): Long
fun getThreadId(publicKeyOrOpenGroupID: String): Long? fun getThreadId(publicKeyOrOpenGroupID: String): Long?
fun getThreadId(address: Address): Long? fun getThreadId(address: Address): Long?
fun getThreadId(recipient: Recipient): Long? fun getThreadId(recipient: Recipient): Long?
@ -198,4 +199,7 @@ interface StorageProtocol {
fun deleteReactions(messageId: Long, mms: Boolean) fun deleteReactions(messageId: Long, mms: Boolean)
fun unblock(toUnblock: List<Recipient>) fun unblock(toUnblock: List<Recipient>)
fun blockedContacts(): List<Recipient> fun blockedContacts(): List<Recipient>
fun getExpirationSettingsConfiguration(threadId: Long): ExpirationSettingsConfiguration?
fun addExpirationSettingsConfiguration(config: ExpirationSettingsConfiguration)
fun getExpiringMessages(messageIds: LongArray): List<Pair<String, Int>>
} }

View File

@ -0,0 +1,64 @@
package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.control.SyncedExpiriesMessage
import org.session.libsession.messaging.messages.control.SyncedExpiry
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
class DisappearingMessagesJob(val messageIds: LongArray, val startedAtMs: Long): Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 1
override fun execute() {
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: return
val module = MessagingModuleConfiguration.shared
try {
module.storage.getExpiringMessages(messageIds).groupBy { it.second }.forEach { (expiresInSeconds, messages) ->
val serverHashes = messages.map { it.first }
if (serverHashes.isEmpty()) return
val expirationTimestamp = startedAtMs + expiresInSeconds * 1000
val syncTarget = ""
val syncedExpiriesMessage = SyncedExpiriesMessage()
syncedExpiriesMessage.conversationExpiries = mapOf(
syncTarget to serverHashes.map { serverHash -> SyncedExpiry(serverHash, expirationTimestamp) }
)
MessageSender.send(syncedExpiriesMessage, Address.fromSerialized(userPublicKey))
SnodeAPI.updateExpiry(expirationTimestamp, serverHashes)
}
} catch (e: Exception) {
delegate?.handleJobFailed(this, e)
return
}
delegate?.handleJobSucceeded(this)
}
override fun serialize(): Data = Data.Builder()
.putLongArray(MESSAGE_IDS, messageIds)
.putLong(STARTED_AT_MS, startedAtMs)
.build()
override fun getFactoryKey(): String = KEY
class Factory : Job.Factory<DisappearingMessagesJob> {
override fun create(data: Data): DisappearingMessagesJob {
return DisappearingMessagesJob(
data.getLongArray(MESSAGE_IDS),
data.getLong(STARTED_AT_MS)
)
}
}
companion object {
const val KEY = "DisappearingMessagesJob"
private const val MESSAGE_IDS = "messageIds"
private const val STARTED_AT_MS = "startedAtMs"
}
}

View File

@ -0,0 +1,11 @@
package org.session.libsession.messaging.messages
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
class ExpirationSettingsConfiguration(
val threadId: Long = -1,
val isEnabled: Boolean = false,
val durationSeconds: Int = 0,
val expirationType: ExpirationType? = null,
val lastChangeTimestampMs: Long = 0
)

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.messages package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
@ -14,8 +15,10 @@ abstract class Message {
var groupPublicKey: String? = null var groupPublicKey: String? = null
var openGroupServerMessageID: Long? = null var openGroupServerMessageID: Long? = null
var serverHash: String? = null var serverHash: String? = null
var specifiedTtl: Long? = null
open val ttl: Long = 14 * 24 * 60 * 60 * 1000 open val defaultTtl: Long = 14 * 24 * 60 * 60 * 1000
val ttl: Long get() = specifiedTtl ?: defaultTtl
open val isSelfSendValid: Boolean = false open val isSelfSendValid: Boolean = false
open fun isValid(): Boolean { open fun isValid(): Boolean {
@ -36,4 +39,13 @@ abstract class Message {
dataMessage.group = groupProto.build() dataMessage.group = groupProto.build()
} }
fun setExpirationSettingsConfigIfNeeded(builder: SignalServiceProtos.Content.Builder) {
val threadId = threadID ?: return
val config = MessagingModuleConfiguration.shared.storage.getExpirationSettingsConfiguration(threadId) ?: return
builder.expirationTimer = config.durationSeconds
if (config.isEnabled) {
builder.expirationType = config.expirationType
builder.lastDisappearingMessageChangeTimestamp = config.lastChangeTimestampMs
}
}
} }

View File

@ -14,10 +14,10 @@ class CallMessage(): ControlMessage() {
override val isSelfSendValid: Boolean get() = type in arrayOf(ANSWER, END_CALL) override val isSelfSendValid: Boolean get() = type in arrayOf(ANSWER, END_CALL)
override val ttl: Long = 300000L // 5m override val defaultTtl: Long = 300000L // 5m
override fun isValid(): Boolean = super.isValid() && type != null && callId != null override fun isValid(): Boolean = super.isValid() && type != null && callId != null
&& (!sdps.isNullOrEmpty() || type in listOf(END_CALL, PRE_OFFER)) && (sdps.isNotEmpty() || type in listOf(END_CALL, PRE_OFFER))
constructor(type: SignalServiceProtos.CallMessage.Type, constructor(type: SignalServiceProtos.CallMessage.Type,
sdps: List<String>, sdps: List<String>,
@ -81,10 +81,11 @@ class CallMessage(): ControlMessage() {
.addAllSdpMids(sdpMids) .addAllSdpMids(sdpMids)
.setUuid(callId!!.toString()) .setUuid(callId!!.toString())
return SignalServiceProtos.Content.newBuilder() val content = SignalServiceProtos.Content.newBuilder()
.setCallMessage( setExpirationSettingsConfigIfNeeded(content)
callMessage
) return content
.setCallMessage(callMessage)
.build() .build()
} }

View File

@ -14,7 +14,7 @@ import org.session.libsignal.utilities.Log
class ClosedGroupControlMessage() : ControlMessage() { class ClosedGroupControlMessage() : ControlMessage() {
var kind: Kind? = null var kind: Kind? = null
override val ttl: Long get() { override val defaultTtl: Long get() {
return when (kind) { return when (kind) {
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000 is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
else -> 14 * 24 * 60 * 60 * 1000 else -> 14 * 24 * 60 * 60 * 1000
@ -167,6 +167,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
val contentProto = SignalServiceProtos.Content.newBuilder() val contentProto = SignalServiceProtos.Content.newBuilder()
val dataMessageProto = DataMessage.newBuilder() val dataMessageProto = DataMessage.newBuilder()
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build() dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
// Expiration timer
setExpirationSettingsConfigIfNeeded(contentProto)
// Group context // Group context
setGroupContext(dataMessageProto) setGroupContext(dataMessageProto)
contentProto.dataMessage = dataMessageProto.build() contentProto.dataMessage = dataMessageProto.build()

View File

@ -64,6 +64,7 @@ class DataExtractionNotification() : ControlMessage() {
} }
val contentProto = SignalServiceProtos.Content.newBuilder() val contentProto = SignalServiceProtos.Content.newBuilder()
contentProto.dataExtractionNotification = dataExtractionNotification.build() contentProto.dataExtractionNotification = dataExtractionNotification.build()
setExpirationSettingsConfigIfNeeded(contentProto)
return contentProto.build() return contentProto.build()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this") Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")

View File

@ -10,10 +10,11 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {
val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder() val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
.setIsApproved(isApproved) .setIsApproved(isApproved)
val contentProto = SignalServiceProtos.Content.newBuilder()
return try { return try {
SignalServiceProtos.Content.newBuilder() contentProto.messageRequestResponse = messageRequestResponseProto.build()
.setMessageRequestResponse(messageRequestResponseProto.build()) setExpirationSettingsConfigIfNeeded(contentProto)
.build() contentProto.build()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't construct message request response proto from: $this") Log.w(TAG, "Couldn't construct message request response proto from: $this")
null null

View File

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

View File

@ -0,0 +1,61 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.SyncedExpiries.SyncedConversationExpiries
import org.session.libsignal.utilities.Log
class SyncedExpiriesMessage(): ControlMessage() {
var conversationExpiries: Map<String, List<SyncedExpiry>> = emptyMap()
override val isSelfSendValid: Boolean = true
// region Validation
override fun isValid(): Boolean {
if (!super.isValid()) return false
return conversationExpiries.isNotEmpty()
}
// endregion
companion object {
const val TAG = "SyncedExpiriesMessage"
fun fromProto(proto: SignalServiceProtos.Content): SyncedExpiriesMessage? {
val syncedExpiriesProto = if (proto.hasSyncedExpiries()) proto.syncedExpiries else return null
val conversationExpiries = syncedExpiriesProto.conversationExpiriesList.associate {
it.syncTarget to it.expiriesList.map { syncedExpiry -> SyncedExpiry.fromProto(syncedExpiry) }
}
return SyncedExpiriesMessage(conversationExpiries)
}
}
constructor(conversationExpiries: Map<String, List<SyncedExpiry>>) : this() {
this.conversationExpiries = conversationExpiries
}
override fun toProto(): SignalServiceProtos.Content? {
val conversationExpiries = conversationExpiries
if (conversationExpiries.isEmpty()) {
Log.w(TAG, "Couldn't construct synced expiries proto from: $this")
return null
}
val conversationExpiriesProto = conversationExpiries.map { (syncTarget, syncedExpiries) ->
val expiriesProto = syncedExpiries.map(SyncedExpiry::toProto)
val syncedConversationExpiriesProto = SyncedConversationExpiries.newBuilder()
syncedConversationExpiriesProto.syncTarget = syncTarget
syncedConversationExpiriesProto.addAllExpiries(expiriesProto)
syncedConversationExpiriesProto.build()
}
val syncedExpiriesProto = SignalServiceProtos.SyncedExpiries.newBuilder()
syncedExpiriesProto.addAllConversationExpiries(conversationExpiriesProto)
val contentProto = SignalServiceProtos.Content.newBuilder()
return try {
contentProto.syncedExpiries = syncedExpiriesProto.build()
setExpirationSettingsConfigIfNeeded(contentProto)
contentProto.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct synced expiries proto from: $this")
null
}
}
}

View File

@ -0,0 +1,35 @@
package org.session.libsession.messaging.messages.control
import org.session.libsignal.protos.SignalServiceProtos.SyncedExpiries
import org.session.libsignal.utilities.Log
class SyncedExpiry(
var serverHash: String? = null,
var expirationTimestamp: Long? = null
) {
fun toProto(): SyncedExpiries.SyncedConversationExpiries.SyncedExpiry? {
val syncedExpiryProto = SyncedExpiries.SyncedConversationExpiries.SyncedExpiry.newBuilder()
serverHash?.let { syncedExpiryProto.serverHash = it }
expirationTimestamp?.let { syncedExpiryProto.expirationTimestamp = it }
return try {
syncedExpiryProto.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct synced expiry proto from: $this")
null
}
}
companion object {
const val TAG = "SyncedExpiry"
@JvmStatic
fun fromProto(proto: SyncedExpiries.SyncedConversationExpiries.SyncedExpiry): SyncedExpiry {
val result = SyncedExpiry()
result.serverHash = if (proto.hasServerHash()) proto.serverHash else null
result.expirationTimestamp = if (proto.hasServerHash()) proto.expirationTimestamp else null
return SyncedExpiry()
}
}
}

View File

@ -6,7 +6,7 @@ import org.session.libsignal.utilities.Log
class TypingIndicator() : ControlMessage() { class TypingIndicator() : ControlMessage() {
var kind: Kind? = null var kind: Kind? = null
override val ttl: Long = 20 * 1000 override val defaultTtl: Long = 20 * 1000
override fun isValid(): Boolean { override fun isValid(): Boolean {
if (!super.isValid()) return false if (!super.isValid()) return false
@ -58,12 +58,13 @@ class TypingIndicator() : ControlMessage() {
typingIndicatorProto.timestamp = timestamp typingIndicatorProto.timestamp = timestamp
typingIndicatorProto.action = kind.toProto() typingIndicatorProto.action = kind.toProto()
val contentProto = SignalServiceProtos.Content.newBuilder() val contentProto = SignalServiceProtos.Content.newBuilder()
try { return try {
contentProto.typingMessage = typingIndicatorProto.build() contentProto.typingMessage = typingIndicatorProto.build()
return contentProto.build() setExpirationSettingsConfigIfNeeded(contentProto)
contentProto.build()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't construct typing indicator proto from: $this") Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
return null null
} }
} }
} }

View File

@ -43,12 +43,13 @@ class UnsendRequest(): ControlMessage() {
unsendRequestProto.timestamp = timestamp unsendRequestProto.timestamp = timestamp
unsendRequestProto.author = author unsendRequestProto.author = author
val contentProto = SignalServiceProtos.Content.newBuilder() val contentProto = SignalServiceProtos.Content.newBuilder()
try { return try {
contentProto.unsendRequest = unsendRequestProto.build() contentProto.unsendRequest = unsendRequestProto.build()
return contentProto.build() setExpirationSettingsConfigIfNeeded(contentProto)
contentProto.build()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't construct unsend request proto from: $this") Log.w(TAG, "Couldn't construct unsend request proto from: $this")
return null null
} }
} }

View File

@ -4,9 +4,6 @@ import com.goterl.lazysodium.BuildConfig
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
@ -119,17 +116,9 @@ class VisibleMessage : Message() {
dataMessage.addAllAttachments(pointers) dataMessage.addAllAttachments(pointers)
// TODO: Contact // TODO: Contact
// Expiration timer // Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation setExpirationSettingsConfigIfNeeded(proto)
// if it receives a message without the current expiration timer value attached to it...
val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context
val expiration = if (storage.isClosedGroup(recipient!!)) {
Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
} else {
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
}
dataMessage.expireTimer = expiration
// Group context // Group context
val storage = MessagingModuleConfiguration.shared.storage
if (storage.isClosedGroup(recipient!!)) { if (storage.isClosedGroup(recipient!!)) {
try { try {
setGroupContext(dataMessage) setGroupContext(dataMessage)

View File

@ -9,6 +9,7 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.SyncedExpiriesMessage
import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
@ -138,6 +139,7 @@ object MessageReceiver {
UnsendRequest.fromProto(proto) ?: UnsendRequest.fromProto(proto) ?:
MessageRequestResponse.fromProto(proto) ?: MessageRequestResponse.fromProto(proto) ?:
CallMessage.fromProto(proto) ?: CallMessage.fromProto(proto) ?:
SyncedExpiriesMessage.fromProto(proto) ?:
VisibleMessage.fromProto(proto) ?: run { VisibleMessage.fromProto(proto) ?: run {
throw Error.UnknownMessage throw Error.UnknownMessage
} }

View File

@ -32,6 +32,7 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
@ -171,7 +172,12 @@ object MessageSender {
val base64EncodedData = Base64.encodeBytes(wrappedMessage) val base64EncodedData = Base64.encodeBytes(wrappedMessage)
// Send the result // Send the result
val timestamp = messageSendTime + SnodeAPI.clockOffset val timestamp = messageSendTime + SnodeAPI.clockOffset
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, timestamp) val snodeMessage = SnodeMessage(
recipient = message.recipient!!,
data = base64EncodedData,
ttl = getSpecifiedTtl(message, isSyncMessage) ?: message.ttl,
timestamp = timestamp
)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime) SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime)
} }
@ -214,6 +220,19 @@ object MessageSender {
return promise return promise
} }
private fun getSpecifiedTtl(message: Message, isSyncMessage: Boolean): Long? {
val storage = MessagingModuleConfiguration.shared.storage
val threadId = message.threadID
?: run {
val address = if (isSyncMessage && message is VisibleMessage) message.syncTarget else message.recipient
storage.getOrCreateThreadIdFor(address!!)
}
val config = storage.getExpirationSettingsConfiguration(threadId) ?: return null
return if (config.isEnabled && (config.expirationType == ExpirationType.DELETE_AFTER_SEND || isSyncMessage)) {
config.durationSeconds * 1000L
} else null
}
// Open Groups // Open Groups
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> { private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()

View File

@ -5,6 +5,7 @@ import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.ExpirationSettingsConfiguration
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.CallMessage import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
@ -13,6 +14,7 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.SyncedExpiriesMessage
import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
@ -58,6 +60,7 @@ internal fun MessageReceiver.isBlocked(publicKey: String): Boolean {
} }
fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) { fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) {
updateExpirationSettingsConfigIfNeeded(message, proto, openGroupID)
when (message) { when (message) {
is ReadReceipt -> handleReadReceipt(message) is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message) is TypingIndicator -> handleTypingIndicator(message)
@ -73,9 +76,30 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
runProfileUpdate = true runProfileUpdate = true
) )
is CallMessage -> handleCallMessage(message) is CallMessage -> handleCallMessage(message)
is SyncedExpiriesMessage -> handleSyncedExpiriesMessage(message)
} }
} }
fun updateExpirationSettingsConfigIfNeeded(message: Message, proto: SignalServiceProtos.Content, openGroupID: String?) {
if (!proto.hasLastDisappearingMessageChangeTimestamp()) return
val storage = MessagingModuleConfiguration.shared.storage
val threadID = storage.getOrCreateThreadIdFor(message.sender!!, message.groupPublicKey, openGroupID)
if (threadID <= 0) return
val localConfig = storage.getExpirationSettingsConfiguration(threadID)
if (localConfig == null || localConfig.lastChangeTimestampMs < proto.lastDisappearingMessageChangeTimestamp) return
val durationSeconds = if (proto.hasExpirationTimer()) proto.expirationTimer else 0
val isEnabled = durationSeconds != 0
val type = if (proto.hasExpirationType()) proto.expirationType else null
val remoteConfig = ExpirationSettingsConfiguration(
threadID,
isEnabled,
durationSeconds,
type,
proto.lastDisappearingMessageChangeTimestamp
)
storage.addExpirationSettingsConfiguration(remoteConfig)
}
// region Control Messages // region Control Messages
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
@ -87,6 +111,19 @@ private fun MessageReceiver.handleCallMessage(message: CallMessage) {
WebRtcUtils.SIGNAL_QUEUE.trySend(message) WebRtcUtils.SIGNAL_QUEUE.trySend(message)
} }
private fun MessageReceiver.handleSyncedExpiriesMessage(message: SyncedExpiriesMessage) {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey() ?: return
if (userPublicKey != message.sender) return
message.conversationExpiries.forEach { (syncTarget, syncedExpiries) ->
val config = storage.getExpirationSettingsConfiguration(storage.getOrCreateThreadIdFor(syncTarget)) ?: return@forEach
syncedExpiries.forEach { syncedExpiry ->
val startedAtMs = syncedExpiry.expirationTimestamp!! - config.durationSeconds * 1000
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(startedAtMs, syncTarget)
}
}
}
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) { private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
when (message.kind!!) { when (message.kind!!) {
TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!) TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!)

View File

@ -493,6 +493,62 @@ object SnodeAPI {
} }
} }
fun updateExpiry(updatedExpiryMs: Long, serverHashes: List<String>): Promise<Map<String, Pair<List<String>, Long>>, Exception> {
return retryIfNeeded(maxRetryCount) {
val module = MessagingModuleConfiguration.shared
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
val updatedExpiryMsWithNetworkOffset = updatedExpiryMs + clockOffset
getSingleTargetSnode(userPublicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
// "expire" || expiry || messages[0] || ... || messages[N]
val verificationData =
(Snode.Method.Expire.rawValue + updatedExpiryMsWithNetworkOffset + serverHashes.fold("") { a, v -> a + v }).toByteArray()
val signature = ByteArray(Sign.BYTES)
sodium.cryptoSignDetached(
signature,
verificationData,
verificationData.size.toLong(),
userED25519KeyPair.secretKey.asBytes
)
val params = mapOf(
"pubkey" to userPublicKey,
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
"expiry" to updatedExpiryMs,
"messages" to serverHashes,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.Expire, snode, params, userPublicKey).map { rawResponse ->
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
val isFailed = json["failed"] as? Boolean ?: false
val statusCode = json["code"] as? String
val reason = json["reason"] as? String
hexSnodePublicKey to if (isFailed) {
Log.e("Loki", "Failed to update expiry for: $hexSnodePublicKey due to error: $reason ($statusCode).")
listOf<String>() to 0L
} else {
val hashes = json["updated"] as List<String>
val expiryApplied = json["expiry"] as Long
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
val message = (userPublicKey + serverHashes.fold("") { a, v -> a + v } + hashes.fold("") { a, v -> a + v }).toByteArray()
if (sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)) {
hashes to expiryApplied
} else listOf<String>() to 0L
}
}
return@map result.toMap()
}.fail { e ->
Log.e("Loki", "Failed to update expiry", e)
}
}
}
}
}
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> { fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0): List<Pair<SignalServiceProtos.Envelope, String?>> {
val messages = rawResponse["messages"] as? List<*> val messages = rawResponse["messages"] as? List<*>
return if (messages != null) { return if (messages != null) {

View File

@ -59,11 +59,26 @@ message Content {
optional ExpirationType expirationType = 11; optional ExpirationType expirationType = 11;
optional uint32 expirationTimer = 12; optional uint32 expirationTimer = 12;
optional uint64 lastDisappearingMessageChangeTimestamp = 13; optional uint64 lastDisappearingMessageChangeTimestamp = 13;
optional SyncedExpiries syncedExpiries = 14;
} }
message SyncedExpiry { message SyncedExpiries {
required string serverHash = 1;
required uint64 expirationTimestamp = 2; message SyncedConversationExpiries {
message SyncedExpiry {
// @required
required string serverHash = 1; // messageHash for desktop and serverHash for mobile
// @required
required uint64 expirationTimestamp = 2; // this is only used for deleteAfterRead
}
// @required
required string syncTarget = 1; // the conversationID those expiries are related to
repeated SyncedExpiry expiries = 2;
}
repeated SyncedConversationExpiries conversationExpiries = 1;
} }
message KeyPair { message KeyPair {

View File

@ -10,7 +10,8 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
DeleteMessage("delete"), DeleteMessage("delete"),
OxenDaemonRPCCall("oxend_request"), OxenDaemonRPCCall("oxend_request"),
Info("info"), Info("info"),
DeleteAll("delete_all") DeleteAll("delete_all"),
Expire("expire")
} }
data class KeySet(val ed25519Key: String, val x25519Key: String) data class KeySet(val ed25519Key: String, val x25519Key: String)