mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 00:37:47 +00:00
Merge branch 'refactor' of https://github.com/RyanRory/loki-messenger-android into refactor-enc-cleanup
This commit is contained in:
commit
73c4e44711
@ -33,7 +33,7 @@ class DatabaseAttachmentDTO {
|
||||
val isUploaded: Boolean = false
|
||||
|
||||
fun toProto(): SignalServiceProtos.AttachmentPointer? {
|
||||
val builder = org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||
builder.contentType = this.contentType
|
||||
|
||||
if (!this.fileName.isNullOrEmpty()) {
|
||||
@ -46,12 +46,12 @@ class DatabaseAttachmentDTO {
|
||||
builder.size = this.size
|
||||
builder.key = this.key
|
||||
builder.digest = this.digest
|
||||
builder.flags = if (this.isVoiceNote) org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
|
||||
builder.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0
|
||||
|
||||
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
|
||||
if (this.shouldHaveImageSize) {
|
||||
if (this.width < kotlin.Int.MAX_VALUE && this.height < kotlin.Int.MAX_VALUE) {
|
||||
val imageSize: Size = Size(this.width, this.height)
|
||||
if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) {
|
||||
val imageSize= Size(this.width, this.height)
|
||||
val imageWidth = round(imageSize.width.toDouble())
|
||||
val imageHeight = round(imageSize.height.toDouble())
|
||||
if (imageWidth > 0 && imageHeight > 0) {
|
||||
|
@ -1,12 +1,14 @@
|
||||
package org.session.libsession.messaging
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsignal.libsignal.loki.SessionResetProtocol
|
||||
import org.session.libsignal.libsignal.state.*
|
||||
import org.session.libsignal.metadata.certificate.CertificateValidator
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||
|
||||
class Configuration(
|
||||
class MessagingConfiguration(
|
||||
val context: Context,
|
||||
val storage: StorageProtocol,
|
||||
val signalStorage: SignalProtocolStore,
|
||||
val sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||
@ -15,9 +17,10 @@ class Configuration(
|
||||
val certificateValidator: CertificateValidator)
|
||||
{
|
||||
companion object {
|
||||
lateinit var shared: Configuration
|
||||
lateinit var shared: MessagingConfiguration
|
||||
|
||||
fun configure(storage: StorageProtocol,
|
||||
fun configure(context: Context,
|
||||
storage: StorageProtocol,
|
||||
signalStorage: SignalProtocolStore,
|
||||
sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||
messageDataProvider: MessageDataProvider,
|
||||
@ -25,7 +28,7 @@ class Configuration(
|
||||
certificateValidator: CertificateValidator
|
||||
) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
shared = Configuration(storage, signalStorage, sskDatabase, messageDataProvider, sessionResetImp, certificateValidator)
|
||||
shared = MessagingConfiguration(context, storage, signalStorage, sskDatabase, messageDataProvider, sessionResetImp, certificateValidator)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,12 @@ 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.opengroups.OpenGroup
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
|
||||
interface StorageProtocol {
|
||||
|
||||
@ -21,7 +24,7 @@ interface StorageProtocol {
|
||||
|
||||
// Signal Protocol
|
||||
|
||||
fun getOrGenerateRegistrationID(): Int //TODO needs impl
|
||||
fun getOrGenerateRegistrationID(): Int
|
||||
|
||||
// Shared Sender Keys
|
||||
fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey?
|
||||
@ -71,8 +74,20 @@ interface StorageProtocol {
|
||||
fun getReceivedMessageTimestamps(): Set<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
|
||||
// Closed Groups
|
||||
fun getGroup(groupID: String): GroupRecord?
|
||||
fun createGroup(groupId: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>)
|
||||
fun setActive(groupID: String, value: Boolean)
|
||||
fun removeMember(groupID: String, member: Address)
|
||||
fun updateMembers(groupID: String, members: List<Address>)
|
||||
|
||||
|
||||
// Settings
|
||||
fun setProfileSharing(address: Address, value: Boolean)
|
||||
|
||||
// Thread
|
||||
fun getOrCreateThreadIdFor(address: Address): String
|
||||
fun getThreadIdFor(address: Address): String?
|
||||
|
||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
||||
|
@ -4,7 +4,7 @@ import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import java.util.Timer
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import kotlin.concurrent.schedule
|
||||
@ -25,7 +25,7 @@ class JobQueue : JobDelegate {
|
||||
|
||||
fun addWithoutExecuting(job: Job) {
|
||||
job.id = System.currentTimeMillis().toString()
|
||||
Configuration.shared.storage.persist(job)
|
||||
MessagingConfiguration.shared.storage.persist(job)
|
||||
job.delegate = this
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ class JobQueue : JobDelegate {
|
||||
hasResumedPendingJobs = true
|
||||
val allJobTypes = listOf(AttachmentDownloadJob.collection, AttachmentDownloadJob.collection, MessageReceiveJob.collection, MessageSendJob.collection, NotifyPNServerJob.collection)
|
||||
allJobTypes.forEach { type ->
|
||||
val allPendingJobs = Configuration.shared.storage.getAllPendingJobs(type)
|
||||
val allPendingJobs = MessagingConfiguration.shared.storage.getAllPendingJobs(type)
|
||||
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||
job.delegate = this
|
||||
@ -47,12 +47,12 @@ class JobQueue : JobDelegate {
|
||||
}
|
||||
|
||||
override fun handleJobSucceeded(job: Job) {
|
||||
Configuration.shared.storage.markJobAsSucceeded(job)
|
||||
MessagingConfiguration.shared.storage.markJobAsSucceeded(job)
|
||||
}
|
||||
|
||||
override fun handleJobFailed(job: Job, error: Exception) {
|
||||
job.failureCount += 1
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")}
|
||||
storage.persist(job)
|
||||
if (job.failureCount == job.maxFailureCount) {
|
||||
@ -69,7 +69,7 @@ class JobQueue : JobDelegate {
|
||||
|
||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
||||
job.failureCount += 1
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
storage.persist(job)
|
||||
storage.markJobAsFailed(job)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.messaging.jobs
|
||||
|
||||
class MessageReceiveJob : Job {
|
||||
class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
||||
override var delegate: JobDelegate? = null
|
||||
override var id: String? = null
|
||||
override var failureCount: Int = 0
|
||||
|
@ -1,5 +1,9 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
|
||||
sealed class Destination {
|
||||
|
||||
class Contact(val publicKey: String) : Destination()
|
||||
@ -7,9 +11,19 @@ sealed class Destination {
|
||||
class OpenGroup(val channel: Long, val server: String) : Destination()
|
||||
|
||||
companion object {
|
||||
//TODO need to implement the equivalent to TSThread and then implement from(...)
|
||||
fun from(threadID: String): Destination {
|
||||
return Contact(threadID) // Fake for dev
|
||||
fun from(address: Address): Destination {
|
||||
if (address.isContact) {
|
||||
return Contact(address.contactIdentifier())
|
||||
} else if (address.isClosedGroup) {
|
||||
val groupID = address.contactIdentifier().toByteArray()
|
||||
val groupPublicKey = GroupUtil.getDecodedGroupID(groupID)
|
||||
return ClosedGroup(groupPublicKey)
|
||||
} else if (address.isOpenGroup) {
|
||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!!
|
||||
return OpenGroup(openGroup.channel, openGroup.server)
|
||||
} else {
|
||||
throw Exception("TODO: Handle legacy closed groups.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.session.libsession.messaging.messages.control.unused
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ControlMessage
|
||||
import org.session.libsignal.libsignal.IdentityKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
@ -21,7 +21,7 @@ class SessionRequest() : ControlMessage() {
|
||||
if (proto.nullMessage == null) return null
|
||||
val preKeyBundleProto = proto.preKeyBundleMessage ?: return null
|
||||
var registrationID: Int = 0
|
||||
registrationID = Configuration.shared.storage.getOrGenerateRegistrationID() //TODO no implementation for getOrGenerateRegistrationID yet
|
||||
registrationID = MessagingConfiguration.shared.storage.getOrGenerateRegistrationID() //TODO no implementation for getOrGenerateRegistrationID yet
|
||||
//TODO just confirm if the above code does the equivalent to swift below:
|
||||
/*iOS code: Configuration.shared.storage.with { transaction in
|
||||
registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction)
|
||||
|
@ -1,8 +1,6 @@
|
||||
package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import android.content.Context
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
@ -46,7 +44,7 @@ class LinkPreview() {
|
||||
title?.let { linkPreviewProto.title = title }
|
||||
val attachmentID = attachmentID
|
||||
attachmentID?.let {
|
||||
val attachmentProto = Configuration.shared.messageDataProvider.getAttachment(attachmentID)
|
||||
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachment(attachmentID)
|
||||
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
|
||||
}
|
||||
// Build
|
||||
|
@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
|
||||
@ -48,7 +48,7 @@ class Quote() {
|
||||
quoteProto.id = timestamp
|
||||
quoteProto.author = publicKey
|
||||
text?.let { quoteProto.text = text }
|
||||
addAttachmentsIfNeeded(quoteProto, Configuration.shared.messageDataProvider)
|
||||
addAttachmentsIfNeeded(quoteProto, MessagingConfiguration.shared.messageDataProvider)
|
||||
// Build
|
||||
try {
|
||||
return quoteProto.build()
|
||||
|
@ -2,8 +2,7 @@ package org.session.libsession.messaging.messages.visible
|
||||
|
||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
@ -52,7 +51,7 @@ class VisibleMessage : Message() {
|
||||
return false
|
||||
}
|
||||
|
||||
fun toProto(): SignalServiceProtos.Content? {
|
||||
override fun toProto(): SignalServiceProtos.Content? {
|
||||
val proto = SignalServiceProtos.Content.newBuilder()
|
||||
var attachmentIDs = this.attachmentIDs
|
||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||
@ -91,7 +90,7 @@ class VisibleMessage : Message() {
|
||||
}
|
||||
}
|
||||
//Attachments
|
||||
val attachments = attachmentIDs.mapNotNull { Configuration.shared.messageDataProvider.getAttachment(it) }
|
||||
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachment(it) }
|
||||
if (!attachments.all { it.isUploaded }) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
//TODO equivalent to iOS's preconditionFailure
|
||||
|
@ -5,11 +5,10 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.util.Base64
|
||||
@ -56,7 +55,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
// region Public API
|
||||
public fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> {
|
||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
||||
val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||
if (lastMessageServerID != null) {
|
||||
@ -161,7 +160,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
|
||||
public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>()
|
||||
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||
if (lastDeletionServerID != null) {
|
||||
@ -193,7 +192,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
|
||||
public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
||||
Thread {
|
||||
@ -287,7 +286,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
||||
val profilePictureURL = info["avatar"] as String
|
||||
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
||||
Configuration.shared.storage.setUserCount(channel, server, memberCount)
|
||||
MessagingConfiguration.shared.storage.setUserCount(channel, server, memberCount)
|
||||
publicChatInfo
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
||||
@ -298,7 +297,7 @@ object OpenGroupAPI: DotNetAPI() {
|
||||
}
|
||||
|
||||
public fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
storage.setUserCount(channel, server, info.memberCount)
|
||||
storage.updateTitle(groupID, info.displayName)
|
||||
// Download and update profile picture if needed
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.messaging.opengroups
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.util.Hex
|
||||
@ -24,7 +24,7 @@ public data class OpenGroupMessage(
|
||||
// region Settings
|
||||
companion object {
|
||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||
// Validation
|
||||
if (!message.isValid()) { return null } // Should be valid at this point
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||
@ -39,7 +39,7 @@ object MessageReceiver {
|
||||
}
|
||||
|
||||
internal fun parse(data: ByteArray, openGroupServerID: Long?): Pair<Message, SignalServiceProtos.Content> {
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()
|
||||
val isOpenGroupMessage = openGroupServerID != null
|
||||
// Parse the envelope
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error
|
||||
import org.session.libsession.utilities.AESGCM
|
||||
|
||||
@ -21,26 +21,26 @@ import javax.crypto.spec.SecretKeySpec
|
||||
object MessageReceiverDecryption {
|
||||
|
||||
internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
|
||||
val storage = Configuration.shared.signalStorage
|
||||
val sskDatabase = Configuration.shared.sskDatabase
|
||||
val sessionResetImp = Configuration.shared.sessionResetImp
|
||||
val certificateValidator = Configuration.shared.certificateValidator
|
||||
val storage = MessagingConfiguration.shared.signalStorage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
|
||||
val certificateValidator = MessagingConfiguration.shared.certificateValidator
|
||||
val data = envelope.content
|
||||
if (data.count() == 0) { throw Error.NoData }
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
|
||||
val localAddress = SignalServiceAddress(userPublicKey)
|
||||
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
||||
val result = cipher.decrypt(SignalServiceEnvelope(envelope))
|
||||
return Pair(result, result.sender)
|
||||
return Pair(ByteArray(1), result.sender) // TODO: Return real plaintext
|
||||
}
|
||||
|
||||
internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
|
||||
// 1. ) Check preconditions
|
||||
val groupPublicKey = envelope.source
|
||||
if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey }
|
||||
if (!MessagingConfiguration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey }
|
||||
val data = envelope.content
|
||||
if (data.count() == 0) { throw Error.NoData }
|
||||
val groupPrivateKey = Configuration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey
|
||||
val groupPrivateKey = MessagingConfiguration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey
|
||||
// 2. ) Parse the wrapper
|
||||
val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
|
||||
val ivAndCiphertext = wrapper.ciphertext.toByteArray()
|
||||
@ -54,7 +54,7 @@ object MessageReceiverDecryption {
|
||||
// 4. ) Parse the closed group ciphertext message
|
||||
val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed
|
||||
val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
|
||||
if (senderPublicKey == Configuration.shared.storage.getUserPublicKey()) { throw Error.SelfSend }
|
||||
if (senderPublicKey == MessagingConfiguration.shared.storage.getUserPublicKey()) { throw Error.SelfSend }
|
||||
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
|
||||
val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex)
|
||||
// 6. ) Return
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||
@ -9,9 +9,9 @@ import org.session.libsession.messaging.messages.control.ReadReceipt
|
||||
import org.session.libsession.messaging.messages.control.TypingIndicator
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.utilities.LKGroupUtilities
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.libsignal.util.Hex
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||
@ -37,7 +37,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||
@ -89,8 +89,8 @@ private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate)
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
||||
val storage = Configuration.shared.storage
|
||||
val sskDatabase = Configuration.shared.sskDatabase
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.New
|
||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||
val name = kind.name
|
||||
@ -122,27 +122,24 @@ private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
||||
MessageSender.requestSenderKey(groupPublicKey, publicKey)
|
||||
}
|
||||
// Create the group
|
||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
if (groupDB.getGroup(groupID).orNull() != null) {
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||
if (storage.getGroup(groupID) != null) {
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
storage.updateTitle(groupID, name)
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
} else {
|
||||
groupDB.create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
}
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Notify the user
|
||||
/* TODO
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) {
|
||||
|
@ -1,10 +1,9 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.google.protobuf.MessageOrBuilder
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
@ -64,7 +63,7 @@ object MessageSender {
|
||||
fun sendToSnodeDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val promise = deferred.promise
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val preconditionFailure = Exception("Destination should not be open groups!")
|
||||
var snodeMessage: SnodeMessage? = null
|
||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
||||
@ -152,7 +151,7 @@ object MessageSender {
|
||||
fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val promise = deferred.promise
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
message.sender = storage.getUserPublicKey()
|
||||
|
@ -2,16 +2,17 @@
|
||||
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||
import org.session.libsession.utilities.LKGroupUtilities
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
|
||||
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.util.Hex
|
||||
@ -26,8 +27,9 @@ import java.util.*
|
||||
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
// Prepare
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val members = members
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
// Generate a key pair for the group
|
||||
val groupKeyPair = Curve.generateKeyPair()
|
||||
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
|
||||
@ -41,12 +43,9 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
// Create the group
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
/* TODO:
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
*/
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||
// Send a closed group update message to all members using established channels
|
||||
val promises = mutableListOf<Promise<Unit, Exception>>()
|
||||
for (member in members) {
|
||||
@ -55,16 +54,17 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
senderKeys, membersAsData, adminsAsData)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val promise = MessageSender.sendNonDurably(closedGroupUpdate, threadID)
|
||||
val address = Address.fromSerialized(member)
|
||||
val promise = MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||
promises.add(promise)
|
||||
}
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
Configuration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
|
||||
MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Notify the user
|
||||
val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
/* TODO
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
*/
|
||||
// Fulfill the promise
|
||||
@ -75,46 +75,47 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
||||
|
||||
fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
||||
val sskDatabase = Configuration.shared.sskDatabase
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID)
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return deferred.reject(Error.NoThread)
|
||||
deferred.reject(Error.NoThread)
|
||||
return deferred.promise
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val newMembers = members.minus(oldMembers)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
|
||||
val groupPrivateKey = sskDatabase.getClosedGroupPrivateKey(groupPublicKey)
|
||||
if (groupPrivateKey == null) {
|
||||
Log.d("Loki", "Couldn't get private key for closed group.")
|
||||
return@Thread deferred.reject(Error.NoPrivateKey)
|
||||
deferred.reject(Error.NoPrivateKey)
|
||||
return deferred.promise
|
||||
}
|
||||
val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
|
||||
val removedMembers = oldMembers.minus(members)
|
||||
val isUserLeaving = removedMembers.contains(userPublicKey)
|
||||
var newSenderKeys = listOf<ClosedGroupSenderKey>()
|
||||
val newSenderKeys: List<ClosedGroupSenderKey>
|
||||
if (wasAnyUserRemoved) {
|
||||
if (isUserLeaving && removedMembers.count() != 1) {
|
||||
Log.d("Loki", "Can't remove self and others simultaneously.")
|
||||
return@Thread deferred.reject(Error.InvalidUpdate)
|
||||
deferred.reject(Error.InvalidClosedGroupUpdate)
|
||||
return deferred.promise
|
||||
}
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
// Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
|
||||
for (member in oldMembers) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
|
||||
val promises = oldMembers.map { member ->
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
|
||||
name, setOf(), membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(member)
|
||||
MessageSender.sendNonDurably(closedGroupUpdate, address).get()
|
||||
}
|
||||
|
||||
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
for (pair in allOldRatchets) {
|
||||
val senderPublicKey = pair.first
|
||||
@ -128,30 +129,30 @@ fun MessageSender.update(groupPublicKey: String, members: Collection<String>, na
|
||||
// send it out to all members (minus the removed ones) using established channels.
|
||||
if (isUserLeaving) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
storage.setActive(groupID, false)
|
||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
} else {
|
||||
// Send closed group update messages to any new members using established channels
|
||||
for (member in newMembers) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(member)
|
||||
MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||
}
|
||||
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
for (member in members) {
|
||||
if (member == userPublicKey) { continue }
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(member)
|
||||
MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||
}
|
||||
}
|
||||
} else if (newMembers.isNotEmpty()) {
|
||||
@ -161,49 +162,68 @@ fun MessageSender.update(groupPublicKey: String, members: Collection<String>, na
|
||||
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
|
||||
}
|
||||
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
newSenderKeys, membersAsData, adminsAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, newMembers)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(groupID)
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
// Send closed group update messages to the new members using established channels
|
||||
var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
allSenderKeys = allSenderKeys.union(newSenderKeys)
|
||||
for (member in newMembers) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(member)
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
}
|
||||
} else {
|
||||
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
allSenderKeys, membersAsData, adminsAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
val address = Address.fromSerialized(groupID)
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
storage.updateTitle(groupID, name)
|
||||
if (!isUserLeaving) {
|
||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||
/* TODO
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||
*/
|
||||
deferred.resolve(Unit)
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun MessageSender.leave(groupPublicKey: String) {
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey()!!
|
||||
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||
val group = storage.getGroup(groupID)
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
val oldMembers = group.members.map { it.serialize() }.toSet()
|
||||
val newMembers = oldMembers.minus(userPublicKey)
|
||||
return update(groupPublicKey, newMembers, name).get()
|
||||
}
|
||||
|
||||
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) {
|
||||
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
|
||||
// Send the request
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
|
||||
val closedGroupUpdate = ClosedGroupUpdate()
|
||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||
MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey))
|
||||
MessageSender.send(closedGroupUpdate, address)
|
||||
}
|
@ -1,35 +1,39 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.Destination
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||
|
||||
fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, threadID: String) {
|
||||
fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address) {
|
||||
prep(attachments, message)
|
||||
send(message, threadID)
|
||||
send(message, address)
|
||||
}
|
||||
|
||||
fun MessageSender.send(message: Message, threadID: String) {
|
||||
fun MessageSender.send(message: Message, address: Address) {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(threadID)
|
||||
val destination = Destination.from(address)
|
||||
val job = MessageSendJob(message, destination)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
|
||||
fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, threadID: String): Promise<Unit, Exception> {
|
||||
fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address): Promise<Unit, Exception> {
|
||||
prep(attachments, message)
|
||||
// TODO: Deal with attachments
|
||||
return sendNonDurably(message, threadID)
|
||||
return sendNonDurably(message, address)
|
||||
}
|
||||
|
||||
fun MessageSender.sendNonDurably(message: Message, threadID: String): Promise<Unit, Exception> {
|
||||
fun MessageSender.sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
|
||||
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
|
||||
message.threadID = threadID
|
||||
val destination = Destination.from(threadID)
|
||||
val destination = Destination.from(address)
|
||||
return MessageSender.send(message, destination)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.session.libsession.messaging.sending_receiving
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.messages.Message
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||
import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil
|
||||
@ -21,11 +21,11 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
object MessageSenderEncryption {
|
||||
|
||||
internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{
|
||||
val storage = Configuration.shared.signalStorage
|
||||
val sskDatabase = Configuration.shared.sskDatabase
|
||||
val sessionResetImp = Configuration.shared.sessionResetImp
|
||||
val storage = MessagingConfiguration.shared.signalStorage
|
||||
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||
val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
|
||||
val localAddress = SignalServiceAddress(recipientPublicKey)
|
||||
val certificateValidator = Configuration.shared.certificateValidator
|
||||
val certificateValidator = MessagingConfiguration.shared.certificateValidator
|
||||
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
||||
val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1)
|
||||
val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey)
|
||||
@ -36,7 +36,7 @@ object MessageSenderEncryption {
|
||||
|
||||
internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray {
|
||||
// 1. ) Encrypt the data with the user's sender key
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey
|
||||
val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey)
|
||||
val ivAndCiphertext = ciphertextAndKeyIndex.first
|
||||
val keyIndex = ciphertextAndKeyIndex.second
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.session.libsession.messaging.sending_receiving.notifications
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.*
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.util.JsonUtil
|
||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||
import java.io.IOException
|
||||
|
||||
object PushNotificationAPI {
|
||||
val context = MessagingConfiguration.shared.context
|
||||
val server = "https://live.apns.getsession.org"
|
||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
private val maxRetryCount = 4
|
||||
@ -46,8 +46,8 @@ object PushNotificationAPI {
|
||||
}
|
||||
}
|
||||
// Unsubscribe from all closed groups
|
||||
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
||||
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()!!
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||
}
|
||||
@ -76,7 +76,7 @@ object PushNotificationAPI {
|
||||
}
|
||||
}
|
||||
// Subscribe to all closed groups
|
||||
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
||||
}
|
||||
|
@ -0,0 +1,90 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import android.os.Handler
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.successBackground
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.util.Base64
|
||||
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
|
||||
|
||||
class ClosedGroupPoller {
|
||||
private var isPolling = false
|
||||
private val handler: Handler by lazy { Handler() }
|
||||
|
||||
private val task = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
poll()
|
||||
handler.postDelayed(this, ClosedGroupPoller.pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollInterval: Long = 2 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Error
|
||||
class InsufficientSnodesException() : Exception("No snodes left to poll.")
|
||||
class PollingCanceledException() : Exception("Polling canceled.")
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
public fun startIfNeeded() {
|
||||
if (isPolling) { return }
|
||||
isPolling = true
|
||||
task.run()
|
||||
}
|
||||
|
||||
public fun pollOnce(): List<Promise<Unit, Exception>> {
|
||||
if (isPolling) { return listOf() }
|
||||
isPolling = true
|
||||
return poll()
|
||||
}
|
||||
|
||||
public fun stopIfNeeded() {
|
||||
isPolling = false
|
||||
handler.removeCallbacks(task)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Private API
|
||||
private fun poll(): List<Promise<Unit, Exception>> {
|
||||
if (!isPolling) { return listOf() }
|
||||
val publicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||
return publicKeys.map { publicKey ->
|
||||
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
|
||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
||||
if (!isPolling) { throw PollingCanceledException() }
|
||||
SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) }
|
||||
}
|
||||
promise.successBackground { messages ->
|
||||
if (messages.isNotEmpty()) {
|
||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
||||
}
|
||||
messages.forEach { message ->
|
||||
val rawMessageAsJSON = message as? Map<*, *>
|
||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||
val data = base64EncodedData?.let { Base64.decode(it) } ?: return@forEach
|
||||
val job = MessageReceiveJob(MessageWrapper.unwrap(data), false)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
}
|
||||
promise.fail {
|
||||
Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.")
|
||||
}
|
||||
promise.map { Unit }
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,307 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.successBackground
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.service.api.messages.SignalServiceContent
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.api.messages.multidevice.SentTranscriptMessage
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI
|
||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage
|
||||
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class OpenGroupPoller(private val context: Context, private val group: PublicChat) {
|
||||
private val handler by lazy { Handler() }
|
||||
private var hasStarted = false
|
||||
private var isPollOngoing = false
|
||||
public var isCaughtUp = false
|
||||
|
||||
// region Convenience
|
||||
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
private var displayNameUpdatees = setOf<String>()
|
||||
|
||||
private val api: PublicChatAPI
|
||||
get() = {
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context)
|
||||
val openGroupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase)
|
||||
}()
|
||||
// endregion
|
||||
|
||||
// region Tasks
|
||||
private val pollForNewMessagesTask = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
pollForNewMessages()
|
||||
handler.postDelayed(this, pollForNewMessagesInterval)
|
||||
}
|
||||
}
|
||||
|
||||
private val pollForDeletedMessagesTask = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
pollForDeletedMessages()
|
||||
handler.postDelayed(this, pollForDeletedMessagesInterval)
|
||||
}
|
||||
}
|
||||
|
||||
private val pollForModeratorsTask = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
pollForModerators()
|
||||
handler.postDelayed(this, pollForModeratorsInterval)
|
||||
}
|
||||
}
|
||||
|
||||
private val pollForDisplayNamesTask = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
pollForDisplayNames()
|
||||
handler.postDelayed(this, pollForDisplayNamesInterval)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollForNewMessagesInterval: Long = 4 * 1000
|
||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted) return
|
||||
pollForNewMessagesTask.run()
|
||||
pollForDeletedMessagesTask.run()
|
||||
pollForModeratorsTask.run()
|
||||
pollForDisplayNamesTask.run()
|
||||
hasStarted = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
handler.removeCallbacks(pollForNewMessagesTask)
|
||||
handler.removeCallbacks(pollForDeletedMessagesTask)
|
||||
handler.removeCallbacks(pollForModeratorsTask)
|
||||
handler.removeCallbacks(pollForDisplayNamesTask)
|
||||
hasStarted = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Polling
|
||||
private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage {
|
||||
val id = group.id.toByteArray()
|
||||
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
|
||||
val quote = if (message.quote != null) {
|
||||
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val attachments = message.attachments.mapNotNull { attachment ->
|
||||
if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
||||
SignalServiceAttachmentPointer(
|
||||
attachment.serverID,
|
||||
attachment.contentType,
|
||||
ByteArray(0),
|
||||
Optional.of(attachment.size),
|
||||
Optional.absent(),
|
||||
attachment.width, attachment.height,
|
||||
Optional.absent(),
|
||||
Optional.of(attachment.fileName),
|
||||
false,
|
||||
Optional.fromNullable(attachment.caption),
|
||||
attachment.url)
|
||||
}
|
||||
val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview }
|
||||
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
|
||||
if (linkPreview != null) {
|
||||
val attachment = SignalServiceAttachmentPointer(
|
||||
linkPreview.serverID,
|
||||
linkPreview.contentType,
|
||||
ByteArray(0),
|
||||
Optional.of(linkPreview.size),
|
||||
Optional.absent(),
|
||||
linkPreview.width, linkPreview.height,
|
||||
Optional.absent(),
|
||||
Optional.of(linkPreview.fileName),
|
||||
false,
|
||||
Optional.fromNullable(linkPreview.caption),
|
||||
linkPreview.url)
|
||||
signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
|
||||
}
|
||||
val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
||||
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
|
||||
}
|
||||
|
||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
||||
fun processIncomingMessage(message: PublicChatMessage) {
|
||||
// If the sender of the current message is not a slave device, set the display name in the database
|
||||
val masterHexEncodedPublicKey = MultiDeviceProtocol.shared.getMasterDevice(message.senderPublicKey)
|
||||
if (masterHexEncodedPublicKey == null) {
|
||||
val senderDisplayName = "${message.displayName} (...${message.senderPublicKey.takeLast(8)})"
|
||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.senderPublicKey, senderDisplayName)
|
||||
}
|
||||
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message.senderPublicKey
|
||||
val serviceDataMessage = getDataMessage(message)
|
||||
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.serverTimestamp, false, false)
|
||||
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
|
||||
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
||||
} else {
|
||||
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
||||
}
|
||||
// Update profile picture if needed
|
||||
val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false)
|
||||
if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) {
|
||||
val profileKey = message.profilePicture!!.profileKey
|
||||
val url = message.profilePicture!!.url
|
||||
if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) {
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
database.setProfileKey(senderAsRecipient, profileKey)
|
||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun processOutgoingMessage(message: PublicChatMessage) {
|
||||
val messageServerID = message.serverID ?: return
|
||||
val messageID = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID)
|
||||
var isDuplicate = false
|
||||
if (messageID != null) {
|
||||
isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) >= 0
|
||||
|| DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) >= 0
|
||||
}
|
||||
if (isDuplicate) { return }
|
||||
if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return }
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val dataMessage = getDataMessage(message)
|
||||
SessionMetaProtocol.dropFromTimestampCacheIfNeeded(message.serverTimestamp)
|
||||
val transcript = SentTranscriptMessage(userHexEncodedPublicKey, message.serverTimestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(userHexEncodedPublicKey, false))
|
||||
transcript.messageServerID = messageServerID
|
||||
if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
|
||||
PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)
|
||||
} else {
|
||||
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
|
||||
}
|
||||
// If we got a message from our master device then make sure our mapping stays in sync
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.senderPublicKey), false)
|
||||
if (recipient.isUserMasterDevice && message.profilePicture != null) {
|
||||
val profileKey = message.profilePicture!!.profileKey
|
||||
val url = message.profilePicture!!.url
|
||||
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) {
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
database.setProfileKey(recipient, profileKey)
|
||||
database.setProfileAvatar(recipient, url)
|
||||
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isPollOngoing) { return Promise.of(Unit) }
|
||||
isPollOngoing = true
|
||||
val userDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userHexEncodedPublicKey)
|
||||
var uniqueDevices = setOf<String>()
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB)
|
||||
// Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below
|
||||
val promise = api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages ->
|
||||
/*
|
||||
if (messages.isNotEmpty()) {
|
||||
// We need to fetch the device mapping for any devices we don't have
|
||||
uniqueDevices = messages.map { it.senderPublicKey }.toSet()
|
||||
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && FileServerAPI.shared.hasDeviceLinkCacheExpired(publicKey = it) }
|
||||
if (devicesToUpdate.isNotEmpty()) {
|
||||
return@bind FileServerAPI.shared.getDeviceLinks(devicesToUpdate.toSet()).then { messages }
|
||||
}
|
||||
}
|
||||
*/
|
||||
Promise.of(messages)
|
||||
}
|
||||
promise.successBackground {
|
||||
/*
|
||||
val newDisplayNameUpdatees = uniqueDevices.mapNotNull {
|
||||
// This will return null if the current device is a master device
|
||||
MultiDeviceProtocol.shared.getMasterDevice(it)
|
||||
}.toSet()
|
||||
// Fetch the display names of the master devices
|
||||
displayNameUpdatees = displayNameUpdatees.union(newDisplayNameUpdatees)
|
||||
*/
|
||||
}
|
||||
promise.successBackground { messages ->
|
||||
// Process messages in the background
|
||||
messages.forEach { message ->
|
||||
if (userDevices.contains(message.senderPublicKey)) {
|
||||
processOutgoingMessage(message)
|
||||
} else {
|
||||
processIncomingMessage(message)
|
||||
}
|
||||
}
|
||||
isCaughtUp = true
|
||||
isPollOngoing = false
|
||||
}
|
||||
promise.fail {
|
||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
||||
isPollOngoing = false
|
||||
}
|
||||
return promise.map { Unit }
|
||||
}
|
||||
|
||||
private fun pollForDisplayNames() {
|
||||
if (displayNameUpdatees.isEmpty()) { return }
|
||||
val hexEncodedPublicKeys = displayNameUpdatees
|
||||
displayNameUpdatees = setOf()
|
||||
api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping ->
|
||||
for (pair in mapping.entries) {
|
||||
val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})"
|
||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName)
|
||||
}
|
||||
}.fail {
|
||||
displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForDeletedMessages() {
|
||||
api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs ->
|
||||
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) }
|
||||
val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
val mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
deletedMessageIDs.forEach {
|
||||
smsMessageDatabase.deleteMessage(it)
|
||||
mmsMessageDatabase.delete(it)
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForModerators() {
|
||||
api.getModerators(group.channel, group.server)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import nl.komponents.kovenant.*
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||
import org.session.libsession.snode.Snode
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeConfiguration
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.util.Base64
|
||||
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
|
||||
private class PromiseCanceledException : Exception("Promise canceled.")
|
||||
|
||||
class Poller {
|
||||
private val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var hasStarted: Boolean = false
|
||||
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
||||
public var isCaughtUp = false
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val retryInterval: Long = 1 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted) { return }
|
||||
Log.d("Loki", "Started polling.")
|
||||
hasStarted = true
|
||||
setUpPolling()
|
||||
}
|
||||
|
||||
fun stopIfNeeded() {
|
||||
Log.d("Loki", "Stopped polling.")
|
||||
hasStarted = false
|
||||
usedSnodes.clear()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Private API
|
||||
private fun setUpPolling() {
|
||||
if (!hasStarted) { return; }
|
||||
val thread = Thread.currentThread()
|
||||
SnodeAPI.getSwarm(userPublicKey).bind(SnodeAPI.messagePollingContext) {
|
||||
usedSnodes.clear()
|
||||
val deferred = deferred<Unit, Exception>(SnodeAPI.messagePollingContext)
|
||||
pollNextSnode(deferred)
|
||||
deferred.promise
|
||||
}.always {
|
||||
Timer().schedule(object : TimerTask() {
|
||||
|
||||
override fun run() {
|
||||
thread.run { setUpPolling() }
|
||||
}
|
||||
}, retryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollNextSnode(deferred: Deferred<Unit, Exception>) {
|
||||
val swarm = SnodeConfiguration.shared.storage.getSwarm(userPublicKey) ?: setOf()
|
||||
val unusedSnodes = swarm.subtract(usedSnodes)
|
||||
if (unusedSnodes.isNotEmpty()) {
|
||||
val index = SecureRandom().nextInt(unusedSnodes.size)
|
||||
val nextSnode = unusedSnodes.elementAt(index)
|
||||
usedSnodes.add(nextSnode)
|
||||
Log.d("Loki", "Polling $nextSnode.")
|
||||
poll(nextSnode, deferred).fail { exception ->
|
||||
if (exception is PromiseCanceledException) {
|
||||
Log.d("Loki", "Polling $nextSnode canceled.")
|
||||
} else {
|
||||
Log.d("Loki", "Polling $nextSnode failed; dropping it and switching to next snode.")
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, userPublicKey)
|
||||
pollNextSnode(deferred)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isCaughtUp = true
|
||||
deferred.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
|
||||
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
|
||||
return SnodeAPI.getRawMessages(snode, userPublicKey).bind(SnodeAPI.messagePollingContext) { rawResponse ->
|
||||
isCaughtUp = true
|
||||
if (deferred.promise.isDone()) {
|
||||
task { Unit } // The long polling connection has been canceled; don't recurse
|
||||
} else {
|
||||
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
||||
messages.forEach { message ->
|
||||
val rawMessageAsJSON = message as? Map<*, *>
|
||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||
val data = base64EncodedData?.let { Base64.decode(it) } ?: return@forEach
|
||||
val job = MessageReceiveJob(MessageWrapper.unwrap(data), false)
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
poll(snode, deferred)
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Pair
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.session.libsession.utilities.DelimiterUtil.escape
|
||||
import org.session.libsession.utilities.DelimiterUtil.split
|
||||
import org.session.libsession.utilities.DelimiterUtil.unescape
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.NumberUtil.isValidEmail
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.internal.util.Util
|
||||
import java.lang.AssertionError
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Address private constructor(address: String) : Parcelable, Comparable<Address?> {
|
||||
private val address: String = address.toLowerCase()
|
||||
|
||||
constructor(`in`: Parcel) : this(`in`.readString()!!) {}
|
||||
|
||||
val isGroup: Boolean
|
||||
get() = GroupUtil.isEncodedGroup(address)
|
||||
val isClosedGroup: Boolean
|
||||
get() = GroupUtil.isClosedGroup(address)
|
||||
val isOpenGroup: Boolean
|
||||
get() = GroupUtil.isOpenGroup(address)
|
||||
val isMmsGroup: Boolean
|
||||
get() = GroupUtil.isMmsGroup(address)
|
||||
val isContact: Boolean
|
||||
get() = !isGroup
|
||||
|
||||
fun contactIdentifier(): String {
|
||||
if (!isContact && !isOpenGroup) {
|
||||
if (isGroup) throw AssertionError("Not e164, is group")
|
||||
throw AssertionError("Not e164, unknown")
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return address
|
||||
}
|
||||
|
||||
fun serialize(): String {
|
||||
return address
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return if (other == null || other !is Address) false else address == other.address
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return address.hashCode()
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeString(address)
|
||||
}
|
||||
|
||||
override fun compareTo(other: Address?): Int {
|
||||
return address.compareTo(other?.address!!)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class ExternalAddressFormatter internal constructor(localCountryCode: String, countryCode: Boolean) {
|
||||
private val localNumber: Optional<PhoneNumber>
|
||||
private val localCountryCode: String
|
||||
private val ALPHA_PATTERN = Pattern.compile("[a-zA-Z]")
|
||||
fun format(number: String?): String {
|
||||
return number ?: "Unknown"
|
||||
}
|
||||
|
||||
private fun parseAreaCode(e164Number: String, countryCode: Int): String? {
|
||||
when (countryCode) {
|
||||
1 -> return e164Number.substring(2, 5)
|
||||
55 -> return e164Number.substring(3, 5)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun applyAreaCodeRules(localNumber: Optional<PhoneNumber>, testNumber: String): String {
|
||||
if (!localNumber.isPresent || !localNumber.get().areaCode.isPresent) {
|
||||
return testNumber
|
||||
}
|
||||
val matcher: Matcher
|
||||
when (localNumber.get().countryCode) {
|
||||
1 -> {
|
||||
matcher = US_NO_AREACODE.matcher(testNumber)
|
||||
if (matcher.matches()) {
|
||||
return localNumber.get().areaCode.toString() + matcher.group()
|
||||
}
|
||||
}
|
||||
55 -> {
|
||||
matcher = BR_NO_AREACODE.matcher(testNumber)
|
||||
if (matcher.matches()) {
|
||||
return localNumber.get().areaCode.toString() + matcher.group()
|
||||
}
|
||||
}
|
||||
}
|
||||
return testNumber
|
||||
}
|
||||
|
||||
private class PhoneNumber internal constructor(val e164Number: String, val countryCode: Int, areaCode: String?) {
|
||||
val areaCode: Optional<String?>
|
||||
|
||||
init {
|
||||
this.areaCode = Optional.fromNullable(areaCode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ExternalAddressFormatter::class.java.simpleName
|
||||
private val SHORT_COUNTRIES: HashSet<String?> = object : HashSet<String?>() {
|
||||
init {
|
||||
add("NU")
|
||||
add("TK")
|
||||
add("NC")
|
||||
add("AC")
|
||||
}
|
||||
}
|
||||
private val US_NO_AREACODE = Pattern.compile("^(\\d{7})$")
|
||||
private val BR_NO_AREACODE = Pattern.compile("^(9?\\d{8})$")
|
||||
}
|
||||
|
||||
init {
|
||||
localNumber = Optional.absent()
|
||||
this.localCountryCode = localCountryCode
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CREATOR: Parcelable.Creator<Address?> = object : Parcelable.Creator<Address?> {
|
||||
override fun createFromParcel(`in`: Parcel): Address {
|
||||
return Address(`in`)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Address?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
val UNKNOWN = Address("Unknown")
|
||||
private val TAG = Address::class.java.simpleName
|
||||
private val cachedFormatter = AtomicReference<Pair<String, ExternalAddressFormatter>>()
|
||||
fun fromSerialized(serialized: String): Address {
|
||||
return Address(serialized)
|
||||
}
|
||||
|
||||
fun fromExternal(context: Context, external: String?): Address {
|
||||
return fromSerialized(external!!)
|
||||
}
|
||||
|
||||
fun fromSerializedList(serialized: String, delimiter: Char): List<Address> {
|
||||
val escapedAddresses = split(serialized, delimiter)
|
||||
val addresses: MutableList<Address> = LinkedList()
|
||||
for (escapedAddress in escapedAddresses) {
|
||||
addresses.add(fromSerialized(unescape(escapedAddress, delimiter)))
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
fun toSerializedList(addresses: List<Address>, delimiter: Char): String {
|
||||
Collections.sort(addresses)
|
||||
val escapedAddresses: MutableList<String> = LinkedList()
|
||||
for (address in addresses) {
|
||||
escapedAddresses.add(escape(address.serialize(), delimiter))
|
||||
}
|
||||
return Util.join(escapedAddresses, delimiter.toString() + "")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.session.libsession.messaging.threads
|
||||
|
||||
import android.text.TextUtils
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class GroupRecord(
|
||||
val encodedId: String, val title: String, members: String?, val avatar: ByteArray,
|
||||
val avatarId: Long, val avatarKey: ByteArray, val avatarContentType: String,
|
||||
val relay: String, val isActive: Boolean, val avatarDigest: ByteArray, val isMms: Boolean, val url: String, admins: String?,
|
||||
) {
|
||||
var members: List<Address> = LinkedList<Address>()
|
||||
var admins: List<Address> = LinkedList<Address>()
|
||||
fun getId(): ByteArray {
|
||||
return try {
|
||||
GroupUtil.getDecodedGroupIDAsData(encodedId.toByteArray())
|
||||
} catch (ioe: IOException) {
|
||||
throw AssertionError(ioe)
|
||||
}
|
||||
}
|
||||
|
||||
val isOpenGroup: Boolean
|
||||
get() = Address.fromSerialized(encodedId).isOpenGroup
|
||||
val isClosedGroup: Boolean
|
||||
get() = Address.fromSerialized(encodedId).isClosedGroup
|
||||
|
||||
init {
|
||||
if (!TextUtils.isEmpty(members)) {
|
||||
this.members = Address.fromSerializedList(members!!, ',')
|
||||
}
|
||||
if (!TextUtils.isEmpty(admins)) {
|
||||
this.admins = Address.fromSerializedList(admins!!, ',')
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import okhttp3.MultipartBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||
@ -57,7 +57,7 @@ open class DotNetAPI {
|
||||
public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
||||
|
||||
public fun getAuthToken(server: String): Promise<String, Exception> {
|
||||
val storage = Configuration.shared.storage
|
||||
val storage = MessagingConfiguration.shared.storage
|
||||
val token = storage.getAuthToken(server)
|
||||
if (token != null) { return Promise.of(token) }
|
||||
// Avoid multiple token requests to the server by caching
|
||||
@ -76,7 +76,7 @@ open class DotNetAPI {
|
||||
|
||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
||||
val userKeyPair = Configuration.shared.storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val userKeyPair = MessagingConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val parameters: Map<String, Any> = mapOf( "pubKey" to userKeyPair.hexEncodedPublicKey )
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json ->
|
||||
try {
|
||||
@ -102,7 +102,7 @@ open class DotNetAPI {
|
||||
|
||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.Generic
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: throw Error.Generic
|
||||
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token }
|
||||
}
|
||||
@ -141,7 +141,7 @@ open class DotNetAPI {
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
Configuration.shared.storage.setAuthToken(server, null)
|
||||
MessagingConfiguration.shared.storage.setAuthToken(server, null)
|
||||
throw Error.TokenExpired
|
||||
}
|
||||
}
|
||||
@ -256,7 +256,7 @@ open class DotNetAPI {
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
Configuration.shared.storage.setAuthToken(server, null)
|
||||
MessagingConfiguration.shared.storage.setAuthToken(server, null)
|
||||
}
|
||||
throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.")
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package org.session.libsession.messaging.utilities
|
||||
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
|
||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
||||
|
||||
import org.session.libsession.messaging.Configuration
|
||||
import org.session.libsession.messaging.MessagingConfiguration
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.metadata.SignalProtos
|
||||
@ -37,12 +37,12 @@ object UnidentifiedAccessUtil {
|
||||
}
|
||||
|
||||
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
|
||||
val theirProfileKey = Configuration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
|
||||
val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
}
|
||||
|
||||
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
return sodium.randomBytesBuf(16)
|
||||
}
|
||||
@ -50,7 +50,7 @@ object UnidentifiedAccessUtil {
|
||||
}
|
||||
|
||||
private fun getUnidentifiedAccessCertificate(): ByteArray? {
|
||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()
|
||||
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||
if (userPublicKey != null) {
|
||||
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
|
||||
return certificate.toByteArray()
|
||||
|
@ -2,17 +2,13 @@
|
||||
|
||||
package org.session.libsession.snode
|
||||
|
||||
import nl.komponents.kovenant.Kovenant
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.*
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.task
|
||||
|
||||
import org.session.libsession.snode.utilities.getRandomElement
|
||||
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.util.Base64
|
||||
import org.session.libsignal.service.loki.api.MessageWrapper
|
||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||
@ -24,8 +20,8 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope
|
||||
import java.security.SecureRandom
|
||||
|
||||
object SnodeAPI {
|
||||
val database = Configuration.shared.storage
|
||||
val broadcaster = Configuration.shared.broadcaster
|
||||
val database = SnodeConfiguration.shared.storage
|
||||
val broadcaster = SnodeConfiguration.shared.broadcaster
|
||||
val sharedContext = Kovenant.createContext("LokiAPISharedContext")
|
||||
val messageSendingContext = Kovenant.createContext("LokiAPIMessageSendingContext")
|
||||
val messagePollingContext = Kovenant.createContext("LokiAPIMessagePollingContext")
|
||||
@ -245,14 +241,13 @@ object SnodeAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<Envelope> {
|
||||
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<*> {
|
||||
val messages = rawResponse["messages"] as? List<*>
|
||||
return if (messages != null) {
|
||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
||||
val newRawMessages = removeDuplicates(publicKey, messages)
|
||||
parseEnvelopes(newRawMessages)
|
||||
removeDuplicates(publicKey, messages)
|
||||
} else {
|
||||
listOf()
|
||||
listOf<Map<*,*>>()
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,25 +279,6 @@ object SnodeAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnvelopes(rawMessages: List<*>): List<Envelope> {
|
||||
return rawMessages.mapNotNull { rawMessage ->
|
||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||
if (data != null) {
|
||||
try {
|
||||
MessageWrapper.unwrap(data)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error Handling
|
||||
internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? {
|
||||
fun handleBadSnode() {
|
||||
@ -366,5 +342,5 @@ object SnodeAPI {
|
||||
|
||||
// Type Aliases
|
||||
typealias RawResponse = Map<*, *>
|
||||
typealias MessageListPromise = Promise<List<Envelope>, Exception>
|
||||
typealias MessageListPromise = Promise<List<*>, Exception>
|
||||
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
||||
|
@ -2,13 +2,13 @@ package org.session.libsession.snode
|
||||
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster
|
||||
|
||||
class Configuration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) {
|
||||
class SnodeConfiguration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) {
|
||||
companion object {
|
||||
lateinit var shared: Configuration
|
||||
lateinit var shared: SnodeConfiguration
|
||||
|
||||
fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
shared = Configuration(storage, broadcaster)
|
||||
shared = SnodeConfiguration(storage, broadcaster)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object DelimiterUtil {
|
||||
fun escape(value: String, delimiter: Char): String {
|
||||
return value.replace("" + delimiter, "\\" + delimiter)
|
||||
}
|
||||
|
||||
fun unescape(value: String, delimiter: Char): String {
|
||||
return value.replace("\\" + delimiter, "" + delimiter)
|
||||
}
|
||||
|
||||
fun split(value: String, delimiter: Char): Array<String> {
|
||||
val regex = "(?<!\\\\)" + Pattern.quote(delimiter.toString() + "")
|
||||
return value.split(regex).toTypedArray()
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
object LKGroupUtilities {
|
||||
object GroupUtil {
|
||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||
const val MMS_GROUP_PREFIX = "__signal_mms_group__!"
|
||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
||||
@ -44,4 +44,20 @@ object LKGroupUtilities {
|
||||
fun getDecodedGroupIDAsData(groupID: ByteArray): ByteArray {
|
||||
return getDecodedGroupID(groupID).toByteArray()
|
||||
}
|
||||
|
||||
fun isEncodedGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX) || groupId.startsWith(MMS_GROUP_PREFIX) || groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
fun isMmsGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(MMS_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
fun isOpenGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(OPEN_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
fun isClosedGroup(groupId: String): Boolean {
|
||||
return groupId.startsWith(CLOSED_GROUP_PREFIX)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import android.util.Patterns
|
||||
|
||||
|
||||
object NumberUtil {
|
||||
private val emailPattern = Patterns.EMAIL_ADDRESS
|
||||
|
||||
@JvmStatic
|
||||
fun isValidEmail(number: String): Boolean {
|
||||
val matcher = emailPattern.matcher(number)
|
||||
return matcher.matches()
|
||||
}
|
||||
|
||||
fun isValidSmsOrEmail(number: String): Boolean {
|
||||
return PhoneNumberUtils.isWellFormedSmsAddress(number) || isValidEmail(number)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
@file:JvmName("PromiseUtilities")
|
||||
package org.session.libsession.utilities
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.libsignal.logging.Log
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
fun <V, E> Promise<V, E>.successBackground(callback: (value: V) -> Unit): Promise<V, E> {
|
||||
Thread {
|
||||
try {
|
||||
callback(get())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to execute task in background: ${e.message}.")
|
||||
}
|
||||
}.start()
|
||||
return this
|
||||
}
|
||||
|
||||
fun <V> Promise<V, Exception>.timeout(millis: Long): Promise<V, Exception> {
|
||||
if (this.isDone()) { return this; }
|
||||
val deferred = deferred<V, Exception>()
|
||||
Thread {
|
||||
Thread.sleep(millis)
|
||||
if (!deferred.promise.isDone()) {
|
||||
deferred.reject(TimeoutException("Promise timed out."))
|
||||
}
|
||||
}.start()
|
||||
this.success {
|
||||
if (!deferred.promise.isDone()) { deferred.resolve(it) }
|
||||
}.fail {
|
||||
if (!deferred.promise.isDone()) { deferred.reject(it) }
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,13 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
object Util {
|
||||
fun join(list: Collection<String?>, delimiter: String?): String {
|
||||
val result = StringBuilder()
|
||||
var i = 0
|
||||
for (item in list) {
|
||||
result.append(item)
|
||||
if (++i < list.size) result.append(delimiter)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user