mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 10:05:15 +00:00
Update expiry configuration
This commit is contained in:
parent
b529d6d341
commit
6eba3ac8af
@ -12,6 +12,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.BuildConfig
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityExpirationSettingsBinding
|
||||
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)}
|
||||
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)}
|
||||
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) {
|
||||
|
@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.Job
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
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.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
@ -961,4 +962,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,6 @@
|
||||
<item
|
||||
android:id="@+id/menu_overflow"
|
||||
android:icon="@drawable/ic_outline_settings_24"
|
||||
android:title="@string/conversation_context__menu_call"
|
||||
app:showAsAction="always">
|
||||
|
||||
<menu>
|
||||
|
@ -8,6 +8,7 @@ import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
import org.session.libsession.messaging.jobs.Job
|
||||
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.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
@ -144,7 +145,7 @@ interface StorageProtocol {
|
||||
|
||||
// Thread
|
||||
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(address: Address): Long?
|
||||
fun getThreadId(recipient: Recipient): Long?
|
||||
@ -198,4 +199,7 @@ interface StorageProtocol {
|
||||
fun deleteReactions(messageId: Long, mms: Boolean)
|
||||
fun unblock(toUnblock: List<Recipient>)
|
||||
fun blockedContacts(): List<Recipient>
|
||||
fun getExpirationSettingsConfiguration(threadId: Long): ExpirationSettingsConfiguration?
|
||||
fun addExpirationSettingsConfiguration(config: ExpirationSettingsConfiguration)
|
||||
fun getExpiringMessages(messageIds: LongArray): List<Pair<String, Int>>
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
@ -14,8 +15,10 @@ abstract class Message {
|
||||
var groupPublicKey: String? = null
|
||||
var openGroupServerMessageID: Long? = 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 fun isValid(): Boolean {
|
||||
@ -36,4 +39,13 @@ abstract class Message {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -14,10 +14,10 @@ class CallMessage(): ControlMessage() {
|
||||
|
||||
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
|
||||
&& (!sdps.isNullOrEmpty() || type in listOf(END_CALL, PRE_OFFER))
|
||||
&& (sdps.isNotEmpty() || type in listOf(END_CALL, PRE_OFFER))
|
||||
|
||||
constructor(type: SignalServiceProtos.CallMessage.Type,
|
||||
sdps: List<String>,
|
||||
@ -81,10 +81,11 @@ class CallMessage(): ControlMessage() {
|
||||
.addAllSdpMids(sdpMids)
|
||||
.setUuid(callId!!.toString())
|
||||
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
.setCallMessage(
|
||||
callMessage
|
||||
)
|
||||
val content = SignalServiceProtos.Content.newBuilder()
|
||||
setExpirationSettingsConfigIfNeeded(content)
|
||||
|
||||
return content
|
||||
.setCallMessage(callMessage)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import org.session.libsignal.utilities.Log
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long get() {
|
||||
override val defaultTtl: Long get() {
|
||||
return when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
|
||||
else -> 14 * 24 * 60 * 60 * 1000
|
||||
@ -167,6 +167,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Expiration timer
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
|
@ -64,6 +64,7 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
contentProto.dataExtractionNotification = dataExtractionNotification.build()
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
return contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
|
||||
|
@ -10,10 +10,11 @@ class MessageRequestResponse(val isApproved: Boolean) : ControlMessage() {
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val messageRequestResponseProto = SignalServiceProtos.MessageRequestResponse.newBuilder()
|
||||
.setIsApproved(isApproved)
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setMessageRequestResponse(messageRequestResponseProto.build())
|
||||
.build()
|
||||
contentProto.messageRequestResponse = messageRequestResponseProto.build()
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct message request response proto from: $this")
|
||||
null
|
||||
|
@ -39,12 +39,13 @@ class ReadReceipt() : ControlMessage() {
|
||||
receiptProto.type = SignalServiceProtos.ReceiptMessage.Type.READ
|
||||
receiptProto.addAllTimestamp(timestamps.asIterable())
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
return try {
|
||||
contentProto.receiptMessage = receiptProto.build()
|
||||
return contentProto.build()
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ import org.session.libsignal.utilities.Log
|
||||
class TypingIndicator() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
|
||||
override val ttl: Long = 20 * 1000
|
||||
override val defaultTtl: Long = 20 * 1000
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
@ -58,12 +58,13 @@ class TypingIndicator() : ControlMessage() {
|
||||
typingIndicatorProto.timestamp = timestamp
|
||||
typingIndicatorProto.action = kind.toProto()
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
return try {
|
||||
contentProto.typingMessage = typingIndicatorProto.build()
|
||||
return contentProto.build()
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct typing indicator proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -43,12 +43,13 @@ class UnsendRequest(): ControlMessage() {
|
||||
unsendRequestProto.timestamp = timestamp
|
||||
unsendRequestProto.author = author
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
return try {
|
||||
contentProto.unsendRequest = unsendRequestProto.build()
|
||||
return contentProto.build()
|
||||
setExpirationSettingsConfigIfNeeded(contentProto)
|
||||
contentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,6 @@ import com.goterl.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
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.utilities.Log
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
@ -119,17 +116,9 @@ class VisibleMessage : Message() {
|
||||
dataMessage.addAllAttachments(pointers)
|
||||
// TODO: Contact
|
||||
// Expiration timer
|
||||
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
|
||||
// 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
|
||||
setExpirationSettingsConfigIfNeeded(proto)
|
||||
// Group context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
|
@ -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.MessageRequestResponse
|
||||
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.UnsendRequest
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
@ -138,6 +139,7 @@ object MessageReceiver {
|
||||
UnsendRequest.fromProto(proto) ?:
|
||||
MessageRequestResponse.fromProto(proto) ?:
|
||||
CallMessage.fromProto(proto) ?:
|
||||
SyncedExpiriesMessage.fromProto(proto) ?:
|
||||
VisibleMessage.fromProto(proto) ?: run {
|
||||
throw Error.UnknownMessage
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.crypto.PushTransportDetails
|
||||
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.IdPrefix
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
@ -171,7 +172,12 @@ object MessageSender {
|
||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||
// Send the result
|
||||
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) {
|
||||
SnodeModule.shared.broadcaster.broadcast("sendingMessage", messageSendTime)
|
||||
}
|
||||
@ -214,6 +220,19 @@ object MessageSender {
|
||||
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
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
|
@ -5,6 +5,7 @@ import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
|
||||
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.control.CallMessage
|
||||
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.MessageRequestResponse
|
||||
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.UnsendRequest
|
||||
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?) {
|
||||
updateExpirationSettingsConfigIfNeeded(message, proto, openGroupID)
|
||||
when (message) {
|
||||
is ReadReceipt -> handleReadReceipt(message)
|
||||
is TypingIndicator -> handleTypingIndicator(message)
|
||||
@ -73,9 +76,30 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
runProfileUpdate = true
|
||||
)
|
||||
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
|
||||
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
@ -87,6 +111,19 @@ private fun MessageReceiver.handleCallMessage(message: CallMessage) {
|
||||
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) {
|
||||
when (message.kind!!) {
|
||||
TypingIndicator.Kind.STARTED -> showTypingIndicatorIfNeeded(message.sender!!)
|
||||
|
@ -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?>> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
return if (messages != null) {
|
||||
|
@ -59,11 +59,26 @@ message Content {
|
||||
optional ExpirationType expirationType = 11;
|
||||
optional uint32 expirationTimer = 12;
|
||||
optional uint64 lastDisappearingMessageChangeTimestamp = 13;
|
||||
optional SyncedExpiries syncedExpiries = 14;
|
||||
}
|
||||
|
||||
message SyncedExpiry {
|
||||
required string serverHash = 1;
|
||||
required uint64 expirationTimestamp = 2;
|
||||
message SyncedExpiries {
|
||||
|
||||
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 {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,8 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
||||
DeleteMessage("delete"),
|
||||
OxenDaemonRPCCall("oxend_request"),
|
||||
Info("info"),
|
||||
DeleteAll("delete_all")
|
||||
DeleteAll("delete_all"),
|
||||
Expire("expire")
|
||||
}
|
||||
|
||||
data class KeySet(val ed25519Key: String, val x25519Key: String)
|
||||
|
Loading…
Reference in New Issue
Block a user