Merge branch 'refactor' of https://github.com/RyanRory/loki-messenger-android into refactor-enc-cleanup

This commit is contained in:
Anton Chekulaev 2020-12-10 16:21:37 +11:00
commit 73c4e44711
35 changed files with 2344 additions and 204 deletions

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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.")
}
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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() + "")
}
}
}

View File

@ -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!!, ',')
}
}
}

View File

@ -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}.")
}

View File

@ -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()

View File

@ -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>

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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()
}
}