mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 17:27:45 +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
|
val isUploaded: Boolean = false
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.AttachmentPointer? {
|
fun toProto(): SignalServiceProtos.AttachmentPointer? {
|
||||||
val builder = org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer.newBuilder()
|
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||||
builder.contentType = this.contentType
|
builder.contentType = this.contentType
|
||||||
|
|
||||||
if (!this.fileName.isNullOrEmpty()) {
|
if (!this.fileName.isNullOrEmpty()) {
|
||||||
@ -46,12 +46,12 @@ class DatabaseAttachmentDTO {
|
|||||||
builder.size = this.size
|
builder.size = this.size
|
||||||
builder.key = this.key
|
builder.key = this.key
|
||||||
builder.digest = this.digest
|
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...
|
//TODO I did copy the behavior of iOS below, not sure if that's relevant here...
|
||||||
if (this.shouldHaveImageSize) {
|
if (this.shouldHaveImageSize) {
|
||||||
if (this.width < kotlin.Int.MAX_VALUE && this.height < kotlin.Int.MAX_VALUE) {
|
if (this.width < Int.MAX_VALUE && this.height < Int.MAX_VALUE) {
|
||||||
val imageSize: Size = Size(this.width, this.height)
|
val imageSize= Size(this.width, this.height)
|
||||||
val imageWidth = round(imageSize.width.toDouble())
|
val imageWidth = round(imageSize.width.toDouble())
|
||||||
val imageHeight = round(imageSize.height.toDouble())
|
val imageHeight = round(imageSize.height.toDouble())
|
||||||
if (imageWidth > 0 && imageHeight > 0) {
|
if (imageWidth > 0 && imageHeight > 0) {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package org.session.libsession.messaging
|
package org.session.libsession.messaging
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.session.libsignal.libsignal.loki.SessionResetProtocol
|
import org.session.libsignal.libsignal.loki.SessionResetProtocol
|
||||||
import org.session.libsignal.libsignal.state.*
|
import org.session.libsignal.libsignal.state.*
|
||||||
import org.session.libsignal.metadata.certificate.CertificateValidator
|
import org.session.libsignal.metadata.certificate.CertificateValidator
|
||||||
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||||
|
|
||||||
class Configuration(
|
class MessagingConfiguration(
|
||||||
|
val context: Context,
|
||||||
val storage: StorageProtocol,
|
val storage: StorageProtocol,
|
||||||
val signalStorage: SignalProtocolStore,
|
val signalStorage: SignalProtocolStore,
|
||||||
val sskDatabase: SharedSenderKeysDatabaseProtocol,
|
val sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||||
@ -15,9 +17,10 @@ class Configuration(
|
|||||||
val certificateValidator: CertificateValidator)
|
val certificateValidator: CertificateValidator)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var shared: Configuration
|
lateinit var shared: MessagingConfiguration
|
||||||
|
|
||||||
fun configure(storage: StorageProtocol,
|
fun configure(context: Context,
|
||||||
|
storage: StorageProtocol,
|
||||||
signalStorage: SignalProtocolStore,
|
signalStorage: SignalProtocolStore,
|
||||||
sskDatabase: SharedSenderKeysDatabaseProtocol,
|
sskDatabase: SharedSenderKeysDatabaseProtocol,
|
||||||
messageDataProvider: MessageDataProvider,
|
messageDataProvider: MessageDataProvider,
|
||||||
@ -25,7 +28,7 @@ class Configuration(
|
|||||||
certificateValidator: CertificateValidator
|
certificateValidator: CertificateValidator
|
||||||
) {
|
) {
|
||||||
if (Companion::shared.isInitialized) { return }
|
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.Job
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
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.ECKeyPair
|
||||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey
|
import org.session.libsignal.libsignal.ecc.ECPrivateKey
|
||||||
|
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||||
|
|
||||||
interface StorageProtocol {
|
interface StorageProtocol {
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Signal Protocol
|
// Signal Protocol
|
||||||
|
|
||||||
fun getOrGenerateRegistrationID(): Int //TODO needs impl
|
fun getOrGenerateRegistrationID(): Int
|
||||||
|
|
||||||
// Shared Sender Keys
|
// Shared Sender Keys
|
||||||
fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey?
|
fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey?
|
||||||
@ -71,8 +74,20 @@ interface StorageProtocol {
|
|||||||
fun getReceivedMessageTimestamps(): Set<Long>
|
fun getReceivedMessageTimestamps(): Set<Long>
|
||||||
fun addReceivedMessageTimestamp(timestamp: 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 getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
||||||
|
@ -4,7 +4,7 @@ import kotlin.math.min
|
|||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
|
|
||||||
import org.session.libsession.messaging.Configuration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
import org.session.libsignal.libsignal.logging.Log
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@ -25,7 +25,7 @@ class JobQueue : JobDelegate {
|
|||||||
|
|
||||||
fun addWithoutExecuting(job: Job) {
|
fun addWithoutExecuting(job: Job) {
|
||||||
job.id = System.currentTimeMillis().toString()
|
job.id = System.currentTimeMillis().toString()
|
||||||
Configuration.shared.storage.persist(job)
|
MessagingConfiguration.shared.storage.persist(job)
|
||||||
job.delegate = this
|
job.delegate = this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ class JobQueue : JobDelegate {
|
|||||||
hasResumedPendingJobs = true
|
hasResumedPendingJobs = true
|
||||||
val allJobTypes = listOf(AttachmentDownloadJob.collection, AttachmentDownloadJob.collection, MessageReceiveJob.collection, MessageSendJob.collection, NotifyPNServerJob.collection)
|
val allJobTypes = listOf(AttachmentDownloadJob.collection, AttachmentDownloadJob.collection, MessageReceiveJob.collection, MessageSendJob.collection, NotifyPNServerJob.collection)
|
||||||
allJobTypes.forEach { type ->
|
allJobTypes.forEach { type ->
|
||||||
val allPendingJobs = Configuration.shared.storage.getAllPendingJobs(type)
|
val allPendingJobs = MessagingConfiguration.shared.storage.getAllPendingJobs(type)
|
||||||
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
||||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||||
job.delegate = this
|
job.delegate = this
|
||||||
@ -47,12 +47,12 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobSucceeded(job: Job) {
|
override fun handleJobSucceeded(job: Job) {
|
||||||
Configuration.shared.storage.markJobAsSucceeded(job)
|
MessagingConfiguration.shared.storage.markJobAsSucceeded(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobFailed(job: Job, error: Exception) {
|
override fun handleJobFailed(job: Job, error: Exception) {
|
||||||
job.failureCount += 1
|
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.")}
|
if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")}
|
||||||
storage.persist(job)
|
storage.persist(job)
|
||||||
if (job.failureCount == job.maxFailureCount) {
|
if (job.failureCount == job.maxFailureCount) {
|
||||||
@ -69,7 +69,7 @@ class JobQueue : JobDelegate {
|
|||||||
|
|
||||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
||||||
job.failureCount += 1
|
job.failureCount += 1
|
||||||
val storage = Configuration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
storage.persist(job)
|
storage.persist(job)
|
||||||
storage.markJobAsFailed(job)
|
storage.markJobAsFailed(job)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.session.libsession.messaging.jobs
|
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 delegate: JobDelegate? = null
|
||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package org.session.libsession.messaging.messages
|
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 {
|
sealed class Destination {
|
||||||
|
|
||||||
class Contact(val publicKey: String) : Destination()
|
class Contact(val publicKey: String) : Destination()
|
||||||
@ -7,9 +11,19 @@ sealed class Destination {
|
|||||||
class OpenGroup(val channel: Long, val server: String) : Destination()
|
class OpenGroup(val channel: Long, val server: String) : Destination()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//TODO need to implement the equivalent to TSThread and then implement from(...)
|
fun from(address: Address): Destination {
|
||||||
fun from(threadID: String): Destination {
|
if (address.isContact) {
|
||||||
return Contact(threadID) // Fake for dev
|
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
|
package org.session.libsession.messaging.messages.control.unused
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
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.libsession.messaging.messages.control.ControlMessage
|
||||||
import org.session.libsignal.libsignal.IdentityKey
|
import org.session.libsignal.libsignal.IdentityKey
|
||||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||||
@ -21,7 +21,7 @@ class SessionRequest() : ControlMessage() {
|
|||||||
if (proto.nullMessage == null) return null
|
if (proto.nullMessage == null) return null
|
||||||
val preKeyBundleProto = proto.preKeyBundleMessage ?: return null
|
val preKeyBundleProto = proto.preKeyBundleMessage ?: return null
|
||||||
var registrationID: Int = 0
|
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:
|
//TODO just confirm if the above code does the equivalent to swift below:
|
||||||
/*iOS code: Configuration.shared.storage.with { transaction in
|
/*iOS code: Configuration.shared.storage.with { transaction in
|
||||||
registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction)
|
registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package org.session.libsession.messaging.messages.visible
|
package org.session.libsession.messaging.messages.visible
|
||||||
|
|
||||||
import android.content.Context
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.database.MessageDataProvider
|
|
||||||
import org.session.libsession.messaging.Configuration
|
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
import org.session.libsignal.libsignal.logging.Log
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
|
||||||
@ -46,7 +44,7 @@ class LinkPreview() {
|
|||||||
title?.let { linkPreviewProto.title = title }
|
title?.let { linkPreviewProto.title = title }
|
||||||
val attachmentID = attachmentID
|
val attachmentID = attachmentID
|
||||||
attachmentID?.let {
|
attachmentID?.let {
|
||||||
val attachmentProto = Configuration.shared.messageDataProvider.getAttachment(attachmentID)
|
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachment(attachmentID)
|
||||||
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
|
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
|
||||||
}
|
}
|
||||||
// Build
|
// Build
|
||||||
|
@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.visible
|
|||||||
|
|
||||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||||
import org.session.libsession.database.MessageDataProvider
|
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.libsignal.logging.Log
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class Quote() {
|
|||||||
quoteProto.id = timestamp
|
quoteProto.id = timestamp
|
||||||
quoteProto.author = publicKey
|
quoteProto.author = publicKey
|
||||||
text?.let { quoteProto.text = text }
|
text?.let { quoteProto.text = text }
|
||||||
addAttachmentsIfNeeded(quoteProto, Configuration.shared.messageDataProvider)
|
addAttachmentsIfNeeded(quoteProto, MessagingConfiguration.shared.messageDataProvider)
|
||||||
// Build
|
// Build
|
||||||
try {
|
try {
|
||||||
return quoteProto.build()
|
return quoteProto.build()
|
||||||
|
@ -2,8 +2,7 @@ package org.session.libsession.messaging.messages.visible
|
|||||||
|
|
||||||
import com.goterl.lazycode.lazysodium.BuildConfig
|
import com.goterl.lazycode.lazysodium.BuildConfig
|
||||||
|
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.messaging.Configuration
|
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
import org.session.libsignal.libsignal.logging.Log
|
||||||
@ -52,7 +51,7 @@ class VisibleMessage : Message() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val proto = SignalServiceProtos.Content.newBuilder()
|
val proto = SignalServiceProtos.Content.newBuilder()
|
||||||
var attachmentIDs = this.attachmentIDs
|
var attachmentIDs = this.attachmentIDs
|
||||||
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
val dataMessage: SignalServiceProtos.DataMessage.Builder
|
||||||
@ -91,7 +90,7 @@ class VisibleMessage : Message() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Attachments
|
//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 (!attachments.all { it.isUploaded }) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
//TODO equivalent to iOS's preconditionFailure
|
//TODO equivalent to iOS's preconditionFailure
|
||||||
|
@ -5,11 +5,10 @@ import nl.komponents.kovenant.Promise
|
|||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import nl.komponents.kovenant.then
|
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.utilities.DotNetAPI
|
||||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
import org.session.libsignal.libsignal.logging.Log
|
||||||
import org.session.libsignal.service.internal.util.Base64
|
import org.session.libsignal.service.internal.util.Base64
|
||||||
@ -56,7 +55,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
// region Public API
|
// region Public API
|
||||||
public fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> {
|
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.")
|
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 parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
||||||
val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||||
if (lastMessageServerID != null) {
|
if (lastMessageServerID != null) {
|
||||||
@ -161,7 +160,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
|
|
||||||
public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
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.")
|
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 parameters = mutableMapOf<String, Any>()
|
||||||
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||||
if (lastDeletionServerID != null) {
|
if (lastDeletionServerID != null) {
|
||||||
@ -193,7 +192,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
|
|
||||||
public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||||
val deferred = deferred<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 userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
|
||||||
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
||||||
Thread {
|
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 memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
||||||
val profilePictureURL = info["avatar"] as String
|
val profilePictureURL = info["avatar"] as String
|
||||||
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
||||||
Configuration.shared.storage.setUserCount(channel, server, memberCount)
|
MessagingConfiguration.shared.storage.setUserCount(channel, server, memberCount)
|
||||||
publicChatInfo
|
publicChatInfo
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
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) {
|
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.setUserCount(channel, server, info.memberCount)
|
||||||
storage.updateTitle(groupID, info.displayName)
|
storage.updateTitle(groupID, info.displayName)
|
||||||
// Download and update profile picture if needed
|
// Download and update profile picture if needed
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.session.libsession.messaging.opengroups
|
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.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
import org.session.libsignal.libsignal.logging.Log
|
||||||
import org.session.libsignal.service.internal.util.Hex
|
import org.session.libsignal.service.internal.util.Hex
|
||||||
@ -24,7 +24,7 @@ public data class OpenGroupMessage(
|
|||||||
// region Settings
|
// region Settings
|
||||||
companion object {
|
companion object {
|
||||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||||
val storage = Configuration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||||
// Validation
|
// Validation
|
||||||
if (!message.isValid()) { return null } // Should be valid at this point
|
if (!message.isValid()) { return null } // Should be valid at this point
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
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.Message
|
||||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
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> {
|
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 userPublicKey = storage.getUserPublicKey()
|
||||||
val isOpenGroupMessage = openGroupServerID != null
|
val isOpenGroupMessage = openGroupServerID != null
|
||||||
// Parse the envelope
|
// Parse the envelope
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
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.messaging.sending_receiving.MessageReceiver.Error
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
|
|
||||||
@ -21,26 +21,26 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
object MessageReceiverDecryption {
|
object MessageReceiverDecryption {
|
||||||
|
|
||||||
internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
|
internal fun decryptWithSignalProtocol(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
|
||||||
val storage = Configuration.shared.signalStorage
|
val storage = MessagingConfiguration.shared.signalStorage
|
||||||
val sskDatabase = Configuration.shared.sskDatabase
|
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||||
val sessionResetImp = Configuration.shared.sessionResetImp
|
val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
|
||||||
val certificateValidator = Configuration.shared.certificateValidator
|
val certificateValidator = MessagingConfiguration.shared.certificateValidator
|
||||||
val data = envelope.content
|
val data = envelope.content
|
||||||
if (data.count() == 0) { throw Error.NoData }
|
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 localAddress = SignalServiceAddress(userPublicKey)
|
||||||
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
||||||
val result = cipher.decrypt(SignalServiceEnvelope(envelope))
|
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> {
|
internal fun decryptWithSharedSenderKeys(envelope: SignalServiceProtos.Envelope): Pair<ByteArray, String> {
|
||||||
// 1. ) Check preconditions
|
// 1. ) Check preconditions
|
||||||
val groupPublicKey = envelope.source
|
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
|
val data = envelope.content
|
||||||
if (data.count() == 0) { throw Error.NoData }
|
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
|
// 2. ) Parse the wrapper
|
||||||
val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
|
val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data)
|
||||||
val ivAndCiphertext = wrapper.ciphertext.toByteArray()
|
val ivAndCiphertext = wrapper.ciphertext.toByteArray()
|
||||||
@ -54,7 +54,7 @@ object MessageReceiverDecryption {
|
|||||||
// 4. ) Parse the closed group ciphertext message
|
// 4. ) Parse the closed group ciphertext message
|
||||||
val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed
|
val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed
|
||||||
val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
|
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
|
// 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)
|
val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex)
|
||||||
// 6. ) Return
|
// 6. ) Return
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
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.Destination
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
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.control.TypingIndicator
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
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.libsignal.util.Hex
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
|
||||||
|
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
|
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) {
|
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||||
@ -89,8 +89,8 @@ private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
||||||
val storage = Configuration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val sskDatabase = Configuration.shared.sskDatabase
|
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||||
val kind = message.kind!! as ClosedGroupUpdate.Kind.New
|
val kind = message.kind!! as ClosedGroupUpdate.Kind.New
|
||||||
val groupPublicKey = kind.groupPublicKey.toHexString()
|
val groupPublicKey = kind.groupPublicKey.toHexString()
|
||||||
val name = kind.name
|
val name = kind.name
|
||||||
@ -122,27 +122,24 @@ private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) {
|
|||||||
MessageSender.requestSenderKey(groupPublicKey, publicKey)
|
MessageSender.requestSenderKey(groupPublicKey, publicKey)
|
||||||
}
|
}
|
||||||
// Create the group
|
// Create the group
|
||||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
if (storage.getGroup(groupID) != null) {
|
||||||
if (groupDB.getGroup(groupID).orNull() != null) {
|
|
||||||
// Update the group
|
// Update the group
|
||||||
groupDB.updateTitle(groupID, name)
|
storage.updateTitle(groupID, name)
|
||||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||||
} else {
|
} else {
|
||||||
groupDB.create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||||
null, null, LinkedList<Address>(admins.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
|
// Add the group to the user's set of public keys to poll for
|
||||||
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
|
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
|
/* TODO
|
||||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
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) {
|
private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
import com.google.protobuf.MessageOrBuilder
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
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.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
@ -64,7 +63,7 @@ object MessageSender {
|
|||||||
fun sendToSnodeDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
fun sendToSnodeDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val promise = deferred.promise
|
val promise = deferred.promise
|
||||||
val storage = Configuration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val preconditionFailure = Exception("Destination should not be open groups!")
|
val preconditionFailure = Exception("Destination should not be open groups!")
|
||||||
var snodeMessage: SnodeMessage? = null
|
var snodeMessage: SnodeMessage? = null
|
||||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
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> {
|
fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val promise = deferred.promise
|
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!")
|
val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
|
||||||
message.sentTimestamp = System.currentTimeMillis()
|
message.sentTimestamp = System.currentTimeMillis()
|
||||||
message.sender = storage.getUserPublicKey()
|
message.sender = storage.getUserPublicKey()
|
||||||
|
@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
|
|
||||||
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.control.ClosedGroupUpdate
|
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
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.ecc.Curve
|
||||||
import org.session.libsignal.libsignal.util.Hex
|
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> {
|
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
|
||||||
val deferred = deferred<String, Exception>()
|
val deferred = deferred<String, Exception>()
|
||||||
// Prepare
|
// Prepare
|
||||||
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val members = members
|
val members = members
|
||||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
// Generate a key pair for the group
|
// Generate a key pair for the group
|
||||||
val groupKeyPair = Curve.generateKeyPair()
|
val groupKeyPair = Curve.generateKeyPair()
|
||||||
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
|
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
|
||||||
@ -41,12 +43,9 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
|||||||
// Create the group
|
// Create the group
|
||||||
val admins = setOf( userPublicKey )
|
val admins = setOf( userPublicKey )
|
||||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||||
/* TODO:
|
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
|
||||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
storage.setProfileSharing(Address.fromSerialized(groupID), true)
|
||||||
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
|
||||||
*/
|
|
||||||
// Send a closed group update message to all members using established channels
|
// Send a closed group update message to all members using established channels
|
||||||
val promises = mutableListOf<Promise<Unit, Exception>>()
|
val promises = mutableListOf<Promise<Unit, Exception>>()
|
||||||
for (member in members) {
|
for (member in members) {
|
||||||
@ -55,16 +54,17 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
|
|||||||
senderKeys, membersAsData, adminsAsData)
|
senderKeys, membersAsData, adminsAsData)
|
||||||
val closedGroupUpdate = ClosedGroupUpdate()
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
val promise = MessageSender.sendNonDurably(closedGroupUpdate, threadID)
|
val address = Address.fromSerialized(member)
|
||||||
|
val promise = MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||||
promises.add(promise)
|
promises.add(promise)
|
||||||
}
|
}
|
||||||
// Add the group to the user's set of public keys to poll for
|
// 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
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
// Notify the user
|
// Notify the user
|
||||||
|
val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
/* TODO
|
/* TODO
|
||||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
|
||||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||||
*/
|
*/
|
||||||
// Fulfill the promise
|
// 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> {
|
fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val sskDatabase = Configuration.shared.sskDatabase
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||||
val groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
val groupID = GroupUtil.getEncodedClosedGroupID(groupPublicKey)
|
||||||
val group = groupDB.getGroup(groupID).orNull()
|
val group = storage.getGroup(groupID)
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
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 oldMembers = group.members.map { it.serialize() }.toSet()
|
||||||
val newMembers = members.minus(oldMembers)
|
val newMembers = members.minus(oldMembers)
|
||||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||||
val admins = group.admins.map { it.serialize() }
|
val admins = group.admins.map { it.serialize() }
|
||||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||||
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
|
val groupPrivateKey = sskDatabase.getClosedGroupPrivateKey(groupPublicKey)
|
||||||
if (groupPrivateKey == null) {
|
if (groupPrivateKey == null) {
|
||||||
Log.d("Loki", "Couldn't get private key for closed group.")
|
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 wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
|
||||||
val removedMembers = oldMembers.minus(members)
|
val removedMembers = oldMembers.minus(members)
|
||||||
val isUserLeaving = removedMembers.contains(userPublicKey)
|
val isUserLeaving = removedMembers.contains(userPublicKey)
|
||||||
var newSenderKeys = listOf<ClosedGroupSenderKey>()
|
val newSenderKeys: List<ClosedGroupSenderKey>
|
||||||
if (wasAnyUserRemoved) {
|
if (wasAnyUserRemoved) {
|
||||||
if (isUserLeaving && removedMembers.count() != 1) {
|
if (isUserLeaving && removedMembers.count() != 1) {
|
||||||
Log.d("Loki", "Can't remove self and others simultaneously.")
|
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)
|
// 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) {
|
val promises = oldMembers.map { member ->
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
|
||||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
|
|
||||||
name, setOf(), membersAsData, adminsAsData)
|
name, setOf(), membersAsData, adminsAsData)
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
job.setContext(context)
|
val address = Address.fromSerialized(member)
|
||||||
job.onRun() // Run the job immediately
|
MessageSender.sendNonDurably(closedGroupUpdate, address).get()
|
||||||
}
|
}
|
||||||
|
|
||||||
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||||
for (pair in allOldRatchets) {
|
for (pair in allOldRatchets) {
|
||||||
val senderPublicKey = pair.first
|
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.
|
// send it out to all members (minus the removed ones) using established channels.
|
||||||
if (isUserLeaving) {
|
if (isUserLeaving) {
|
||||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||||
groupDB.setActive(groupID, false)
|
storage.setActive(groupID, false)
|
||||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||||
} else {
|
} else {
|
||||||
// Send closed group update messages to any new members using established channels
|
// Send closed group update messages to any new members using established channels
|
||||||
for (member in newMembers) {
|
for (member in newMembers) {
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
|
||||||
Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
|
Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
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
|
// 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 userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||||
for (member in members) {
|
for (member in members) {
|
||||||
if (member == userPublicKey) { continue }
|
if (member == userPublicKey) { continue }
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
@Suppress("NAME_SHADOWING")
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
val address = Address.fromSerialized(member)
|
||||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
MessageSender.sendNonDurably(closedGroupUpdate, address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (newMembers.isNotEmpty()) {
|
} 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))
|
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)
|
// 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)
|
newSenderKeys, membersAsData, adminsAsData)
|
||||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
// Establish sessions if needed
|
val address = Address.fromSerialized(groupID)
|
||||||
establishSessionsWithMembersIfNeeded(context, newMembers)
|
MessageSender.send(closedGroupUpdate, address)
|
||||||
// Send closed group update messages to the new members using established channels
|
// Send closed group update messages to the new members using established channels
|
||||||
var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
||||||
allSenderKeys = allSenderKeys.union(newSenderKeys)
|
allSenderKeys = allSenderKeys.union(newSenderKeys)
|
||||||
for (member in newMembers) {
|
for (member in newMembers) {
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
|
||||||
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
|
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
|
||||||
@Suppress("NAME_SHADOWING")
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
val address = Address.fromSerialized(member)
|
||||||
|
MessageSender.send(closedGroupUpdate, address)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
|
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)
|
allSenderKeys, membersAsData, adminsAsData)
|
||||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
|
val address = Address.fromSerialized(groupID)
|
||||||
|
MessageSender.send(closedGroupUpdate, address)
|
||||||
}
|
}
|
||||||
// Update the group
|
// Update the group
|
||||||
groupDB.updateTitle(groupID, name)
|
storage.updateTitle(groupID, name)
|
||||||
if (!isUserLeaving) {
|
if (!isUserLeaving) {
|
||||||
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
|
// 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
|
// Notify the user
|
||||||
val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
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)
|
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||||
|
*/
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
return deferred.promise
|
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) {
|
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) {
|
||||||
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
|
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 closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
|
||||||
val closedGroupUpdate = ClosedGroupUpdate()
|
val closedGroupUpdate = ClosedGroupUpdate()
|
||||||
closedGroupUpdate.kind = closedGroupUpdateKind
|
closedGroupUpdate.kind = closedGroupUpdateKind
|
||||||
MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey))
|
MessageSender.send(closedGroupUpdate, address)
|
||||||
}
|
}
|
@ -1,35 +1,39 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||||
import org.session.libsession.messaging.messages.Destination
|
import org.session.libsession.messaging.messages.Destination
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
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)
|
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
|
message.threadID = threadID
|
||||||
val destination = Destination.from(threadID)
|
val destination = Destination.from(address)
|
||||||
val job = MessageSendJob(message, destination)
|
val job = MessageSendJob(message, destination)
|
||||||
JobQueue.shared.add(job)
|
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)
|
prep(attachments, message)
|
||||||
// TODO: Deal with attachments
|
// 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
|
message.threadID = threadID
|
||||||
val destination = Destination.from(threadID)
|
val destination = Destination.from(address)
|
||||||
return MessageSender.send(message, destination)
|
return MessageSender.send(message, destination)
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
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.messages.Message
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||||
import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil
|
import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil
|
||||||
@ -21,11 +21,11 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
|||||||
object MessageSenderEncryption {
|
object MessageSenderEncryption {
|
||||||
|
|
||||||
internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{
|
internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{
|
||||||
val storage = Configuration.shared.signalStorage
|
val storage = MessagingConfiguration.shared.signalStorage
|
||||||
val sskDatabase = Configuration.shared.sskDatabase
|
val sskDatabase = MessagingConfiguration.shared.sskDatabase
|
||||||
val sessionResetImp = Configuration.shared.sessionResetImp
|
val sessionResetImp = MessagingConfiguration.shared.sessionResetImp
|
||||||
val localAddress = SignalServiceAddress(recipientPublicKey)
|
val localAddress = SignalServiceAddress(recipientPublicKey)
|
||||||
val certificateValidator = Configuration.shared.certificateValidator
|
val certificateValidator = MessagingConfiguration.shared.certificateValidator
|
||||||
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator)
|
||||||
val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1)
|
val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1)
|
||||||
val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey)
|
val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey)
|
||||||
@ -36,7 +36,7 @@ object MessageSenderEncryption {
|
|||||||
|
|
||||||
internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray {
|
internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray {
|
||||||
// 1. ) Encrypt the data with the user's sender key
|
// 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 ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey)
|
||||||
val ivAndCiphertext = ciphertextAndKeyIndex.first
|
val ivAndCiphertext = ciphertextAndKeyIndex.first
|
||||||
val keyIndex = ciphertextAndKeyIndex.second
|
val keyIndex = ciphertextAndKeyIndex.second
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving.notifications
|
package org.session.libsession.messaging.sending_receiving.notifications
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import okhttp3.*
|
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.libsignal.logging.Log
|
||||||
import org.session.libsignal.service.internal.util.JsonUtil
|
import org.session.libsignal.service.internal.util.JsonUtil
|
||||||
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object PushNotificationAPI {
|
object PushNotificationAPI {
|
||||||
|
val context = MessagingConfiguration.shared.context
|
||||||
val server = "https://live.apns.getsession.org"
|
val server = "https://live.apns.getsession.org"
|
||||||
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||||
private val maxRetryCount = 4
|
private val maxRetryCount = 4
|
||||||
@ -46,8 +46,8 @@ object PushNotificationAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unsubscribe from all closed groups
|
// Unsubscribe from all closed groups
|
||||||
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()!!
|
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()!!
|
||||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||||
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
performOperation(ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey)
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ object PushNotificationAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Subscribe to all closed groups
|
// Subscribe to all closed groups
|
||||||
val allClosedGroupPublicKeys = Configuration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = MessagingConfiguration.shared.sskDatabase.getAllClosedGroupPublicKeys()
|
||||||
allClosedGroupPublicKeys.forEach { closedGroup ->
|
allClosedGroupPublicKeys.forEach { closedGroup ->
|
||||||
performOperation(ClosedGroupOperation.Subscribe, closedGroup, publicKey)
|
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.Request
|
||||||
import okhttp3.RequestBody
|
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.OnionRequestAPI
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
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 data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
||||||
|
|
||||||
public fun getAuthToken(server: String): Promise<String, Exception> {
|
public fun getAuthToken(server: String): Promise<String, Exception> {
|
||||||
val storage = Configuration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val token = storage.getAuthToken(server)
|
val token = storage.getAuthToken(server)
|
||||||
if (token != null) { return Promise.of(token) }
|
if (token != null) { return Promise.of(token) }
|
||||||
// Avoid multiple token requests to the server by caching
|
// Avoid multiple token requests to the server by caching
|
||||||
@ -76,7 +76,7 @@ open class DotNetAPI {
|
|||||||
|
|
||||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
||||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
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 )
|
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 ->
|
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json ->
|
||||||
try {
|
try {
|
||||||
@ -102,7 +102,7 @@ open class DotNetAPI {
|
|||||||
|
|
||||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
||||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
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 )
|
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
||||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { 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) {
|
if (exception is HTTP.HTTPRequestFailedException) {
|
||||||
val statusCode = exception.statusCode
|
val statusCode = exception.statusCode
|
||||||
if (statusCode == 401 || statusCode == 403) {
|
if (statusCode == 401 || statusCode == 403) {
|
||||||
Configuration.shared.storage.setAuthToken(server, null)
|
MessagingConfiguration.shared.storage.setAuthToken(server, null)
|
||||||
throw Error.TokenExpired
|
throw Error.TokenExpired
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,7 +256,7 @@ open class DotNetAPI {
|
|||||||
if (exception is HTTP.HTTPRequestFailedException) {
|
if (exception is HTTP.HTTPRequestFailedException) {
|
||||||
val statusCode = exception.statusCode
|
val statusCode = exception.statusCode
|
||||||
if (statusCode == 401 || statusCode == 403) {
|
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}.")
|
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.LazySodiumAndroid
|
||||||
import com.goterl.lazycode.lazysodium.SodiumAndroid
|
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.libsignal.logging.Log
|
||||||
import org.session.libsignal.metadata.SignalProtos
|
import org.session.libsignal.metadata.SignalProtos
|
||||||
@ -37,12 +37,12 @@ object UnidentifiedAccessUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
|
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)
|
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
|
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
|
||||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()
|
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
return sodium.randomBytesBuf(16)
|
return sodium.randomBytesBuf(16)
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ object UnidentifiedAccessUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getUnidentifiedAccessCertificate(): ByteArray? {
|
private fun getUnidentifiedAccessCertificate(): ByteArray? {
|
||||||
val userPublicKey = Configuration.shared.storage.getUserPublicKey()
|
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
|
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
|
||||||
return certificate.toByteArray()
|
return certificate.toByteArray()
|
||||||
|
@ -2,17 +2,13 @@
|
|||||||
|
|
||||||
package org.session.libsession.snode
|
package org.session.libsession.snode
|
||||||
|
|
||||||
import nl.komponents.kovenant.Kovenant
|
import nl.komponents.kovenant.*
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.deferred
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import nl.komponents.kovenant.task
|
|
||||||
|
|
||||||
import org.session.libsession.snode.utilities.getRandomElement
|
import org.session.libsession.snode.utilities.getRandomElement
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.logging.Log
|
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.internal.util.Base64
|
||||||
import org.session.libsignal.service.loki.api.MessageWrapper
|
import org.session.libsignal.service.loki.api.MessageWrapper
|
||||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
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
|
import java.security.SecureRandom
|
||||||
|
|
||||||
object SnodeAPI {
|
object SnodeAPI {
|
||||||
val database = Configuration.shared.storage
|
val database = SnodeConfiguration.shared.storage
|
||||||
val broadcaster = Configuration.shared.broadcaster
|
val broadcaster = SnodeConfiguration.shared.broadcaster
|
||||||
val sharedContext = Kovenant.createContext("LokiAPISharedContext")
|
val sharedContext = Kovenant.createContext("LokiAPISharedContext")
|
||||||
val messageSendingContext = Kovenant.createContext("LokiAPIMessageSendingContext")
|
val messageSendingContext = Kovenant.createContext("LokiAPIMessageSendingContext")
|
||||||
val messagePollingContext = Kovenant.createContext("LokiAPIMessagePollingContext")
|
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<*>
|
val messages = rawResponse["messages"] as? List<*>
|
||||||
return if (messages != null) {
|
return if (messages != null) {
|
||||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
||||||
val newRawMessages = removeDuplicates(publicKey, messages)
|
removeDuplicates(publicKey, messages)
|
||||||
parseEnvelopes(newRawMessages)
|
|
||||||
} else {
|
} 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
|
// Error Handling
|
||||||
internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? {
|
internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? {
|
||||||
fun handleBadSnode() {
|
fun handleBadSnode() {
|
||||||
@ -366,5 +342,5 @@ object SnodeAPI {
|
|||||||
|
|
||||||
// Type Aliases
|
// Type Aliases
|
||||||
typealias RawResponse = Map<*, *>
|
typealias RawResponse = Map<*, *>
|
||||||
typealias MessageListPromise = Promise<List<Envelope>, Exception>
|
typealias MessageListPromise = Promise<List<*>, Exception>
|
||||||
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
||||||
|
@ -2,13 +2,13 @@ package org.session.libsession.snode
|
|||||||
|
|
||||||
import org.session.libsignal.service.loki.utilities.Broadcaster
|
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 {
|
companion object {
|
||||||
lateinit var shared: Configuration
|
lateinit var shared: SnodeConfiguration
|
||||||
|
|
||||||
fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) {
|
fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) {
|
||||||
if (Companion::shared.isInitialized) { return }
|
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
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
object LKGroupUtilities {
|
object GroupUtil {
|
||||||
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
|
||||||
const val MMS_GROUP_PREFIX = "__signal_mms_group__!"
|
const val MMS_GROUP_PREFIX = "__signal_mms_group__!"
|
||||||
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
const val OPEN_GROUP_PREFIX = "__loki_public_chat_group__!"
|
||||||
@ -44,4 +44,20 @@ object LKGroupUtilities {
|
|||||||
fun getDecodedGroupIDAsData(groupID: ByteArray): ByteArray {
|
fun getDecodedGroupIDAsData(groupID: ByteArray): ByteArray {
|
||||||
return getDecodedGroupID(groupID).toByteArray()
|
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