mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 14:57:45 +00:00
Merge branch 'dev' into fix-attr-ex
This commit is contained in:
@@ -21,7 +21,7 @@ dependencies {
|
||||
implementation project(":libsignal")
|
||||
implementation project(":libsession-util")
|
||||
implementation project(":liblazysodium")
|
||||
implementation "net.java.dev.jna:jna:5.8.0@aar"
|
||||
implementation "net.java.dev.jna:jna:5.12.1@aar"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
@@ -29,8 +29,8 @@ dependencies {
|
||||
implementation "com.google.android.material:material:$materialVersion"
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.annimon:stream:1.1.8'
|
||||
@@ -46,7 +46,7 @@ dependencies {
|
||||
implementation "nl.komponents.kovenant:kovenant:$kovenantVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||
testImplementation "org.mockito:mockito-inline:4.0.0"
|
||||
testImplementation "org.mockito:mockito-inline:4.11.0"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||
testImplementation "androidx.test:core:$testCoreVersion"
|
||||
testImplementation "androidx.arch.core:core-testing:2.1.0"
|
||||
|
@@ -24,7 +24,7 @@ interface MessageDataProvider {
|
||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
|
||||
fun updateMessageAsDeleted(timestamp: Long, author: String): Long?
|
||||
fun getServerHashForMessage(messageID: Long): String?
|
||||
fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
|
||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||
|
@@ -10,6 +10,7 @@ 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.Destination
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.control.MessageRequestResponse
|
||||
@@ -113,7 +114,7 @@ interface StorageProtocol {
|
||||
*/
|
||||
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Pair<Long, Boolean>? // TODO: This is a weird name
|
||||
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
|
||||
fun markAsResyncing(timestamp: Long, author: String)
|
||||
fun markAsSyncing(timestamp: Long, author: String)
|
||||
@@ -123,12 +124,12 @@ interface StorageProtocol {
|
||||
fun markAsSyncFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun markAsSentFailed(timestamp: Long, author: String, error: Exception)
|
||||
fun clearErrorMessage(messageID: Long)
|
||||
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||
fun setMessageServerHash(messageID: Long, mms: Boolean, serverHash: String)
|
||||
|
||||
// Closed Groups
|
||||
fun getGroup(groupID: String): GroupRecord?
|
||||
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
||||
fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair)
|
||||
fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair, expirationTimer: Int)
|
||||
fun updateGroupConfig(groupPublicKey: String)
|
||||
fun isGroupActive(groupPublicKey: String): Boolean
|
||||
fun setActive(groupID: String, value: Boolean)
|
||||
@@ -151,7 +152,6 @@ interface StorageProtocol {
|
||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||
fun updateFormationTimestamp(groupID: String, formationTimestamp: Long)
|
||||
fun updateTimestampUpdated(groupID: String, updatedTimestamp: Long)
|
||||
fun setExpirationTimer(address: String, duration: Int)
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
@@ -159,7 +159,6 @@ interface StorageProtocol {
|
||||
// Settings
|
||||
fun setProfileSharing(address: Address, value: Boolean)
|
||||
|
||||
|
||||
// Thread
|
||||
fun getOrCreateThreadIdFor(address: Address): Long
|
||||
fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long?
|
||||
@@ -176,6 +175,8 @@ interface StorageProtocol {
|
||||
fun isPinned(threadID: Long): Boolean
|
||||
fun deleteConversation(threadID: Long)
|
||||
fun setThreadDate(threadId: Long, newDate: Long)
|
||||
fun getLastLegacyRecipient(threadRecipient: String): String?
|
||||
fun setLastLegacyRecipient(threadRecipient: String, senderRecipient: String?)
|
||||
|
||||
// Contacts
|
||||
fun getContactWithSessionID(sessionID: String): Contact?
|
||||
@@ -183,7 +184,7 @@ interface StorageProtocol {
|
||||
fun setContact(contact: Contact)
|
||||
fun getRecipientForThread(threadId: Long): Recipient?
|
||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||
fun addLibSessionContacts(contacts: List<LibSessionContact>)
|
||||
fun addLibSessionContacts(contacts: List<LibSessionContact>, timestamp: Long)
|
||||
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||
|
||||
// Attachments
|
||||
@@ -224,9 +225,17 @@ interface StorageProtocol {
|
||||
fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
|
||||
fun setRecipientHash(recipient: Recipient, recipientHash: String?)
|
||||
fun blockedContacts(): List<Recipient>
|
||||
fun getExpirationConfiguration(threadId: Long): ExpirationConfiguration?
|
||||
fun setExpirationConfiguration(config: ExpirationConfiguration)
|
||||
fun getExpiringMessages(messageIds: List<Long> = emptyList()): List<Pair<Long, Long>>
|
||||
fun updateDisappearingState(
|
||||
messageSender: String,
|
||||
threadID: Long,
|
||||
disappearingState: Recipient.DisappearingState
|
||||
)
|
||||
|
||||
// Shared configs
|
||||
fun notifyConfigUpdates(forConfigObject: ConfigBase)
|
||||
fun notifyConfigUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||
fun conversationInConfig(publicKey: String?, groupPublicKey: String?, openGroupId: String?, visibleOnly: Boolean): Boolean
|
||||
fun canPerformConfigChange(variant: String, publicKey: String, changeTimestampMs: Long): Boolean
|
||||
fun isCheckingCommunityRequests(): Boolean
|
||||
|
@@ -35,7 +35,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
override val maxFailureCount: Int = 2
|
||||
|
||||
companion object {
|
||||
val KEY: String = "AttachmentDownloadJob"
|
||||
const val KEY: String = "AttachmentDownloadJob"
|
||||
|
||||
// Keys used for database storage
|
||||
private val ATTACHMENT_ID_KEY = "attachment_id"
|
||||
@@ -89,19 +89,21 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
||||
}
|
||||
|
||||
val threadRecipient = storage.getRecipientForThread(threadID)
|
||||
val sender = if (messageDataProvider.isMmsOutgoing(databaseMessageID)) {
|
||||
val selfSend = messageDataProvider.isMmsOutgoing(databaseMessageID)
|
||||
val sender = if (selfSend) {
|
||||
storage.getUserPublicKey()
|
||||
} else {
|
||||
messageDataProvider.getIndividualRecipientForMms(databaseMessageID)?.address?.serialize()
|
||||
}
|
||||
val contact = sender?.let { storage.getContactWithSessionID(it) }
|
||||
if (threadRecipient == null || sender == null || contact == null) {
|
||||
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) {
|
||||
handleFailure(Error.NoSender, null)
|
||||
return
|
||||
}
|
||||
if (!threadRecipient.isGroupRecipient && (!contact.isTrusted && storage.getUserPublicKey() != sender)) {
|
||||
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) {
|
||||
// if we aren't receiving a group message, a message from ourselves (self-send) and the contact sending is not trusted:
|
||||
// do not continue, but do not fail
|
||||
handleFailure(Error.NoSender, null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,7 @@ import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsignal.protos.UtilProtos
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Log
|
||||
import kotlin.math.max
|
||||
|
||||
data class MessageReceiveParameters(
|
||||
val data: ByteArray,
|
||||
@@ -146,7 +147,7 @@ class BatchMessageReceiveJob(
|
||||
// The LinkedHashMap should preserve insertion order
|
||||
val messageIds = linkedMapOf<Long, Pair<Boolean, Boolean>>()
|
||||
val myLastSeen = storage.getLastSeen(threadId)
|
||||
var newLastSeen = if (myLastSeen == -1L) 0 else myLastSeen
|
||||
var newLastSeen = myLastSeen.takeUnless { it == -1L } ?: 0
|
||||
messages.forEach { (parameters, message, proto) ->
|
||||
try {
|
||||
when (message) {
|
||||
@@ -162,18 +163,14 @@ class BatchMessageReceiveJob(
|
||||
IdPrefix.BLINDED, it.publicKey.asBytes
|
||||
).hexString
|
||||
}
|
||||
val sentTimestamp = message.sentTimestamp!!
|
||||
if (message.sender == localUserPublicKey || isUserBlindedSender) {
|
||||
if (sentTimestamp > newLastSeen) {
|
||||
newLastSeen =
|
||||
sentTimestamp // use sent timestamp here since that is technically the last one we have
|
||||
}
|
||||
// use sent timestamp here since that is technically the last one we have
|
||||
newLastSeen = max(newLastSeen, message.sentTimestamp!!)
|
||||
}
|
||||
val messageId = MessageReceiver.handleVisibleMessage(
|
||||
message, proto, openGroupID, threadId,
|
||||
val messageId = MessageReceiver.handleVisibleMessage(message, proto, openGroupID,
|
||||
threadId,
|
||||
runThreadUpdate = false,
|
||||
runProfileUpdate = true
|
||||
)
|
||||
runProfileUpdate = true)
|
||||
|
||||
if (messageId != null && message.reaction == null) {
|
||||
messageIds[messageId] = Pair(
|
||||
@@ -216,9 +213,7 @@ class BatchMessageReceiveJob(
|
||||
// last seen will be the current last seen if not changed (re-computes the read counts for thread record)
|
||||
// might have been updated from a different thread at this point
|
||||
val currentLastSeen = storage.getLastSeen(threadId).let { if (it == -1L) 0 else it }
|
||||
if (currentLastSeen > newLastSeen) {
|
||||
newLastSeen = currentLastSeen
|
||||
}
|
||||
newLastSeen = max(newLastSeen, currentLastSeen)
|
||||
if (newLastSeen > 0 || currentLastSeen == 0L) {
|
||||
storage.markConversationAsRead(threadId, newLastSeen, force = true)
|
||||
}
|
||||
@@ -247,12 +242,12 @@ class BatchMessageReceiveJob(
|
||||
|
||||
private fun handleSuccess(dispatcherName: String) {
|
||||
Log.i(TAG, "Completed processing of ${messages.size} messages (id: $id)")
|
||||
this.delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||
}
|
||||
|
||||
private fun handleFailure(dispatcherName: String) {
|
||||
Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully) (id: $id)")
|
||||
this.delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure"))
|
||||
delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure"))
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
|
@@ -144,7 +144,7 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Unexpected job type.")
|
||||
throw IllegalStateException("Unexpected job type: ${job.getFactoryKey()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,20 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
|
||||
data class ExpirationConfiguration(
|
||||
val threadId: Long = -1,
|
||||
val expiryMode: ExpiryMode = ExpiryMode.NONE,
|
||||
val updatedTimestampMs: Long = 0
|
||||
) {
|
||||
val isEnabled = expiryMode.expirySeconds > 0
|
||||
|
||||
companion object {
|
||||
val isNewConfigEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
data class ExpirationDatabaseMetadata(
|
||||
val threadId: Long = -1,
|
||||
val updatedTimestampMs: Long
|
||||
)
|
@@ -1,11 +1,14 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
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.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.Content.ExpirationType
|
||||
|
||||
abstract class Message {
|
||||
var id: Long? = null
|
||||
@@ -18,8 +21,14 @@ 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
|
||||
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
|
||||
|
||||
companion object {
|
||||
@@ -33,22 +42,54 @@ abstract class Message {
|
||||
}
|
||||
}
|
||||
|
||||
open fun isValid(): Boolean {
|
||||
val sentTimestamp = sentTimestamp
|
||||
if (sentTimestamp != null && sentTimestamp <= 0) { return false }
|
||||
val receivedTimestamp = receivedTimestamp
|
||||
if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
|
||||
return sender != null && recipient != null
|
||||
}
|
||||
open fun isValid(): Boolean =
|
||||
sentTimestamp?.let { it > 0 } != false
|
||||
&& receivedTimestamp?.let { it > 0 } != false
|
||||
&& sender != null
|
||||
&& recipient != null
|
||||
|
||||
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()
|
||||
fun SignalServiceProtos.DataMessage.Builder.setGroupContext() {
|
||||
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.applyExpiryMode() = apply {
|
||||
expirationTimer = expiryMode.expirySeconds.toInt()
|
||||
expirationType = when (expiryMode) {
|
||||
is ExpiryMode.AfterSend -> ExpirationType.DELETE_AFTER_SEND
|
||||
is ExpiryMode.AfterRead -> ExpirationType.DELETE_AFTER_READ
|
||||
else -> ExpirationType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified M: Message> M.copyExpiration(proto: SignalServiceProtos.Content): M = apply {
|
||||
(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(thread: Long): M = apply {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
expiryMode = storage.getExpirationConfiguration(thread)?.expiryMode?.coerceSendToRead(coerceDisappearAfterSendToRead) ?: ExpiryMode.NONE
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
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.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
@@ -12,12 +14,13 @@ 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 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>,
|
||||
@@ -64,7 +67,8 @@ class CallMessage(): ControlMessage() {
|
||||
val sdpMLineIndexes = callMessageProto.sdpMLineIndexesList
|
||||
val sdpMids = callMessageProto.sdpMidsList
|
||||
val callId = UUID.fromString(callMessageProto.uuid)
|
||||
return CallMessage(type,sdps, sdpMLineIndexes, sdpMids, callId)
|
||||
return CallMessage(type, sdps, sdpMLineIndexes, sdpMids, callId)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +86,8 @@ class CallMessage(): ControlMessage() {
|
||||
.setUuid(callId!!.toString())
|
||||
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
.setCallMessage(
|
||||
callMessage
|
||||
)
|
||||
.applyExpiryMode()
|
||||
.setCallMessage(callMessage)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@@ -6,15 +6,22 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
|
||||
class ClosedGroupControlMessage() : ControlMessage() {
|
||||
var kind: Kind? = null
|
||||
var groupID: String? = 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
|
||||
@@ -76,50 +83,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
companion object {
|
||||
const val TAG = "ClosedGroupControlMessage"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||
val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
|
||||
val kind: Kind
|
||||
when (closedGroupControlMessageProto.type!!) {
|
||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey ?: return null
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
|
||||
val expirationTimer = closedGroupControlMessageProto.expirationTimer
|
||||
try {
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList, expirationTimer)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> {
|
||||
val publicKey = closedGroupControlMessageProto.publicKey
|
||||
val wrappers = closedGroupControlMessageProto.wrappersList.mapNotNull { KeyPairWrapper.fromProto(it) }
|
||||
kind = Kind.EncryptionKeyPair(publicKey, wrappers)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
|
||||
val name = closedGroupControlMessageProto.name ?: return null
|
||||
kind = Kind.NameChange(name)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> {
|
||||
kind = Kind.MembersAdded(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED -> {
|
||||
kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList)
|
||||
}
|
||||
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
|
||||
kind = Kind.MemberLeft()
|
||||
}
|
||||
}
|
||||
return ClosedGroupControlMessage(kind)
|
||||
}
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? =
|
||||
proto.takeIf { it.hasDataMessage() }?.dataMessage
|
||||
?.takeIf { it.hasClosedGroupControlMessage() }?.closedGroupControlMessage
|
||||
?.run {
|
||||
when (type) {
|
||||
NEW -> takeIf { it.hasPublicKey() && it.hasEncryptionKeyPair() && it.hasName() }?.let {
|
||||
ECKeyPair(
|
||||
DjbECPublicKey(encryptionKeyPair.publicKey.toByteArray()),
|
||||
DjbECPrivateKey(encryptionKeyPair.privateKey.toByteArray())
|
||||
).let { Kind.New(publicKey, name, it, membersList, adminsList, expirationTimer) }
|
||||
}
|
||||
ENCRYPTION_KEY_PAIR -> Kind.EncryptionKeyPair(publicKey, wrappersList.mapNotNull(KeyPairWrapper::fromProto))
|
||||
NAME_CHANGE -> takeIf { it.hasName() }?.let { Kind.NameChange(name) }
|
||||
MEMBERS_ADDED -> Kind.MembersAdded(membersList)
|
||||
MEMBERS_REMOVED -> Kind.MembersRemoved(membersList)
|
||||
MEMBER_LEFT -> Kind.MemberLeft()
|
||||
else -> null
|
||||
}?.let(::ClosedGroupControlMessage)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(kind: Kind?) : this() {
|
||||
internal constructor(kind: Kind?, groupID: String? = null) : this() {
|
||||
this.kind = kind
|
||||
this.groupID = groupID
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
@@ -132,45 +119,44 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
val closedGroupControlMessage: DataMessage.ClosedGroupControlMessage.Builder = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupControlMessage.type = NEW
|
||||
closedGroupControlMessage.publicKey = kind.publicKey
|
||||
closedGroupControlMessage.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupControlMessage.encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder().also {
|
||||
it.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removingIdPrefixIfNeeded())
|
||||
it.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
|
||||
}.build()
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
closedGroupControlMessage.addAllAdmins(kind.admins)
|
||||
closedGroupControlMessage.expirationTimer = kind.expirationTimer
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupControlMessage.type = ENCRYPTION_KEY_PAIR
|
||||
closedGroupControlMessage.publicKey = kind.publicKey ?: ByteString.EMPTY
|
||||
closedGroupControlMessage.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
closedGroupControlMessage.type = NAME_CHANGE
|
||||
closedGroupControlMessage.name = kind.name
|
||||
}
|
||||
is Kind.MembersAdded -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
closedGroupControlMessage.type = MEMBERS_ADDED
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
}
|
||||
is Kind.MembersRemoved -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
closedGroupControlMessage.type = MEMBERS_REMOVED
|
||||
closedGroupControlMessage.addAllMembers(kind.members)
|
||||
}
|
||||
is Kind.MemberLeft -> {
|
||||
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
closedGroupControlMessage.type = MEMBER_LEFT
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
// Group context
|
||||
setGroupContext(dataMessageProto)
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
return SignalServiceProtos.Content.newBuilder().apply {
|
||||
dataMessage = DataMessage.newBuilder().also {
|
||||
it.closedGroupControlMessage = closedGroupControlMessage.build()
|
||||
it.setGroupContext()
|
||||
}.build()
|
||||
}.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
|
||||
return null
|
||||
@@ -191,11 +177,9 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
|
||||
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper? {
|
||||
val publicKey = publicKey ?: return null
|
||||
val encryptedKeyPair = encryptedKeyPair ?: return null
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = encryptedKeyPair
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey ?: return null))
|
||||
result.encryptedKeyPair = encryptedKeyPair ?: return null
|
||||
return try {
|
||||
result.build()
|
||||
} catch (e: Exception) {
|
||||
@@ -204,4 +188,4 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,28 +2,28 @@ package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.crypto.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>,
|
||||
var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() {
|
||||
|
||||
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,8 +126,13 @@ 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 recipient = Recipient.from(context, Address.fromSerialized(group.encodedId), false)
|
||||
val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() }, recipient.expireMessages)
|
||||
val closedGroup = ClosedGroup(
|
||||
groupPublicKey,
|
||||
group.title,
|
||||
encryptionKeyPair,
|
||||
group.members.map { it.serialize() },
|
||||
group.admins.map { it.serialize() }
|
||||
)
|
||||
closedGroups.add(closedGroup)
|
||||
}
|
||||
if (group.isOpenGroup) {
|
||||
@@ -152,6 +155,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
||||
val profileKey = configurationProto.profileKey
|
||||
val contacts = configurationProto.contactsList.mapNotNull { Contact.fromProto(it) }
|
||||
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey.toByteArray())
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,14 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
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()
|
||||
@@ -31,6 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
return DataExtractionNotification(kind)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +66,10 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
dataExtractionNotification.timestamp = kind.timestamp
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
contentProto.dataExtractionNotification = dataExtractionNotification.build()
|
||||
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,77 +1,52 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class ExpirationTimerUpdate() : ControlMessage() {
|
||||
/** In the case of a sync message, the public key of the person the message was targeted at.
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
var syncTarget: String? = null
|
||||
var duration: Int? = 0
|
||||
|
||||
/** 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, val isGroup: Boolean = false) : ControlMessage() {
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
if (!super.isValid()) return false
|
||||
return duration != null
|
||||
}
|
||||
|
||||
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
|
||||
val syncTarget = dataMessageProto.syncTarget
|
||||
val duration = dataMessageProto.expireTimer
|
||||
return ExpirationTimerUpdate(syncTarget, duration)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(duration: Int) : this() {
|
||||
this.syncTarget = null
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
internal constructor(syncTarget: String, duration: Int) : this() {
|
||||
this.syncTarget = syncTarget
|
||||
this.duration = duration
|
||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? =
|
||||
proto.dataMessage?.takeIf { it.flags and EXPIRATION_TIMER_UPDATE_VALUE != 0 }?.run {
|
||||
ExpirationTimerUpdate(takeIf { hasSyncTarget() }?.syncTarget, hasGroup()).copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val duration = duration
|
||||
if (duration == null) {
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this")
|
||||
return null
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder().apply {
|
||||
flags = EXPIRATION_TIMER_UPDATE_VALUE
|
||||
expireTimer = expiryMode.expirySeconds.toInt()
|
||||
}
|
||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||
dataMessageProto.expireTimer = duration
|
||||
// Sync target
|
||||
if (syncTarget != null) {
|
||||
dataMessageProto.syncTarget = syncTarget
|
||||
}
|
||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
||||
// Group context
|
||||
if (MessagingModuleConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessageProto)
|
||||
dataMessageProto.setGroupContext()
|
||||
} 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
|
||||
}
|
||||
}
|
||||
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||
try {
|
||||
contentProto.dataMessage = dataMessageProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.setDataMessage(dataMessageProto)
|
||||
.applyExpiryMode()
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this")
|
||||
return null
|
||||
Log.w(TAG, "Couldn't construct expiration timer update proto from: $this", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsession.messaging.messages.visible.Profile
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
@@ -19,6 +20,7 @@ class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = nu
|
||||
profile?.profileKey?.let { messageRequestResponseProto.profileKey = ByteString.copyFrom(it) }
|
||||
return try {
|
||||
SignalServiceProtos.Content.newBuilder()
|
||||
.applyExpiryMode()
|
||||
.setMessageRequestResponse(messageRequestResponseProto.build())
|
||||
.build()
|
||||
} catch (e: Exception) {
|
||||
@@ -40,7 +42,7 @@ class MessageRequestResponse(val isApproved: Boolean, var profile: Profile? = nu
|
||||
profilePictureURL = profileProto.profilePicture
|
||||
}
|
||||
return MessageRequestResponse(isApproved, profile)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
@@ -22,10 +23,11 @@ class ReadReceipt() : ControlMessage() {
|
||||
val timestamps = receiptProto.timestampList
|
||||
if (timestamps.isEmpty()) return null
|
||||
return ReadReceipt(timestamps = timestamps)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(timestamps: List<Long>?) : this() {
|
||||
constructor(timestamps: List<Long>?) : this() {
|
||||
this.timestamps = timestamps
|
||||
}
|
||||
|
||||
@@ -35,16 +37,18 @@ 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()
|
||||
try {
|
||||
contentProto.receiptMessage = receiptProto.build()
|
||||
return contentProto.build()
|
||||
|
||||
return try {
|
||||
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")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,12 +10,10 @@ class SharedConfigurationMessage(val kind: SharedConfigMessage.Kind, val data: B
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
companion object {
|
||||
fun fromProto(proto: SignalServiceProtos.Content): SharedConfigurationMessage? {
|
||||
if (!proto.hasSharedConfigMessage()) return null
|
||||
val sharedConfig = proto.sharedConfigMessage
|
||||
if (!sharedConfig.hasKind() || !sharedConfig.hasData()) return null
|
||||
return SharedConfigurationMessage(sharedConfig.kind, sharedConfig.data.toByteArray(), sharedConfig.seqno)
|
||||
}
|
||||
fun fromProto(proto: SignalServiceProtos.Content): SharedConfigurationMessage? =
|
||||
proto.takeIf { it.hasSharedConfigMessage() }?.sharedConfigMessage
|
||||
?.takeIf { it.hasKind() && it.hasData() }
|
||||
?.run { SharedConfigurationMessage(kind, data.toByteArray(), seqno) }
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
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
|
||||
@@ -20,6 +21,7 @@ class TypingIndicator() : ControlMessage() {
|
||||
val typingIndicatorProto = if (proto.hasTypingMessage()) proto.typingMessage else return null
|
||||
val kind = Kind.fromProto(typingIndicatorProto.action)
|
||||
return TypingIndicator(kind = kind)
|
||||
.copyExpiration(proto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,16 +56,14 @@ 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()
|
||||
try {
|
||||
contentProto.typingMessage = typingIndicatorProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
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")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
package org.session.libsession.messaging.messages.control
|
||||
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
class UnsendRequest(): ControlMessage() {
|
||||
var timestamp: Long? = null
|
||||
var author: String? = null
|
||||
class UnsendRequest(var timestamp: Long? = null, var author: String? = null): ControlMessage() {
|
||||
|
||||
override val isSelfSendValid: Boolean = true
|
||||
|
||||
@@ -19,17 +18,8 @@ class UnsendRequest(): ControlMessage() {
|
||||
companion object {
|
||||
const val TAG = "UnsendRequest"
|
||||
|
||||
fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? {
|
||||
val unsendRequestProto = if (proto.hasUnsendRequest()) proto.unsendRequest else return null
|
||||
val timestamp = unsendRequestProto.timestamp
|
||||
val author = unsendRequestProto.author
|
||||
return UnsendRequest(timestamp, author)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(timestamp: Long, author: String) : this() {
|
||||
this.timestamp = timestamp
|
||||
this.author = author
|
||||
fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? =
|
||||
proto.takeIf { it.hasUnsendRequest() }?.unsendRequest?.run { UnsendRequest(timestamp, author) }?.copyExpiration(proto)
|
||||
}
|
||||
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
@@ -39,16 +29,14 @@ 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()
|
||||
try {
|
||||
contentProto.unsendRequest = unsendRequestProto.build()
|
||||
return contentProto.build()
|
||||
return try {
|
||||
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")
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,6 +26,7 @@ public class IncomingMediaMessage {
|
||||
private final long sentTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final boolean expirationUpdate;
|
||||
private final boolean unidentified;
|
||||
private final boolean messageRequestResponse;
|
||||
@@ -42,6 +43,7 @@ public class IncomingMediaMessage {
|
||||
long sentTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
boolean expirationUpdate,
|
||||
boolean unidentified,
|
||||
boolean messageRequestResponse,
|
||||
@@ -60,6 +62,7 @@ public class IncomingMediaMessage {
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.dataExtractionNotification = dataExtractionNotification.orNull();
|
||||
this.quote = quote.orNull();
|
||||
@@ -78,12 +81,13 @@ public class IncomingMediaMessage {
|
||||
public static IncomingMediaMessage from(VisibleMessage message,
|
||||
Address from,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
Optional<SignalServiceGroup> group,
|
||||
List<SignalServiceAttachment> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<LinkPreview>> linkPreviews)
|
||||
{
|
||||
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
|
||||
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, expireStartedAt, false,
|
||||
false, false, message.getHasMention(), Optional.fromNullable(message.getText()),
|
||||
group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews, Optional.absent());
|
||||
}
|
||||
@@ -124,6 +128,10 @@ public class IncomingMediaMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
@@ -41,6 +41,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
private final boolean push;
|
||||
private final int subscriptionId;
|
||||
private final long expiresInMillis;
|
||||
private final long expireStartedAt;
|
||||
private final boolean unidentified;
|
||||
private final int callType;
|
||||
private final boolean hasMention;
|
||||
@@ -49,19 +50,19 @@ public class IncomingTextMessage implements Parcelable {
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, -1, hasMention);
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, -1, hasMention);
|
||||
}
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified, int callType, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, unidentified, callType, hasMention, true);
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention) {
|
||||
this(sender, senderDeviceId, sentTimestampMillis, encodedBody, group, expiresInMillis, expireStartedAt, unidentified, callType, hasMention, true);
|
||||
}
|
||||
|
||||
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
|
||||
String encodedBody, Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis, boolean unidentified, int callType, boolean hasMention, boolean isPush) {
|
||||
long expiresInMillis, long expireStartedAt, boolean unidentified, int callType, boolean hasMention, boolean isPush) {
|
||||
this.message = encodedBody;
|
||||
this.sender = sender;
|
||||
this.senderDeviceId = senderDeviceId;
|
||||
@@ -73,6 +74,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = isPush;
|
||||
this.subscriptionId = -1;
|
||||
this.expiresInMillis = expiresInMillis;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.unidentified = unidentified;
|
||||
this.callType = callType;
|
||||
this.hasMention = hasMention;
|
||||
@@ -97,6 +99,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = (in.readInt() == 1);
|
||||
this.subscriptionId = in.readInt();
|
||||
this.expiresInMillis = in.readLong();
|
||||
this.expireStartedAt = in.readLong();
|
||||
this.unidentified = in.readInt() == 1;
|
||||
this.isOpenGroupInvitation = in.readInt() == 1;
|
||||
this.callType = in.readInt();
|
||||
@@ -116,6 +119,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.push = base.isPush();
|
||||
this.subscriptionId = base.getSubscriptionId();
|
||||
this.expiresInMillis = base.getExpiresIn();
|
||||
this.expireStartedAt = base.getExpireStartedAt();
|
||||
this.unidentified = base.isUnidentified();
|
||||
this.isOpenGroupInvitation = base.isOpenGroupInvitation();
|
||||
this.callType = base.callType;
|
||||
@@ -125,19 +129,23 @@ public class IncomingTextMessage implements Parcelable {
|
||||
public static IncomingTextMessage from(VisibleMessage message,
|
||||
Address sender,
|
||||
Optional<SignalServiceGroup> group,
|
||||
long expiresInMillis)
|
||||
long expiresInMillis,
|
||||
long expireStartedAt)
|
||||
{
|
||||
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false, message.getHasMention());
|
||||
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, expireStartedAt, false, message.getHasMention());
|
||||
}
|
||||
|
||||
public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Address sender, Long sentTimestamp)
|
||||
{
|
||||
public static IncomingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation,
|
||||
Address sender,
|
||||
Long sentTimestamp,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt) {
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), 0, false, false);
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(sender, 1, sentTimestamp, body, Optional.absent(), expiresInMillis, expireStartedAt, false, false);
|
||||
incomingTextMessage.isOpenGroupInvitation = true;
|
||||
return incomingTextMessage;
|
||||
}
|
||||
@@ -145,8 +153,10 @@ public class IncomingTextMessage implements Parcelable {
|
||||
public static IncomingTextMessage fromCallInfo(CallMessageType callMessageType,
|
||||
Address sender,
|
||||
Optional<SignalServiceGroup> group,
|
||||
long sentTimestamp) {
|
||||
return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, 0, false, callMessageType.ordinal(), false, false);
|
||||
long sentTimestamp,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt) {
|
||||
return new IncomingTextMessage(sender, 1, sentTimestamp, null, group, expiresInMillis, expireStartedAt, false, callMessageType.ordinal(), false, false);
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
@@ -157,6 +167,10 @@ public class IncomingTextMessage implements Parcelable {
|
||||
return expiresInMillis;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public long getSentTimestampMillis() {
|
||||
return sentTimestampMillis;
|
||||
}
|
||||
|
@@ -11,9 +11,9 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
||||
|
||||
private final String groupId;
|
||||
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, String groupId) {
|
||||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn, long expireStartedAt, String groupId) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
||||
DistributionTypes.CONVERSATION, expiresIn, expireStartedAt, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
@Nullable final Attachment avatar,
|
||||
long sentTime,
|
||||
long expireIn,
|
||||
long expireStartedAt,
|
||||
boolean updateMessage,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@@ -32,7 +33,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
||||
super(recipient, body,
|
||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||
sentTime,
|
||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
||||
DistributionTypes.CONVERSATION, expireIn, expireStartedAt, quote, contacts, previews);
|
||||
|
||||
this.groupID = groupId;
|
||||
this.isUpdateMessage = updateMessage;
|
||||
|
@@ -4,13 +4,13 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.utilities.DistributionTypes;
|
||||
import org.session.libsession.utilities.IdentityKeyMismatch;
|
||||
import org.session.libsession.utilities.NetworkFailure;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.utilities.Contact;
|
||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||
import org.session.libsession.utilities.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -26,6 +26,7 @@ public class OutgoingMediaMessage {
|
||||
private final int distributionType;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final QuoteModel outgoingQuote;
|
||||
|
||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||
@@ -35,7 +36,7 @@ public class OutgoingMediaMessage {
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
int subscriptionId, long expiresIn,
|
||||
int subscriptionId, long expiresIn, long expireStartedAt,
|
||||
int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@@ -50,6 +51,7 @@ public class OutgoingMediaMessage {
|
||||
this.attachments = attachments;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt = expireStartedAt;
|
||||
this.outgoingQuote = outgoingQuote;
|
||||
|
||||
this.contacts.addAll(contacts);
|
||||
@@ -66,6 +68,7 @@ public class OutgoingMediaMessage {
|
||||
this.sentTimeMillis = that.sentTimeMillis;
|
||||
this.subscriptionId = that.subscriptionId;
|
||||
this.expiresIn = that.expiresIn;
|
||||
this.expireStartedAt = that.expireStartedAt;
|
||||
this.outgoingQuote = that.outgoingQuote;
|
||||
|
||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||
@@ -78,14 +81,16 @@ public class OutgoingMediaMessage {
|
||||
Recipient recipient,
|
||||
List<Attachment> attachments,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@Nullable LinkPreview linkPreview)
|
||||
@Nullable LinkPreview linkPreview,
|
||||
long expiresInMillis,
|
||||
long expireStartedAt)
|
||||
{
|
||||
List<LinkPreview> previews = Collections.emptyList();
|
||||
if (linkPreview != null) {
|
||||
previews = Collections.singletonList(linkPreview);
|
||||
}
|
||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote,
|
||||
expiresInMillis, expireStartedAt, DistributionTypes.DEFAULT, outgoingQuote,
|
||||
Collections.emptyList(), previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@@ -123,6 +128,10 @@ public class OutgoingMediaMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public @Nullable QuoteModel getOutgoingQuote() {
|
||||
return outgoingQuote;
|
||||
}
|
||||
|
@@ -19,11 +19,12 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
||||
long sentTimeMillis,
|
||||
int distributionType,
|
||||
long expiresIn,
|
||||
long expireStartedAt,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, expireStartedAt, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
@@ -10,28 +10,30 @@ public class OutgoingTextMessage {
|
||||
private final String message;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final long expireStartedAt;
|
||||
private final long sentTimestampMillis;
|
||||
private boolean isOpenGroupInvitation = false;
|
||||
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId, long sentTimestampMillis) {
|
||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, long expireStartedAt, int subscriptionId, long sentTimestampMillis) {
|
||||
this.recipient = recipient;
|
||||
this.message = message;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expireStartedAt= expireStartedAt;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.sentTimestampMillis = sentTimestampMillis;
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1, message.getSentTimestamp());
|
||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient, long expiresInMillis, long expireStartedAt) {
|
||||
return new OutgoingTextMessage(recipient, message.getText(), expiresInMillis, expireStartedAt, -1, message.getSentTimestamp());
|
||||
}
|
||||
|
||||
public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp) {
|
||||
public static OutgoingTextMessage fromOpenGroupInvitation(OpenGroupInvitation openGroupInvitation, Recipient recipient, Long sentTimestamp, long expiresInMillis, long expireStartedAt) {
|
||||
String url = openGroupInvitation.getUrl();
|
||||
String name = openGroupInvitation.getName();
|
||||
if (url == null || name == null) { return null; }
|
||||
// FIXME: Doing toJSON() to get the body here is weird
|
||||
String body = UpdateMessageData.Companion.buildOpenGroupInvitation(url, name).toJSON();
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, 0, -1, sentTimestamp);
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, expiresInMillis, expireStartedAt, -1, sentTimestamp);
|
||||
outgoingTextMessage.isOpenGroupInvitation = true;
|
||||
return outgoingTextMessage;
|
||||
}
|
||||
@@ -40,6 +42,10 @@ public class OutgoingTextMessage {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public long getExpireStartedAt() {
|
||||
return expireStartedAt;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
@@ -4,10 +4,11 @@ import com.google.protobuf.ByteString
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.protos.SignalServiceProtos
|
||||
|
||||
class Profile() {
|
||||
var displayName: String? = null
|
||||
var profileKey: ByteArray? = null
|
||||
class Profile(
|
||||
var displayName: String? = null,
|
||||
var profileKey: ByteArray? = null,
|
||||
var profilePictureURL: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "Profile"
|
||||
@@ -25,12 +26,6 @@ class Profile() {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() {
|
||||
this.displayName = displayName
|
||||
this.profileKey = profileKey
|
||||
this.profilePictureURL = profilePictureURL
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.DataMessage? {
|
||||
val displayName = displayName
|
||||
if (displayName == null) {
|
||||
|
@@ -3,10 +3,8 @@ package org.session.libsession.messaging.messages.visible
|
||||
import com.goterl.lazysodium.BuildConfig
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.copyExpiration
|
||||
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
|
||||
@@ -16,7 +14,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
*
|
||||
* **Note:** `nil` if this isn't a sync message.
|
||||
*/
|
||||
class VisibleMessage(
|
||||
data class VisibleMessage(
|
||||
var syncTarget: String? = null,
|
||||
var text: String? = null,
|
||||
val attachmentIDs: MutableList<Long> = mutableListOf(),
|
||||
@@ -46,52 +44,26 @@ 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
|
||||
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
|
||||
@@ -126,20 +98,12 @@ class VisibleMessage(
|
||||
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
|
||||
proto.applyExpiryMode()
|
||||
// Group context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
if (storage.isClosedGroup(recipient!!)) {
|
||||
try {
|
||||
setGroupContext(dataMessage)
|
||||
dataMessage.setGroupContext()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||
return null
|
||||
@@ -173,4 +137,4 @@ class VisibleMessage(
|
||||
fun isMediaMessage(): Boolean {
|
||||
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || reaction != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -973,5 +973,17 @@ object OpenGroupApi {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllInboxMessages(server: String): Promise<Map<*, *>, java.lang.Exception> {
|
||||
val request = Request(
|
||||
verb = DELETE,
|
||||
room = null,
|
||||
server = server,
|
||||
endpoint = Endpoint.Inbox
|
||||
)
|
||||
return getResponseBody(request).map { response ->
|
||||
JsonUtil.fromJson(response, Map::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
@@ -38,11 +38,13 @@ object MessageReceiver {
|
||||
object NoGroupThread: Error("No thread exists for this group.")
|
||||
object NoGroupKeyPair: Error("Missing group key pair.")
|
||||
object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.")
|
||||
object ExpiredMessage: Error("Message has already expired, prevent adding")
|
||||
|
||||
internal val isRetryable: Boolean = when (this) {
|
||||
is DuplicateMessage, is InvalidMessage, is UnknownMessage,
|
||||
is UnknownEnvelopeType, is InvalidSignature, is NoData,
|
||||
is SenderBlocked, is SelfSend, is NoGroupThread -> false
|
||||
is SenderBlocked, is SelfSend,
|
||||
is ExpiredMessage, is NoGroupThread -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
@@ -143,15 +145,14 @@ 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 }
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) {
|
||||
throw Error.SelfSend
|
||||
}
|
||||
if (sender == userPublicKey || isUserBlindedSender) {
|
||||
val isUserSender = sender == userPublicKey
|
||||
|
||||
if (isUserSender || isUserBlindedSender) {
|
||||
// Ignore self send if needed
|
||||
if (!message.isSelfSendValid) throw Error.SelfSend
|
||||
message.isSenderSelf = true
|
||||
}
|
||||
// Guard against control messages in open groups
|
||||
@@ -191,4 +192,5 @@ object MessageReceiver {
|
||||
// Return
|
||||
return Pair(message, proto)
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@@ -8,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
|
||||
@@ -26,6 +28,7 @@ import org.session.libsession.messaging.utilities.SessionId
|
||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.snode.RawResponsePromise
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeAPI.nowWithOffset
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.snode.SnodeModule
|
||||
import org.session.libsession.utilities.Address
|
||||
@@ -34,7 +37,12 @@ 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.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.Namespace
|
||||
import org.session.libsignal.utilities.defaultRequiresAuth
|
||||
import org.session.libsignal.utilities.hasNamespaces
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||
@@ -77,14 +85,14 @@ object MessageSender {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
// Set the timestamp, sender and recipient
|
||||
val messageSendTime = SnodeAPI.nowWithOffset
|
||||
val messageSendTime = nowWithOffset
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp =
|
||||
messageSendTime // Visible messages will already have their sent timestamp set
|
||||
}
|
||||
|
||||
message.sender = userPublicKey
|
||||
|
||||
// SHARED CONFIG
|
||||
when (destination) {
|
||||
is Destination.Contact -> message.recipient = destination.publicKey
|
||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||
@@ -155,7 +163,7 @@ object MessageSender {
|
||||
return SnodeMessage(
|
||||
message.recipient!!,
|
||||
base64EncodedData,
|
||||
message.ttl,
|
||||
ttl = getSpecifiedTtl(message, isSyncMessage) ?: message.ttl,
|
||||
messageSendTime
|
||||
)
|
||||
}
|
||||
@@ -185,8 +193,13 @@ object MessageSender {
|
||||
val namespaces: List<Int> = when {
|
||||
destination is Destination.ClosedGroup
|
||||
&& forkInfo.defaultRequiresAuth() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP)
|
||||
|
||||
destination is Destination.ClosedGroup
|
||||
&& forkInfo.hasNamespaces() -> listOf(Namespace.UNAUTHENTICATED_CLOSED_GROUP, Namespace.DEFAULT)
|
||||
&& forkInfo.hasNamespaces() -> listOf(
|
||||
Namespace.UNAUTHENTICATED_CLOSED_GROUP,
|
||||
Namespace.DEFAULT
|
||||
)
|
||||
|
||||
else -> listOf(Namespace.DEFAULT)
|
||||
}
|
||||
namespaces.map { namespace -> SnodeAPI.sendMessage(snodeMessage, requiresAuth = false, namespace = namespace) }.let { promises ->
|
||||
@@ -238,13 +251,26 @@ object MessageSender {
|
||||
return promise
|
||||
}
|
||||
|
||||
private fun getSpecifiedTtl(
|
||||
message: Message,
|
||||
isSyncMessage: Boolean
|
||||
): Long? = message.takeUnless { it is ClosedGroupControlMessage }?.run {
|
||||
threadID ?: (if (isSyncMessage && this is VisibleMessage) syncTarget else recipient)
|
||||
?.let(Address.Companion::fromSerialized)
|
||||
?.let(MessagingModuleConfiguration.shared.storage::getThreadId)
|
||||
}?.let(MessagingModuleConfiguration.shared.storage::getExpirationConfiguration)
|
||||
?.takeIf { it.isEnabled }
|
||||
?.expiryMode
|
||||
?.takeIf { it is ExpiryMode.AfterSend || isSyncMessage }
|
||||
?.expiryMillis
|
||||
|
||||
// Open Groups
|
||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val configFactory = MessagingModuleConfiguration.shared.configFactory
|
||||
if (message.sentTimestamp == null) {
|
||||
message.sentTimestamp = SnodeAPI.nowWithOffset
|
||||
message.sentTimestamp = nowWithOffset
|
||||
}
|
||||
// Attach the blocks message requests info
|
||||
configFactory.user?.let { user ->
|
||||
@@ -347,20 +373,23 @@ object MessageSender {
|
||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val timestamp = message.sentTimestamp!!
|
||||
// Ignore future self-sends
|
||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||
storage.getMessageIdInDatabase(message.sentTimestamp!!, userPublicKey)?.let { messageID ->
|
||||
storage.addReceivedMessageTimestamp(timestamp)
|
||||
storage.getMessageIdInDatabase(timestamp, userPublicKey)?.let { (messageID, mms) ->
|
||||
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
||||
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
||||
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
||||
message.sentTimestamp = openGroupSentTimestamp
|
||||
}
|
||||
|
||||
// When the sync message is successfully sent, the hash value of this TSOutgoingMessage
|
||||
// will be replaced by the hash value of the sync message. Since the hash value of the
|
||||
// real message has no use when we delete a message. It is OK to let it be.
|
||||
message.serverHash?.let {
|
||||
storage.setMessageServerHash(messageID, it)
|
||||
storage.setMessageServerHash(messageID, mms, it)
|
||||
}
|
||||
|
||||
// in case any errors from previous sends
|
||||
storage.clearErrorMessage(messageID)
|
||||
// Track the open group server message ID
|
||||
@@ -387,12 +416,10 @@ object MessageSender {
|
||||
}
|
||||
}
|
||||
// Mark the message as sent
|
||||
storage.markAsSent(message.sentTimestamp!!, userPublicKey)
|
||||
storage.markUnidentified(message.sentTimestamp!!, userPublicKey)
|
||||
storage.markAsSent(timestamp, userPublicKey)
|
||||
storage.markUnidentified(timestamp, userPublicKey)
|
||||
// Start the disappearing messages timer if needed
|
||||
if (message is VisibleMessage && !isSyncMessage) {
|
||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, userPublicKey)
|
||||
}
|
||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message, startDisappearAfterRead = true)
|
||||
} ?: run {
|
||||
storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp)
|
||||
}
|
||||
@@ -404,7 +431,7 @@ object MessageSender {
|
||||
if (message is VisibleMessage) message.syncTarget = destination.publicKey
|
||||
if (message is ExpirationTimerUpdate) message.syncTarget = destination.publicKey
|
||||
|
||||
storage.markAsSyncing(message.sentTimestamp!!, userPublicKey)
|
||||
storage.markAsSyncing(timestamp, userPublicKey)
|
||||
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
|
||||
}
|
||||
}
|
||||
@@ -431,7 +458,7 @@ object MessageSender {
|
||||
message.linkPreview?.let { linkPreview ->
|
||||
if (linkPreview.attachmentID == null) {
|
||||
messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID ->
|
||||
message.linkPreview!!.attachmentID = attachmentID
|
||||
linkPreview.attachmentID = attachmentID
|
||||
message.attachmentIDs.remove(attachmentID)
|
||||
}
|
||||
}
|
||||
@@ -442,6 +469,7 @@ object MessageSender {
|
||||
@JvmStatic
|
||||
fun send(message: Message, address: Address) {
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
|
||||
threadID?.let(message::applyExpiryMode)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(address)
|
||||
val job = MessageSendJob(message, destination)
|
||||
|
@@ -16,7 +16,6 @@ import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.crypto.ecc.Curve
|
||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
@@ -27,7 +26,7 @@ import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
const val groupSizeLimit = 100
|
||||
@@ -54,7 +53,7 @@ fun MessageSender.create(
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), SnodeAPI.nowWithOffset)
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
|
||||
@@ -75,7 +74,7 @@ fun MessageSender.create(
|
||||
|
||||
val ourPubKey = storage.getUserPublicKey()
|
||||
for (member in members) {
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
try {
|
||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).get()
|
||||
@@ -92,7 +91,7 @@ fun MessageSender.create(
|
||||
}
|
||||
|
||||
// Add the group to the config now that it was successfully created
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair)
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), sentTime, encryptionKeyPair, 0)
|
||||
// Notify the PN server
|
||||
PushRegistryV1.register(device = device, publicKey = userPublicKey)
|
||||
// Start polling
|
||||
@@ -117,7 +116,7 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Update the group
|
||||
@@ -136,8 +135,8 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
Log.d("Loki", "Can't add members to nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val recipient = Recipient.from(context, fromSerialized(groupID), false)
|
||||
val expireTimer = recipient.expireMessages
|
||||
val threadId = storage.getOrCreateThreadIdFor(fromSerialized(groupID))
|
||||
val expireTimer = storage.getExpirationConfiguration(threadId)?.expiryMode?.expirySeconds ?: 0
|
||||
if (membersToAdd.isEmpty()) {
|
||||
Log.d("Loki", "Invalid closed group update.")
|
||||
throw Error.InvalidClosedGroupUpdate
|
||||
@@ -157,13 +156,20 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData, expireTimer)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
|
||||
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(
|
||||
ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)),
|
||||
name,
|
||||
encryptionKeyPair,
|
||||
membersAsData,
|
||||
adminsAsData,
|
||||
expireTimer.toInt()
|
||||
)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind, groupID)
|
||||
// It's important that the sent timestamp of this message is greater than the sent timestamp
|
||||
// of the `MembersAdded` message above. The reason is that upon receiving this `New` message,
|
||||
// the recipient will update the closed group formation timestamp and ignore any closed group
|
||||
@@ -212,7 +218,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind, groupID)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||
// Send the new encryption key pair to the remaining group members.
|
||||
@@ -241,7 +247,7 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft(), groupID)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
storage.setActive(groupID, false)
|
||||
@@ -302,7 +308,7 @@ fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKe
|
||||
}
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
|
||||
val sentTime = SnodeAPI.nowWithOffset
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, null)
|
||||
closedGroupControlMessage.sentTimestamp = sentTime
|
||||
return if (force) {
|
||||
val isSync = MessagingModuleConfiguration.shared.storage.getUserPublicKey() == destination
|
||||
@@ -337,6 +343,6 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey:
|
||||
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||
val closedGroupControlMessage = ClosedGroupControlMessage(kind, groupID)
|
||||
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey))
|
||||
}
|
@@ -2,10 +2,13 @@ package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.text.TextUtils
|
||||
import network.loki.messenger.libsession_util.ConfigBase
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
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.ExpirationConfiguration
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.CallMessage
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||
@@ -31,8 +34,10 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
|
||||
import org.session.libsession.messaging.utilities.WebRtcUtils
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||
import org.session.libsession.utilities.GroupRecord
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.GroupUtil.doubleEncodeGroupID
|
||||
import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@@ -148,10 +153,27 @@ fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
|
||||
if (message.duration!! > 0) {
|
||||
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message)
|
||||
} else {
|
||||
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(message)
|
||||
SSKEnvironment.shared.messageExpirationManager.insertExpirationTimerMessage(message)
|
||||
|
||||
// TODO (Groups V2 - FIXME)
|
||||
val isGroupV1 = message.groupPublicKey != null
|
||||
|
||||
if (isNewConfigEnabled && !isGroupV1) return
|
||||
|
||||
val module = MessagingModuleConfiguration.shared
|
||||
try {
|
||||
val threadId = fromSerialized(message.groupPublicKey?.let(::doubleEncodeGroupID) ?: message.sender!!)
|
||||
.let(module.storage::getOrCreateThreadIdFor)
|
||||
|
||||
module.storage.setExpirationConfiguration(
|
||||
ExpirationConfiguration(
|
||||
threadId,
|
||||
message.expiryMode,
|
||||
message.sentTimestamp!!
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Failed to update expiration configuration.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +214,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) {
|
||||
// just handle the closed group encryption key pairs to avoid sync'd devices getting out of sync
|
||||
storage.addClosedGroupEncryptionKeyPair(closedGroup.encryptionKeyPair!!, closedGroup.publicKey, message.sentTimestamp!!)
|
||||
} else if (firstTimeSync) {
|
||||
} 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!!, -1)
|
||||
}
|
||||
}
|
||||
val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||
@@ -233,8 +255,8 @@ fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val timestamp = message.timestamp ?: return null
|
||||
val author = message.author ?: return null
|
||||
val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return null
|
||||
messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
|
||||
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
|
||||
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
|
||||
SnodeAPI.deleteMessage(author, listOf(serverHash))
|
||||
}
|
||||
val deletedMessageId = messageDataProvider.updateMessageAsDeleted(timestamp, author)
|
||||
@@ -250,6 +272,14 @@ fun handleMessageRequestResponse(message: MessageRequestResponse) {
|
||||
}
|
||||
//endregion
|
||||
|
||||
private fun SignalServiceProtos.Content.ExpirationType.expiryMode(durationSeconds: Long) = takeIf { durationSeconds > 0 }?.let {
|
||||
when (it) {
|
||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_READ -> ExpiryMode.AfterRead(durationSeconds)
|
||||
SignalServiceProtos.Content.ExpirationType.DELETE_AFTER_SEND, SignalServiceProtos.Content.ExpirationType.UNKNOWN -> ExpiryMode.AfterSend(durationSeconds)
|
||||
else -> ExpiryMode.NONE
|
||||
}
|
||||
} ?: ExpiryMode.NONE
|
||||
|
||||
fun MessageReceiver.handleVisibleMessage(
|
||||
message: VisibleMessage,
|
||||
proto: SignalServiceProtos.Content,
|
||||
@@ -308,6 +338,17 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
if (userPublicKey != messageSender && !isUserBlindedSender) {
|
||||
storage.setBlocksCommunityMessageRequests(recipient, message.blocksMessageRequests)
|
||||
}
|
||||
|
||||
// update the disappearing / legacy banner for the sender
|
||||
val disappearingState = when {
|
||||
proto.dataMessage.expireTimer > 0 && !proto.hasExpirationType() -> Recipient.DisappearingState.LEGACY
|
||||
else -> Recipient.DisappearingState.UPDATED
|
||||
}
|
||||
storage.updateDisappearingState(
|
||||
messageSender,
|
||||
threadID,
|
||||
disappearingState
|
||||
)
|
||||
}
|
||||
// Parse quote if needed
|
||||
var quoteModel: QuoteModel? = null
|
||||
@@ -348,14 +389,7 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
}
|
||||
}
|
||||
// Parse attachments if needed
|
||||
val attachments = proto.dataMessage.attachmentsList.mapNotNull { attachmentProto ->
|
||||
val attachment = Attachment.fromProto(attachmentProto)
|
||||
if (!attachment.isValid()) {
|
||||
return@mapNotNull null
|
||||
} else {
|
||||
return@mapNotNull attachment
|
||||
}
|
||||
}
|
||||
val attachments = proto.dataMessage.attachmentsList.map(Attachment::fromProto).filter { it.isValid() }
|
||||
// Cancel any typing indicators if needed
|
||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||
// Parse reaction if needed
|
||||
@@ -386,15 +420,12 @@ fun MessageReceiver.handleVisibleMessage(
|
||||
|
||||
// Persist the message
|
||||
message.threadID = threadID
|
||||
val messageID =
|
||||
storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID,
|
||||
attachments, runThreadUpdate
|
||||
) ?: return null
|
||||
val openGroupServerID = message.openGroupServerMessageID
|
||||
if (openGroupServerID != null) {
|
||||
val isSms = !(message.isMediaMessage() || attachments.isNotEmpty())
|
||||
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms)
|
||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments, runThreadUpdate) ?: return null
|
||||
message.openGroupServerMessageID?.let {
|
||||
val isSms = !message.isMediaMessage() && attachments.isEmpty()
|
||||
storage.setOpenGroupServerMessageID(messageID, it, threadID, isSms)
|
||||
}
|
||||
SSKEnvironment.shared.messageExpirationManager.maybeStartExpiration(message)
|
||||
return messageID
|
||||
}
|
||||
return null
|
||||
@@ -506,17 +537,20 @@ private fun ClosedGroupControlMessage.getPublicKey(): String = kind!!.let { when
|
||||
}}
|
||||
|
||||
private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
|
||||
val recipient = Recipient.from(MessagingModuleConfiguration.shared.context, Address.fromSerialized(message.sender!!), false)
|
||||
if (!recipient.isApproved && !recipient.isLocalNumber) return Log.e("Loki", "not accepting new closed group from unapproved recipient")
|
||||
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
|
||||
// hard code check by group public key in the big function because I can't be bothered to do group double decode re-encodej
|
||||
if ((storage.getThreadIdFor(message.sender!!, groupPublicKey, null, false) ?: -1L) >= 0L) return
|
||||
val members = kind.members.map { it.toByteArray().toHexString() }
|
||||
val admins = kind.admins.map { it.toByteArray().toHexString() }
|
||||
val expireTimer = kind.expirationTimer
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expireTimer)
|
||||
val expirationTimer = kind.expirationTimer
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!, expirationTimer)
|
||||
}
|
||||
|
||||
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, expirationTimer: Int) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
@@ -548,7 +582,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
|
||||
}
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
@@ -556,9 +590,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
||||
storage.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, sentTimestamp)
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), formationTimestamp, encryptionKeyPair)
|
||||
// Set expiration timer
|
||||
storage.setExpirationTimer(groupID, expireTimer)
|
||||
storage.createInitialConfigGroup(groupPublicKey, name, GroupUtil.createConfigMemberMap(members, admins), formationTimestamp, encryptionKeyPair, expirationTimer)
|
||||
// Notify the PN server
|
||||
PushRegistryV1.register(device = MessagingModuleConfiguration.shared.device, publicKey = userPublicKey)
|
||||
// Notify the user
|
||||
|
@@ -152,14 +152,18 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
|
||||
Log.w("Loki", "shared config message handled in configs wasn't SharedConfigurationMessage but was ${message.javaClass.simpleName}")
|
||||
return@forEach
|
||||
}
|
||||
forConfigObject.merge(hash!! to message.data)
|
||||
latestMessageTimestamp = if ((message.sentTimestamp ?: 0L) > (latestMessageTimestamp ?: 0L)) { message.sentTimestamp } else { latestMessageTimestamp }
|
||||
val merged = forConfigObject.merge(hash!! to message.data).firstOrNull { it == hash }
|
||||
if (merged != null) {
|
||||
// We successfully merged the hash, we can now update the timestamp
|
||||
latestMessageTimestamp = if ((message.sentTimestamp ?: 0L) > (latestMessageTimestamp ?: 0L)) { message.sentTimestamp } else { latestMessageTimestamp }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", e)
|
||||
}
|
||||
}
|
||||
// process new results
|
||||
if (forConfigObject.needsDump()) {
|
||||
// latestMessageTimestamp should always be non-null if the config object needs dump
|
||||
if (forConfigObject.needsDump() && latestMessageTimestamp != null) {
|
||||
configFactory.persist(forConfigObject, latestMessageTimestamp ?: SnodeAPI.nowWithOffset)
|
||||
}
|
||||
}
|
||||
@@ -210,7 +214,6 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
|
||||
val responseList = (rawResponses["results"] as List<RawResponse>)
|
||||
// in case we had null configs, the array won't be fully populated
|
||||
// index of the sparse array key iterator should be the request index, with the key being the namespace
|
||||
// TODO: add in specific ordering of config namespaces for processing
|
||||
listOfNotNull(
|
||||
configFactory.user?.configNamespace(),
|
||||
configFactory.contacts?.configNamespace(),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
@@ -9,14 +10,17 @@ import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
|
||||
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.messaging.messages.ExpirationConfiguration.Companion.isNewConfigEnabled
|
||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||
import org.session.libsession.utilities.ExpirationUtil
|
||||
import org.session.libsession.utilities.getExpirationTypeDisplayValue
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsession.utilities.truncateIdForDisplay
|
||||
|
||||
object UpdateMessageBuilder {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
|
||||
fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId)
|
||||
private fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId)
|
||||
?.displayName(Contact.ContactContext.REGULAR)
|
||||
?: truncateIdForDisplay(senderId)
|
||||
|
||||
@@ -73,18 +77,44 @@ object UpdateMessageBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||
fun buildExpirationTimerMessage(
|
||||
context: Context,
|
||||
duration: Long,
|
||||
recipient: Recipient,
|
||||
senderId: String? = null,
|
||||
isOutgoing: Boolean = false,
|
||||
timestamp: Long,
|
||||
expireStarted: Long
|
||||
): String {
|
||||
if (!isOutgoing && senderId == null) return ""
|
||||
val senderName: String = if (!isOutgoing) {
|
||||
getSenderName(senderId!!)
|
||||
} else { context.getString(R.string.MessageRecord_you) }
|
||||
val senderName = if (isOutgoing) context.getString(R.string.MessageRecord_you) else getSenderName(senderId!!)
|
||||
return if (duration <= 0) {
|
||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||
else context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
|
||||
if (isOutgoing) {
|
||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||
else context.getString(if (recipient.is1on1) R.string.MessageRecord_you_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_you_turned_off_disappearing_messages)
|
||||
} else {
|
||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, senderName)
|
||||
else context.getString(if (recipient.is1on1) R.string.MessageRecord_s_turned_off_disappearing_messages_1_on_1 else R.string.MessageRecord_s_turned_off_disappearing_messages, senderName)
|
||||
}
|
||||
} else {
|
||||
val time = ExpirationUtil.getExpirationDisplayValue(context, duration.toInt())
|
||||
if (isOutgoing)context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)
|
||||
else context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
|
||||
val action = context.getExpirationTypeDisplayValue(timestamp == expireStarted)
|
||||
if (isOutgoing) {
|
||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time)
|
||||
else context.getString(
|
||||
if (recipient.is1on1) R.string.MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_you_set_messages_to_disappear_s_after_s,
|
||||
time,
|
||||
action
|
||||
)
|
||||
} else {
|
||||
if (!isNewConfigEnabled) context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, senderName, time)
|
||||
else context.getString(
|
||||
if (recipient.is1on1) R.string.MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1 else R.string.MessageRecord_s_set_messages_to_disappear_s_after_s,
|
||||
senderName,
|
||||
time,
|
||||
action
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -611,7 +611,7 @@ object OnionRequestAPI {
|
||||
}
|
||||
if (body["t"] != null) {
|
||||
val timestamp = body["t"] as Long
|
||||
val offset = timestamp - Date().time
|
||||
val offset = timestamp - System.currentTimeMillis()
|
||||
SnodeAPI.clockOffset = offset
|
||||
}
|
||||
if (body.containsKey("hf")) {
|
||||
|
@@ -533,14 +533,10 @@ object SnodeAPI {
|
||||
|
||||
fun getExpiries(messageHashes: List<String>, publicKey: String) : RawResponsePromise {
|
||||
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(NullPointerException("No user key pair"))
|
||||
val hashes = messageHashes.takeIf { it.size != 1 } ?: (messageHashes + "///////////////////////////////////////////") // TODO remove this when bug is fixed on nodes.
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val timestamp = System.currentTimeMillis() + clockOffset
|
||||
val params = mutableMapOf(
|
||||
"pubkey" to publicKey,
|
||||
"messages" to messageHashes,
|
||||
"timestamp" to timestamp
|
||||
)
|
||||
val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${messageHashes.joinToString(separator = "")}".toByteArray()
|
||||
val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${hashes.joinToString(separator = "")}".toByteArray()
|
||||
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val signature = ByteArray(Sign.BYTES)
|
||||
@@ -555,9 +551,14 @@ object SnodeAPI {
|
||||
Log.e("Loki", "Signing data failed with user secret key", e)
|
||||
return@retryIfNeeded Promise.ofFail(e)
|
||||
}
|
||||
params["pubkey_ed25519"] = ed25519PublicKey
|
||||
params["signature"] = Base64.encodeBytes(signature)
|
||||
getSingleTargetSnode(publicKey).bind { snode ->
|
||||
val params = mapOf(
|
||||
"pubkey" to publicKey,
|
||||
"messages" to hashes,
|
||||
"timestamp" to timestamp,
|
||||
"pubkey_ed25519" to ed25519PublicKey,
|
||||
"signature" to Base64.encodeBytes(signature)
|
||||
)
|
||||
getSingleTargetSnode(publicKey) bind { snode ->
|
||||
invoke(Snode.Method.GetExpiries, snode, params, publicKey)
|
||||
}
|
||||
}
|
||||
@@ -761,6 +762,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, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
return if (messages != null) {
|
||||
|
@@ -19,5 +19,5 @@ interface ConfigFactoryProtocol {
|
||||
}
|
||||
|
||||
interface ConfigFactoryUpdateListener {
|
||||
fun notifyUpdates(forConfigObject: ConfigBase)
|
||||
fun notifyUpdates(forConfigObject: ConfigBase, messageTimestamp: Long)
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
package org.session.libsession.utilities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.session.libsession.R;
|
||||
|
||||
public class ExpirationUtil {
|
||||
|
||||
public static String getExpirationDisplayValue(Context context, int expirationTime) {
|
||||
if (expirationTime <= 0) {
|
||||
return context.getString(R.string.expiration_off);
|
||||
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_seconds, expirationTime, expirationTime);
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_minutes, minutes, minutes);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_hours, hours, hours);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_days, days, days);
|
||||
} else {
|
||||
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
|
||||
return context.getResources().getQuantityString(R.plurals.expiration_weeks, weeks, weeks);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getExpirationAbbreviatedDisplayValue(Context context, int expirationTime) {
|
||||
if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
return context.getResources().getString(R.string.expiration_seconds_abbreviated, expirationTime);
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
int minutes = expirationTime / (int)TimeUnit.MINUTES.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_minutes_abbreviated, minutes);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
int hours = expirationTime / (int)TimeUnit.HOURS.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_hours_abbreviated, hours);
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
int days = expirationTime / (int)TimeUnit.DAYS.toSeconds(1);
|
||||
return context.getResources().getString(R.string.expiration_days_abbreviated, days);
|
||||
} else {
|
||||
int weeks = expirationTime / (int)TimeUnit.DAYS.toSeconds(7);
|
||||
return context.getResources().getString(R.string.expiration_weeks_abbreviated, weeks);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import org.session.libsession.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Context.getExpirationTypeDisplayValue(sent: Boolean) = if (sent) getString(R.string.MessageRecord_state_sent) else getString(R.string.MessageRecord_state_read)
|
||||
|
||||
object ExpirationUtil {
|
||||
@JvmStatic
|
||||
fun getExpirationDisplayValue(context: Context, duration: Duration): String = getExpirationDisplayValue(context, duration.inWholeSeconds.toInt())
|
||||
|
||||
@JvmStatic
|
||||
fun getExpirationDisplayValue(context: Context, expirationTime: Int): String {
|
||||
return if (expirationTime <= 0) {
|
||||
context.getString(R.string.expiration_off)
|
||||
} else if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.expiration_seconds,
|
||||
expirationTime,
|
||||
expirationTime
|
||||
)
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_minutes, minutes, minutes)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_hours, hours, hours)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
val days = expirationTime / TimeUnit.DAYS.toSeconds(1).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_days, days, days)
|
||||
} else {
|
||||
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7).toInt()
|
||||
context.resources.getQuantityString(R.plurals.expiration_weeks, weeks, weeks)
|
||||
}
|
||||
}
|
||||
|
||||
fun getExpirationAbbreviatedDisplayValue(context: Context, expirationTime: Long): String {
|
||||
return if (expirationTime < TimeUnit.MINUTES.toSeconds(1)) {
|
||||
context.resources.getString(R.string.expiration_seconds_abbreviated, expirationTime)
|
||||
} else if (expirationTime < TimeUnit.HOURS.toSeconds(1)) {
|
||||
val minutes = expirationTime / TimeUnit.MINUTES.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_minutes_abbreviated, minutes)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(1)) {
|
||||
val hours = expirationTime / TimeUnit.HOURS.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_hours_abbreviated, hours)
|
||||
} else if (expirationTime < TimeUnit.DAYS.toSeconds(7)) {
|
||||
val days = expirationTime / TimeUnit.DAYS.toSeconds(1)
|
||||
context.resources.getString(R.string.expiration_days_abbreviated, days)
|
||||
} else {
|
||||
val weeks = expirationTime / TimeUnit.DAYS.toSeconds(7)
|
||||
context.resources.getString(R.string.expiration_weeks_abbreviated, weeks)
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,4 +34,4 @@ class GroupRecord(
|
||||
this.admins = Address.fromSerializedList(admins!!, ',')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -108,22 +108,23 @@ object GroupUtil {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun doubleDecodeGroupId(groupID: String): String {
|
||||
return Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID)))
|
||||
}
|
||||
fun doubleDecodeGroupId(groupID: String): String =
|
||||
Hex.toStringCondensed(getDecodedGroupIDAsData(getDecodedGroupID(groupID)))
|
||||
|
||||
@JvmStatic
|
||||
fun addressToGroupSessionId(address: Address): String =
|
||||
doubleDecodeGroupId(address.toGroupString())
|
||||
|
||||
fun createConfigMemberMap(
|
||||
members: Collection<String>,
|
||||
admins: Collection<String>
|
||||
): Map<String, Boolean> {
|
||||
// Start with admins
|
||||
val memberMap = admins.associate {
|
||||
it to true
|
||||
}.toMutableMap()
|
||||
val memberMap = admins.associateWith { true }.toMutableMap()
|
||||
|
||||
// Add the remaining members (there may be duplicates, so only add ones that aren't already in there from admins)
|
||||
for (member in members) {
|
||||
if (!memberMap.contains(member)) {
|
||||
if (member !in memberMap) {
|
||||
memberMap[member] = false
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,15 @@
|
||||
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.ClosedGroupControlMessage
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
|
||||
import org.session.libsession.snode.SnodeAPI.nowWithOffset
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
|
||||
class SSKEnvironment(
|
||||
@@ -37,9 +43,37 @@ class SSKEnvironment(
|
||||
}
|
||||
|
||||
interface MessageExpirationManagerProtocol {
|
||||
fun setExpirationTimer(message: ExpirationTimerUpdate)
|
||||
fun disableExpirationTimer(message: ExpirationTimerUpdate)
|
||||
fun startAnyExpiration(timestamp: Long, author: String)
|
||||
fun insertExpirationTimerMessage(message: ExpirationTimerUpdate)
|
||||
fun startAnyExpiration(timestamp: Long, author: String, expireStartedAt: Long)
|
||||
|
||||
fun maybeStartExpiration(message: Message, startDisappearAfterRead: Boolean = false) {
|
||||
if (message is ExpirationTimerUpdate && message.isGroup || message is ClosedGroupControlMessage) return
|
||||
|
||||
maybeStartExpiration(
|
||||
message.sentTimestamp ?: return,
|
||||
message.sender ?: return,
|
||||
message.expiryMode,
|
||||
startDisappearAfterRead || message.isSenderSelf
|
||||
)
|
||||
}
|
||||
|
||||
fun startDisappearAfterRead(timestamp: Long, sender: String) {
|
||||
startAnyExpiration(
|
||||
timestamp,
|
||||
sender,
|
||||
expireStartedAt = nowWithOffset.coerceAtLeast(timestamp + 1)
|
||||
)
|
||||
}
|
||||
|
||||
fun maybeStartExpiration(timestamp: Long, sender: String, mode: ExpiryMode, startDisappearAfterRead: Boolean = false) {
|
||||
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 {
|
||||
|
@@ -365,4 +365,21 @@ object Util {
|
||||
val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt()
|
||||
return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, R> T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this
|
||||
|
||||
fun <T, K: Any> Iterable<T>.associateByNotNull(
|
||||
keySelector: (T) -> K?
|
||||
) = associateByNotNull(keySelector) { it }
|
||||
|
||||
fun <T, K: Any, V: Any> Iterable<T>.associateByNotNull(
|
||||
keySelector: (T) -> K?,
|
||||
valueTransform: (T) -> V?,
|
||||
): Map<K, V> = buildMap {
|
||||
for (item in this@associateByNotNull) {
|
||||
val key = keySelector(item) ?: continue
|
||||
val value = valueTransform(item) ?: continue
|
||||
this[key] = value
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@ package org.session.libsession.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
@@ -14,3 +16,7 @@ fun Context.getColorFromAttr(
|
||||
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
inline fun <reified LP: ViewGroup.LayoutParams> View.modifyLayoutParams(function: LP.() -> Unit) {
|
||||
layoutParams = (layoutParams as LP).apply { function() }
|
||||
}
|
||||
|
@@ -86,6 +86,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private boolean blocked = false;
|
||||
private boolean approved = false;
|
||||
private boolean approvedMe = false;
|
||||
private DisappearingState disappearingState = null;
|
||||
private VibrateState messageVibrate = VibrateState.DEFAULT;
|
||||
private VibrateState callVibrate = VibrateState.DEFAULT;
|
||||
private int expireMessages = 0;
|
||||
@@ -163,6 +164,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.unidentifiedAccessMode = stale.unidentifiedAccessMode;
|
||||
this.forceSmsSelection = stale.forceSmsSelection;
|
||||
this.notifyType = stale.notifyType;
|
||||
this.disappearingState = stale.disappearingState;
|
||||
|
||||
this.participants.clear();
|
||||
this.participants.addAll(stale.participants);
|
||||
@@ -194,6 +196,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.forceSmsSelection = details.get().forceSmsSelection;
|
||||
this.notifyType = details.get().notifyType;
|
||||
this.blocksCommunityMessageRequests = details.get().blocksCommunityMessageRequests;
|
||||
this.disappearingState = details.get().disappearingState;
|
||||
|
||||
this.participants.clear();
|
||||
this.participants.addAll(details.get().participants);
|
||||
@@ -230,6 +233,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
Recipient.this.unidentifiedAccessMode = result.unidentifiedAccessMode;
|
||||
Recipient.this.forceSmsSelection = result.forceSmsSelection;
|
||||
Recipient.this.notifyType = result.notifyType;
|
||||
Recipient.this.disappearingState = result.disappearingState;
|
||||
Recipient.this.blocksCommunityMessageRequests = result.blocksCommunityMessageRequests;
|
||||
|
||||
Recipient.this.participants.clear();
|
||||
@@ -453,6 +457,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
public boolean isContactRecipient() {
|
||||
return address.isContact();
|
||||
}
|
||||
public boolean is1on1() { return address.isContact() && !isLocalNumber; }
|
||||
|
||||
public boolean isOpenGroupRecipient() {
|
||||
return address.isOpenGroup();
|
||||
@@ -681,6 +686,18 @@ public class Recipient implements RecipientModifiedListener {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized DisappearingState getDisappearingState() {
|
||||
return disappearingState;
|
||||
}
|
||||
|
||||
public void setDisappearingState(DisappearingState disappearingState) {
|
||||
synchronized (this) {
|
||||
this.disappearingState = disappearingState;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public synchronized RegisteredState getRegistered() {
|
||||
if (isPushGroupRecipient()) return RegisteredState.REGISTERED;
|
||||
|
||||
@@ -770,6 +787,10 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized boolean showCallMenu() {
|
||||
return !isGroupRecipient() && hasApprovedMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@@ -854,6 +875,24 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
}
|
||||
|
||||
public enum DisappearingState {
|
||||
LEGACY(0), UPDATED(1);
|
||||
|
||||
private final int id;
|
||||
|
||||
DisappearingState(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static DisappearingState fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public enum RegisteredState {
|
||||
UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2);
|
||||
|
||||
@@ -896,6 +935,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
private final boolean approvedMe;
|
||||
private final long muteUntil;
|
||||
private final int notifyType;
|
||||
private final DisappearingState disappearingState;
|
||||
private final VibrateState messageVibrateState;
|
||||
private final VibrateState callVibrateState;
|
||||
private final Uri messageRingtone;
|
||||
@@ -920,7 +960,8 @@ public class Recipient implements RecipientModifiedListener {
|
||||
|
||||
public RecipientSettings(boolean blocked, boolean approved, boolean approvedMe, long muteUntil,
|
||||
int notifyType,
|
||||
@NonNull VibrateState messageVibrateState,
|
||||
@NonNull DisappearingState disappearingState,
|
||||
@NonNull VibrateState messageVibrateState,
|
||||
@NonNull VibrateState callVibrateState,
|
||||
@Nullable Uri messageRingtone,
|
||||
@Nullable Uri callRingtone,
|
||||
@@ -948,6 +989,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
this.approvedMe = approvedMe;
|
||||
this.muteUntil = muteUntil;
|
||||
this.notifyType = notifyType;
|
||||
this.disappearingState = disappearingState;
|
||||
this.messageVibrateState = messageVibrateState;
|
||||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
@@ -995,6 +1037,10 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return notifyType;
|
||||
}
|
||||
|
||||
public @NonNull DisappearingState getDisappearingState() {
|
||||
return disappearingState;
|
||||
}
|
||||
|
||||
public @NonNull VibrateState getMessageVibrateState() {
|
||||
return messageVibrateState;
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ import org.session.libsession.utilities.ListenableFutureTask;
|
||||
import org.session.libsession.utilities.MaterialColor;
|
||||
import org.session.libsession.utilities.TextSecurePreferences;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsession.utilities.recipients.Recipient.DisappearingState;
|
||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
|
||||
import org.session.libsession.utilities.recipients.Recipient.RegisteredState;
|
||||
import org.session.libsession.utilities.recipients.Recipient.UnidentifiedAccessMode;
|
||||
@@ -159,6 +160,7 @@ class RecipientProvider {
|
||||
@Nullable final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
final int notifyType;
|
||||
@Nullable final DisappearingState disappearingState;
|
||||
@Nullable final VibrateState messageVibrateState;
|
||||
@Nullable final VibrateState callVibrateState;
|
||||
final boolean blocked;
|
||||
@@ -193,6 +195,7 @@ class RecipientProvider {
|
||||
this.callRingtone = settings != null ? settings.getCallRingtone() : null;
|
||||
this.mutedUntil = settings != null ? settings.getMuteUntil() : 0;
|
||||
this.notifyType = settings != null ? settings.getNotifyType() : 0;
|
||||
this.disappearingState = settings != null ? settings.getDisappearingState() : null;
|
||||
this.messageVibrateState = settings != null ? settings.getMessageVibrateState() : null;
|
||||
this.callVibrateState = settings != null ? settings.getCallVibrateState() : null;
|
||||
this.blocked = settings != null && settings.isBlocked();
|
||||
|
@@ -252,11 +252,6 @@
|
||||
<attr name="doc_downloadButtonTint" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ConversationItemFooter">
|
||||
<attr name="footer_text_color" format="color" />
|
||||
<attr name="footer_icon_color" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ConversationItemThumbnail">
|
||||
<attr name="conversationThumbnail_minWidth" format="dimension" />
|
||||
<attr name="conversationThumbnail_maxWidth" format="dimension" />
|
||||
|
@@ -15,12 +15,26 @@
|
||||
<string name="MessageRecord_s_called_you">%s called you</string>
|
||||
<string name="MessageRecord_called_s">Called %s</string>
|
||||
<string name="MessageRecord_missed_call_from">Missed call from %s</string>
|
||||
<string name="MessageRecord_follow_setting">Follow Setting</string>
|
||||
<string name="AccessibilityId_follow_setting">Follow setting</string>
|
||||
<string name="MessageRecord_you_disabled_disappearing_messages">You disabled disappearing messages.</string>
|
||||
<string name="MessageRecord_you_turned_off_disappearing_messages">You have turned off disappearing messages.</string>
|
||||
<string name="MessageRecord_you_turned_off_disappearing_messages_1_on_1">You turned off disappearing messages. Messages you send will no longer disappear.</string>
|
||||
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
|
||||
<string name="MessageRecord_s_turned_off_disappearing_messages">%1$s turned off disappearing messages.</string>
|
||||
<string name="MessageRecord_s_turned_off_disappearing_messages_1_on_1">%1$s has turned off disappearing messages. Messages they send will no longer disappear.</string>
|
||||
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
|
||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s">You have set messages to disappear %1$s after they have been %2$s</string>
|
||||
<string name="MessageRecord_you_set_messages_to_disappear_s_after_s_1_on_1">You set your messages to disappear %1$s after they have been %2$s.</string>
|
||||
<string name="MessageRecord_you_changed_messages_to_disappear_s_after_s">You have changed messages to disappear %1$s after they have been %2$s</string>
|
||||
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
|
||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s">%1$s has set messages to disappear %2$s after they have been %3$s</string>
|
||||
<string name="MessageRecord_s_set_messages_to_disappear_s_after_s_1_on_1">%1$s has set their messages to disappear %2$s after they have been %3$s.</string>
|
||||
<string name="MessageRecord_s_changed_messages_to_disappear_s_after_s">%1$s has changed messages to disappear %2$s after they have been %3$s</string>
|
||||
<string name="MessageRecord_s_took_a_screenshot">%1$s took a screenshot.</string>
|
||||
<string name="MessageRecord_media_saved_by_s">Media saved by %1$s.</string>
|
||||
<string name="MessageRecord_state_read">read</string>
|
||||
<string name="MessageRecord_state_sent">sent</string>
|
||||
|
||||
<!-- expiration -->
|
||||
<string name="expiration_off">Off</string>
|
||||
|
Reference in New Issue
Block a user