mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
clean & fix expiration timer setting issue
This commit is contained in:
parent
0e049469aa
commit
60f51af295
@ -268,7 +268,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
"SendReadReceiptJob",
|
||||
"TypingSendJob",
|
||||
"AttachmentUploadJob",
|
||||
"RequestGroupInfoJob");
|
||||
"RequestGroupInfoJob",
|
||||
"ClosedGroupUpdateMessageSendJobV2");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.dependencies;
|
||||
import android.content.Context;
|
||||
|
||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@ -30,30 +29,12 @@ public class SignalCommunicationModule {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private SignalServiceMessageSender messageSender;
|
||||
private SignalServiceMessageReceiver messageReceiver;
|
||||
|
||||
public SignalCommunicationModule(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
|
||||
if (this.messageSender == null) {
|
||||
this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context),
|
||||
TextSecurePreferences.getLocalNumber(context),
|
||||
DatabaseFactory.getLokiAPIDatabase(context),
|
||||
DatabaseFactory.getLokiThreadDatabase(context),
|
||||
DatabaseFactory.getLokiMessageDatabase(context),
|
||||
new SessionProtocolImpl(context),
|
||||
DatabaseFactory.getLokiUserDatabase(context),
|
||||
DatabaseFactory.getGroupDatabase(context),
|
||||
((ApplicationContext)context.getApplicationContext()).broadcaster);
|
||||
}
|
||||
|
||||
return this.messageSender;
|
||||
}
|
||||
|
||||
@Provides
|
||||
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
|
||||
if (this.messageReceiver == null) {
|
||||
|
@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -32,7 +31,6 @@ public final class JobManagerFactories {
|
||||
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
|
||||
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
|
||||
|
@ -67,7 +67,6 @@ import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.SignalServiceMessageSender;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceContent;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
||||
@ -84,8 +83,6 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
private MessageNotifier messageNotifier;
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
public PushDecryptJob(Context context) {
|
||||
this(context, -1);
|
||||
}
|
||||
@ -458,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
||||
String syncTarget = message.getSyncTarget().orNull();
|
||||
|
||||
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
|
||||
handleExpirationUpdate(content, message, Optional.absent());
|
||||
}
|
||||
|
||||
Long threadId = null;
|
||||
|
||||
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
|
||||
|
@ -1,253 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.session.libsession.messaging.jobs.Data
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.session.libsignal.utilities.logging.Log
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters,
|
||||
private val destination: String,
|
||||
private val kind: Kind,
|
||||
private val sentTime: Long) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
|
||||
object Leave : Kind()
|
||||
class RemoveMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class AddMembers(val members: Collection<ByteArray>) : Kind()
|
||||
class NameChange(val name: String) : Kind()
|
||||
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
|
||||
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper {
|
||||
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
|
||||
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
|
||||
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
|
||||
return result.build()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(20)
|
||||
.build(),
|
||||
destination,
|
||||
kind,
|
||||
sentTime)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
builder.putLong("sentTime", sentTime)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
builder.putByteArray("publicKey", kind.publicKey)
|
||||
builder.putString("name", kind.name)
|
||||
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.Update -> {
|
||||
builder.putString("kind", "Update")
|
||||
builder.putString("name", kind.name)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
builder.putString("kind", "RemoveMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
Kind.Leave -> {
|
||||
builder.putString("kind", "Leave")
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
builder.putString("kind", "AddMembers")
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
builder.putString("kind", "NameChange")
|
||||
builder.putString("name", kind.name)
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
builder.putString("kind", "EncryptionKeyPair")
|
||||
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
|
||||
builder.putString("wrappers", wrappers)
|
||||
builder.putString("targetUser", kind.targetUser)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val sentTime = data.getLong("sentTime")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
val publicKey = data.getByteArray("publicKey")
|
||||
val name = data.getString("name")
|
||||
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
|
||||
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
|
||||
}
|
||||
"Update" -> {
|
||||
val name = data.getString("name")
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.Update(name, members)
|
||||
}
|
||||
"EncryptionKeyPair" -> {
|
||||
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
|
||||
val targetUser = data.getString("targetUser")
|
||||
kind = Kind.EncryptionKeyPair(wrappers, targetUser)
|
||||
}
|
||||
"RemoveMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.RemoveMembers(members)
|
||||
}
|
||||
"AddMembers" -> {
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.AddMembers(members)
|
||||
}
|
||||
"NameChange" -> {
|
||||
val name = data.getString("name")
|
||||
kind = Kind.NameChange(name)
|
||||
}
|
||||
"Leave" -> {
|
||||
kind = Kind.Leave
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) {
|
||||
kind.targetUser
|
||||
} else {
|
||||
destination
|
||||
}
|
||||
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage = DataMessage.newBuilder()
|
||||
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
|
||||
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
|
||||
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.Update -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.EncryptionKeyPair -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
if (kind.targetUser != null) {
|
||||
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination))
|
||||
}
|
||||
}
|
||||
Kind.Leave -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
|
||||
closedGroupUpdate.name = kind.name
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupControlMessage = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
val serializedContentMessage = contentMessage.build().toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(sendDestination)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl = when (kind) {
|
||||
is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000
|
||||
else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
}
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess,
|
||||
sentTime, serializedContentMessage, false, ttl,
|
||||
true, false, false, Optional.absent())
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
}
|
@ -3,9 +3,6 @@ package org.thoughtcrime.securesms.loki.protocol
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
@ -14,11 +11,8 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
|
||||
import org.session.libsignal.utilities.ThreadUtils
|
||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
|
||||
@ -28,7 +22,10 @@ import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
|
||||
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
|
||||
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
@ -37,258 +34,8 @@ import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
|
||||
object InvalidUpdate : Error("Invalid group update.")
|
||||
}
|
||||
|
||||
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
// Generate the group's public key
|
||||
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
|
||||
val sentTime = System.currentTimeMillis()
|
||||
// Generate the key pair that'll be used for encryption and decryption
|
||||
val encryptionKeyPair = Curve.generateKeyPair()
|
||||
// Create the group
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val admins = setOf( userPublicKey )
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis())
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
apiDB.addClosedGroupPublicKey(groupPublicKey)
|
||||
// Store the encryption key pair
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
|
||||
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately to make all of this sync
|
||||
}
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
deferred.resolve(groupID)
|
||||
}
|
||||
// Return
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if
|
||||
* you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation]
|
||||
*/
|
||||
@JvmStatic @JvmOverloads
|
||||
fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = group.title
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.QUIT
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
if (notifyUser) {
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
}
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
|
||||
val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) }
|
||||
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() ?: Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)).orNull()
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
// Send closed group update messages to any new members individually
|
||||
for (member in membersToAdd) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
|
||||
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
|
||||
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
throw Error.NoKeyPair
|
||||
}
|
||||
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
|
||||
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
|
||||
throw Error.InvalidUpdate
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val sentTime = System.currentTimeMillis()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
throw Error.NoThread
|
||||
}
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
}
|
||||
|
||||
private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't update nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
|
||||
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
|
||||
return
|
||||
}
|
||||
// Generate the new encryption key pair
|
||||
val newKeyPair = Curve.generateKeyPair()
|
||||
// replace call will not succeed if no value already set
|
||||
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
|
||||
do {
|
||||
// make sure we set the pendingKeyPair or wait until it is not null
|
||||
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
|
||||
// Distribute it
|
||||
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
|
||||
// Store it * after * having sent out the message to the group
|
||||
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
|
||||
pendingKeyPair[groupPublicKey] = Optional.absent()
|
||||
}
|
||||
|
||||
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true) {
|
||||
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
|
||||
val plaintext = proto.build().toByteArray()
|
||||
val wrappers = targetMembers.mapNotNull { publicKey ->
|
||||
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
|
||||
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
|
||||
}
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis())
|
||||
if (force) {
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
} else {
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
|
||||
@ -410,7 +157,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
@ -460,7 +207,7 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
} else {
|
||||
for (user in updateMembers) {
|
||||
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -523,7 +270,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
// Notify user
|
||||
@ -685,7 +432,7 @@ object ClosedGroupsProtocolV2 {
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
|
||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
|
@ -1,825 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
package org.session.libsignal.service.api;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.session.libsignal.libsignal.ecc.ECKeyPair;
|
||||
import org.session.libsignal.libsignal.state.IdentityKeyStore;
|
||||
import org.session.libsignal.utilities.logging.Log;
|
||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream;
|
||||
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
|
||||
import org.session.libsignal.service.api.crypto.UntrustedIdentityException;
|
||||
import org.session.libsignal.service.api.messages.SendMessageResult;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
||||
import org.session.libsignal.service.api.push.SignalServiceAddress;
|
||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
||||
import org.session.libsignal.service.internal.crypto.PaddingInputStream;
|
||||
import org.session.libsignal.service.internal.push.OutgoingPushMessage;
|
||||
import org.session.libsignal.service.internal.push.OutgoingPushMessageList;
|
||||
import org.session.libsignal.service.internal.push.PushAttachmentData;
|
||||
import org.session.libsignal.service.internal.push.PushTransportDetails;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage.LokiProfile;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage;
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage;
|
||||
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
import org.session.libsignal.service.internal.push.http.OutputStreamFactory;
|
||||
import org.session.libsignal.utilities.Base64;
|
||||
import org.session.libsignal.service.internal.util.Util;
|
||||
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI;
|
||||
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
||||
import org.session.libsignal.service.loki.api.SignalMessageInfo;
|
||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||
import org.session.libsignal.service.loki.api.crypto.SessionProtocol;
|
||||
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.database.LokiAPIDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol;
|
||||
import org.session.libsignal.service.loki.utilities.TTLUtilities;
|
||||
import org.session.libsignal.service.loki.utilities.Broadcaster;
|
||||
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
||||
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import nl.komponents.kovenant.Promise;
|
||||
|
||||
/**
|
||||
* The main interface for sending Signal Service messages.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class SignalServiceMessageSender {
|
||||
|
||||
private static final String TAG = SignalServiceMessageSender.class.getSimpleName();
|
||||
|
||||
private final IdentityKeyStore store;
|
||||
// Loki
|
||||
private final String userPublicKey;
|
||||
private final LokiAPIDatabaseProtocol apiDatabase;
|
||||
private final LokiThreadDatabaseProtocol threadDatabase;
|
||||
private final LokiMessageDatabaseProtocol messageDatabase;
|
||||
private final SessionProtocol sessionProtocolImpl;
|
||||
private final LokiUserDatabaseProtocol userDatabase;
|
||||
private final LokiOpenGroupDatabaseProtocol openGroupDatabase;
|
||||
private final Broadcaster broadcaster;
|
||||
|
||||
public SignalServiceMessageSender(IdentityKeyStore store,
|
||||
String userPublicKey,
|
||||
LokiAPIDatabaseProtocol apiDatabase,
|
||||
LokiThreadDatabaseProtocol threadDatabase,
|
||||
LokiMessageDatabaseProtocol messageDatabase,
|
||||
SessionProtocol sessionProtocolImpl,
|
||||
LokiUserDatabaseProtocol userDatabase,
|
||||
LokiOpenGroupDatabaseProtocol openGroupDatabase,
|
||||
Broadcaster broadcaster)
|
||||
{
|
||||
this.store = store;
|
||||
this.userPublicKey = userPublicKey;
|
||||
this.apiDatabase = apiDatabase;
|
||||
this.threadDatabase = threadDatabase;
|
||||
this.messageDatabase = messageDatabase;
|
||||
this.sessionProtocolImpl = sessionProtocolImpl;
|
||||
this.userDatabase = userDatabase;
|
||||
this.openGroupDatabase = openGroupDatabase;
|
||||
this.broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a read receipt for a received message.
|
||||
*
|
||||
* @param recipient The sender of the received message you're acknowledging.
|
||||
* @param message The read receipt to deliver.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void sendReceipt(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceReceiptMessage message)
|
||||
throws IOException {
|
||||
byte[] content = createReceiptContent(message);
|
||||
boolean useFallbackEncryption = true;
|
||||
sendMessage(recipient, unidentifiedAccess, message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
|
||||
}
|
||||
|
||||
public void sendTyping(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
SignalServiceTypingMessage message)
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createTypingContent(message);
|
||||
sendMessage(0, recipients, unidentifiedAccess, message.getTimestamp(), content, true, message.getTTL(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a single recipient.
|
||||
*
|
||||
* @param recipient The message's destination.
|
||||
* @param message The message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public SendMessageResult sendMessage(long messageID,
|
||||
SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceDataMessage message,
|
||||
boolean isSelfSend)
|
||||
throws IOException
|
||||
{
|
||||
byte[] content = createMessageContent(message, recipient);
|
||||
long timestamp = message.getTimestamp();
|
||||
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccess, timestamp, content, false, message.getTTL(), true, isClosedGroup, message.hasVisibleContent() && !isSelfSend, message.getSyncTarget());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a group.
|
||||
*
|
||||
* @param recipients The group members.
|
||||
* @param message The group message.
|
||||
* @throws IOException
|
||||
*/
|
||||
public List<SendMessageResult> sendMessage(long messageID,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
SignalServiceDataMessage message)
|
||||
throws IOException {
|
||||
// Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine
|
||||
// whether an attachment is being sent to an open group or not.
|
||||
byte[] content = createMessageContent(message, recipients.get(0));
|
||||
long timestamp = message.getTimestamp();
|
||||
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
|
||||
|
||||
return sendMessage(messageID, recipients, unidentifiedAccess, timestamp, content, false, message.getTTL(), isClosedGroup, message.hasVisibleContent());
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment, boolean usePadding, @Nullable SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
boolean shouldEncrypt = true;
|
||||
String server = FileServerAPI.shared.getServer();
|
||||
|
||||
// Loki - Check if we are sending to an open group
|
||||
if (recipient != null) {
|
||||
long threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
|
||||
if (publicChat != null) {
|
||||
shouldEncrypt = false;
|
||||
server = publicChat.getServer();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] attachmentKey = Util.getSecretBytes(64);
|
||||
long paddedLength = usePadding ? PaddingInputStream.getPaddedSize(attachment.getLength()) : attachment.getLength();
|
||||
InputStream dataStream = usePadding ? new PaddingInputStream(attachment.getInputStream(), attachment.getLength()) : attachment.getInputStream();
|
||||
long ciphertextLength = shouldEncrypt ? AttachmentCipherOutputStream.getCiphertextLength(paddedLength) : attachment.getLength();
|
||||
|
||||
OutputStreamFactory outputStreamFactory = shouldEncrypt ? new AttachmentCipherOutputStreamFactory(attachmentKey) : new PlaintextOutputStreamFactory();
|
||||
PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), dataStream, ciphertextLength, outputStreamFactory, attachment.getListener());
|
||||
|
||||
// Loki - Upload attachment
|
||||
LokiDotNetAPI.UploadResult result = FileServerAPI.shared.uploadAttachment(server, attachmentData);
|
||||
return new SignalServiceAttachmentPointer(result.getId(),
|
||||
attachment.getContentType(),
|
||||
attachmentKey,
|
||||
Optional.of(Util.toIntExact(attachment.getLength())),
|
||||
attachment.getPreview(),
|
||||
attachment.getWidth(), attachment.getHeight(),
|
||||
Optional.fromNullable(result.getDigest()),
|
||||
attachment.getFileName(),
|
||||
attachment.getVoiceNote(),
|
||||
attachment.getCaption(),
|
||||
result.getUrl());
|
||||
}
|
||||
|
||||
private byte[] createTypingContent(SignalServiceTypingMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
TypingMessage.Builder builder = TypingMessage.newBuilder();
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
if (message.isTypingStarted()) builder.setAction(TypingMessage.Action.STARTED);
|
||||
else if (message.isTypingStopped()) builder.setAction(TypingMessage.Action.STOPPED);
|
||||
else throw new IllegalArgumentException("Unknown typing indicator");
|
||||
|
||||
return container.setTypingMessage(builder).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createReceiptContent(SignalServiceReceiptMessage message) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
|
||||
|
||||
for (long timestamp : message.getTimestamps()) {
|
||||
builder.addTimestamp(timestamp);
|
||||
}
|
||||
|
||||
if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY);
|
||||
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
|
||||
|
||||
return container.setReceiptMessage(builder).build().toByteArray();
|
||||
}
|
||||
|
||||
private byte[] createMessageContent(SignalServiceDataMessage message, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
Content.Builder container = Content.newBuilder();
|
||||
|
||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
|
||||
|
||||
if (!pointers.isEmpty()) {
|
||||
builder.addAllAttachments(pointers);
|
||||
}
|
||||
|
||||
if (message.getBody().isPresent()) {
|
||||
builder.setBody(message.getBody().get());
|
||||
}
|
||||
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
builder.setGroup(createGroupContent(message.getGroupInfo().get(), recipient));
|
||||
}
|
||||
|
||||
if (message.isExpirationUpdate()) {
|
||||
builder.setFlags(DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE);
|
||||
}
|
||||
|
||||
if (message.getExpiresInSeconds() > 0) {
|
||||
builder.setExpireTimer(message.getExpiresInSeconds());
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent()) {
|
||||
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
|
||||
}
|
||||
|
||||
if (message.getSyncTarget().isPresent()) {
|
||||
builder.setSyncTarget(message.getSyncTarget().get());
|
||||
}
|
||||
|
||||
if (message.getQuote().isPresent()) {
|
||||
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
|
||||
.setId(message.getQuote().get().getId())
|
||||
.setAuthor(message.getQuote().get().getAuthor().getNumber())
|
||||
.setText(message.getQuote().get().getText());
|
||||
|
||||
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) {
|
||||
DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder();
|
||||
|
||||
quotedAttachment.setContentType(attachment.getContentType());
|
||||
|
||||
if (attachment.getFileName() != null) {
|
||||
quotedAttachment.setFileName(attachment.getFileName());
|
||||
}
|
||||
|
||||
if (attachment.getThumbnail() != null) {
|
||||
quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream(), recipient));
|
||||
}
|
||||
|
||||
quoteBuilder.addAttachments(quotedAttachment);
|
||||
}
|
||||
|
||||
builder.setQuote(quoteBuilder);
|
||||
}
|
||||
|
||||
if (message.getSharedContacts().isPresent()) {
|
||||
builder.addAllContact(createSharedContactContent(message.getSharedContacts().get(), recipient));
|
||||
}
|
||||
|
||||
if (message.getPreviews().isPresent()) {
|
||||
for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) {
|
||||
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
|
||||
previewBuilder.setTitle(preview.getTitle());
|
||||
previewBuilder.setUrl(preview.getUrl());
|
||||
|
||||
if (preview.getImage().isPresent()) {
|
||||
if (preview.getImage().get().isStream()) {
|
||||
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream(), recipient));
|
||||
} else {
|
||||
previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer()));
|
||||
}
|
||||
}
|
||||
|
||||
builder.addPreview(previewBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
LokiProfile.Builder lokiUserProfileBuilder = LokiProfile.newBuilder();
|
||||
String displayName = userDatabase.getDisplayName(userPublicKey);
|
||||
if (displayName != null) { lokiUserProfileBuilder.setDisplayName(displayName); }
|
||||
String profilePictureURL = userDatabase.getProfilePictureURL(userPublicKey);
|
||||
if (profilePictureURL != null) { lokiUserProfileBuilder.setProfilePicture(profilePictureURL); }
|
||||
builder.setProfile(lokiUserProfileBuilder.build());
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
container.setDataMessage(builder);
|
||||
|
||||
return container.build().toByteArray();
|
||||
}
|
||||
|
||||
private GroupContext createGroupContent(SignalServiceGroup group, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
GroupContext.Builder builder = GroupContext.newBuilder();
|
||||
builder.setId(ByteString.copyFrom(group.getGroupId()));
|
||||
|
||||
if (group.getType() != SignalServiceGroup.Type.DELIVER) {
|
||||
if (group.getType() == SignalServiceGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE);
|
||||
else if (group.getType() == SignalServiceGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT);
|
||||
else if (group.getType() == SignalServiceGroup.Type.REQUEST_INFO) builder.setType(GroupContext.Type.REQUEST_INFO);
|
||||
else throw new AssertionError("Unknown type: " + group.getType());
|
||||
|
||||
if (group.getName().isPresent()) builder.setName(group.getName().get());
|
||||
if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get());
|
||||
if (group.getAdmins().isPresent()) builder.addAllAdmins(group.getAdmins().get());
|
||||
|
||||
if (group.getAvatar().isPresent()) {
|
||||
if (group.getAvatar().get().isStream()) {
|
||||
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asStream(), recipient));
|
||||
} else {
|
||||
builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asPointer()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
builder.setType(GroupContext.Type.DELIVER);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private List<DataMessage.Contact> createSharedContactContent(List<SharedContact> contacts, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
List<DataMessage.Contact> results = new LinkedList<>();
|
||||
|
||||
for (SharedContact contact : contacts) {
|
||||
DataMessage.Contact.Name.Builder nameBuilder = DataMessage.Contact.Name.newBuilder();
|
||||
|
||||
if (contact.getName().getFamily().isPresent()) nameBuilder.setFamilyName(contact.getName().getFamily().get());
|
||||
if (contact.getName().getGiven().isPresent()) nameBuilder.setGivenName(contact.getName().getGiven().get());
|
||||
if (contact.getName().getMiddle().isPresent()) nameBuilder.setMiddleName(contact.getName().getMiddle().get());
|
||||
if (contact.getName().getPrefix().isPresent()) nameBuilder.setPrefix(contact.getName().getPrefix().get());
|
||||
if (contact.getName().getSuffix().isPresent()) nameBuilder.setSuffix(contact.getName().getSuffix().get());
|
||||
if (contact.getName().getDisplay().isPresent()) nameBuilder.setDisplayName(contact.getName().getDisplay().get());
|
||||
|
||||
DataMessage.Contact.Builder contactBuilder = DataMessage.Contact.newBuilder()
|
||||
.setName(nameBuilder);
|
||||
|
||||
if (contact.getAddress().isPresent()) {
|
||||
for (SharedContact.PostalAddress address : contact.getAddress().get()) {
|
||||
DataMessage.Contact.PostalAddress.Builder addressBuilder = DataMessage.Contact.PostalAddress.newBuilder();
|
||||
|
||||
switch (address.getType()) {
|
||||
case HOME: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.HOME); break;
|
||||
case WORK: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.WORK); break;
|
||||
case CUSTOM: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + address.getType());
|
||||
}
|
||||
|
||||
if (address.getCity().isPresent()) addressBuilder.setCity(address.getCity().get());
|
||||
if (address.getCountry().isPresent()) addressBuilder.setCountry(address.getCountry().get());
|
||||
if (address.getLabel().isPresent()) addressBuilder.setLabel(address.getLabel().get());
|
||||
if (address.getNeighborhood().isPresent()) addressBuilder.setNeighborhood(address.getNeighborhood().get());
|
||||
if (address.getPobox().isPresent()) addressBuilder.setPobox(address.getPobox().get());
|
||||
if (address.getPostcode().isPresent()) addressBuilder.setPostcode(address.getPostcode().get());
|
||||
if (address.getRegion().isPresent()) addressBuilder.setRegion(address.getRegion().get());
|
||||
if (address.getStreet().isPresent()) addressBuilder.setStreet(address.getStreet().get());
|
||||
|
||||
contactBuilder.addAddress(addressBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getEmail().isPresent()) {
|
||||
for (SharedContact.Email email : contact.getEmail().get()) {
|
||||
DataMessage.Contact.Email.Builder emailBuilder = DataMessage.Contact.Email.newBuilder()
|
||||
.setValue(email.getValue());
|
||||
|
||||
switch (email.getType()) {
|
||||
case HOME: emailBuilder.setType(DataMessage.Contact.Email.Type.HOME); break;
|
||||
case WORK: emailBuilder.setType(DataMessage.Contact.Email.Type.WORK); break;
|
||||
case MOBILE: emailBuilder.setType(DataMessage.Contact.Email.Type.MOBILE); break;
|
||||
case CUSTOM: emailBuilder.setType(DataMessage.Contact.Email.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + email.getType());
|
||||
}
|
||||
|
||||
if (email.getLabel().isPresent()) emailBuilder.setLabel(email.getLabel().get());
|
||||
|
||||
contactBuilder.addEmail(emailBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getPhone().isPresent()) {
|
||||
for (SharedContact.Phone phone : contact.getPhone().get()) {
|
||||
DataMessage.Contact.Phone.Builder phoneBuilder = DataMessage.Contact.Phone.newBuilder()
|
||||
.setValue(phone.getValue());
|
||||
|
||||
switch (phone.getType()) {
|
||||
case HOME: phoneBuilder.setType(DataMessage.Contact.Phone.Type.HOME); break;
|
||||
case WORK: phoneBuilder.setType(DataMessage.Contact.Phone.Type.WORK); break;
|
||||
case MOBILE: phoneBuilder.setType(DataMessage.Contact.Phone.Type.MOBILE); break;
|
||||
case CUSTOM: phoneBuilder.setType(DataMessage.Contact.Phone.Type.CUSTOM); break;
|
||||
default: throw new AssertionError("Unknown type: " + phone.getType());
|
||||
}
|
||||
|
||||
if (phone.getLabel().isPresent()) phoneBuilder.setLabel(phone.getLabel().get());
|
||||
|
||||
contactBuilder.addNumber(phoneBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getAvatar().isPresent()) {
|
||||
AttachmentPointer pointer = contact.getAvatar().get().getAttachment().isStream() ? createAttachmentPointer(contact.getAvatar().get().getAttachment().asStream(), recipient)
|
||||
: createAttachmentPointer(contact.getAvatar().get().getAttachment().asPointer());
|
||||
contactBuilder.setAvatar(DataMessage.Contact.Avatar.newBuilder()
|
||||
.setAvatar(pointer)
|
||||
.setIsProfile(contact.getAvatar().get().isProfile()));
|
||||
}
|
||||
|
||||
if (contact.getOrganization().isPresent()) {
|
||||
contactBuilder.setOrganization(contact.getOrganization().get());
|
||||
}
|
||||
|
||||
results.add(contactBuilder.build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<SendMessageResult> sendMessage(long messageID,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean isClosedGroup,
|
||||
boolean notifyPNServer)
|
||||
{
|
||||
List<SendMessageResult> results = new LinkedList<>();
|
||||
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
|
||||
|
||||
while (recipientIterator.hasNext()) {
|
||||
SignalServiceAddress recipient = recipientIterator.next();
|
||||
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, true, isClosedGroup, notifyPNServer, Optional.absent());
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessage(SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption)
|
||||
throws IOException
|
||||
{
|
||||
// Loki - This method is only invoked for various types of control messages
|
||||
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, useFallbackEncryption, false,Optional.absent());
|
||||
}
|
||||
|
||||
public SendMessageResult sendMessage(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
{
|
||||
boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty();
|
||||
long threadID;
|
||||
if (isSelfSend) {
|
||||
threadID = threadDatabase.getThreadID(syncTarget.get());
|
||||
} else {
|
||||
threadID = threadDatabase.getThreadID(recipient.getNumber());
|
||||
}
|
||||
PublicChat publicChat = threadDatabase.getPublicChat(threadID);
|
||||
if (publicChat != null) {
|
||||
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
|
||||
} else {
|
||||
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessageToPublicChat(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
long timestamp,
|
||||
byte[] content,
|
||||
PublicChat publicChat) {
|
||||
if (messageID == 0) {
|
||||
Log.d("Loki", "Missing message ID.");
|
||||
}
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
try {
|
||||
DataMessage data = Content.parseFrom(content).getDataMessage();
|
||||
String body = (data.getBody() != null && data.getBody().length() > 0) ? data.getBody() : Long.toString(data.getTimestamp());
|
||||
PublicChatMessage.Quote quote = null;
|
||||
if (data.hasQuote()) {
|
||||
long quoteID = data.getQuote().getId();
|
||||
String quoteePublicKey = data.getQuote().getAuthor();
|
||||
long serverID = messageDatabase.getQuoteServerID(quoteID, quoteePublicKey);
|
||||
quote = new PublicChatMessage.Quote(quoteID, quoteePublicKey, data.getQuote().getText(), serverID);
|
||||
}
|
||||
DataMessage.Preview linkPreview = (data.getPreviewList().size() > 0) ? data.getPreviewList().get(0) : null;
|
||||
ArrayList<PublicChatMessage.Attachment> attachments = new ArrayList<>();
|
||||
if (linkPreview != null && linkPreview.hasImage()) {
|
||||
AttachmentPointer attachmentPointer = linkPreview.getImage();
|
||||
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
|
||||
attachments.add(new PublicChatMessage.Attachment(
|
||||
PublicChatMessage.Attachment.Kind.LinkPreview,
|
||||
publicChat.getServer(),
|
||||
attachmentPointer.getId(),
|
||||
attachmentPointer.getContentType(),
|
||||
attachmentPointer.getSize(),
|
||||
attachmentPointer.getFileName(),
|
||||
attachmentPointer.getFlags(),
|
||||
attachmentPointer.getWidth(),
|
||||
attachmentPointer.getHeight(),
|
||||
caption,
|
||||
attachmentPointer.getUrl(),
|
||||
linkPreview.getUrl(),
|
||||
linkPreview.getTitle()
|
||||
));
|
||||
}
|
||||
for (AttachmentPointer attachmentPointer : data.getAttachmentsList()) {
|
||||
String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null;
|
||||
attachments.add(new PublicChatMessage.Attachment(
|
||||
PublicChatMessage.Attachment.Kind.Attachment,
|
||||
publicChat.getServer(),
|
||||
attachmentPointer.getId(),
|
||||
attachmentPointer.getContentType(),
|
||||
attachmentPointer.getSize(),
|
||||
attachmentPointer.getFileName(),
|
||||
attachmentPointer.getFlags(),
|
||||
attachmentPointer.getWidth(),
|
||||
attachmentPointer.getHeight(),
|
||||
caption,
|
||||
attachmentPointer.getUrl(),
|
||||
null,
|
||||
null
|
||||
));
|
||||
}
|
||||
PublicChatMessage message = new PublicChatMessage(userPublicKey, "", body, timestamp, PublicChatAPI.getPublicChatMessageType(), quote, attachments);
|
||||
byte[] privateKey = store.getIdentityKeyPair().getPrivateKey().serialize();
|
||||
new PublicChatAPI(userPublicKey, privateKey, apiDatabase, userDatabase, openGroupDatabase).sendMessage(message, publicChat.getChannel(), publicChat.getServer()).success(new Function1<PublicChatMessage, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(PublicChatMessage message) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
messageDatabase.setServerID(messageID, message.getServerID());
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
try {
|
||||
f.get(1, TimeUnit.MINUTES);
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
} catch (Exception exception) {
|
||||
return SendMessageResult.networkFailure(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private SendMessageResult sendMessageToPrivateChat(final long messageID,
|
||||
final SignalServiceAddress recipient,
|
||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||
final long timestamp,
|
||||
byte[] content,
|
||||
boolean online,
|
||||
int ttl,
|
||||
boolean useFallbackEncryption,
|
||||
boolean isClosedGroup,
|
||||
final boolean notifyPNServer,
|
||||
Optional<String> syncTarget)
|
||||
{
|
||||
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
|
||||
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
|
||||
// Loki - Remove this when we have shared sender keys
|
||||
// ========
|
||||
if (messages.getMessages().isEmpty()) {
|
||||
return SendMessageResult.success(recipient, false, false);
|
||||
}
|
||||
// ========
|
||||
OutgoingPushMessage message = messages.getMessages().get(0);
|
||||
final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type);
|
||||
final String senderID;
|
||||
if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) {
|
||||
senderID = recipient.getNumber();
|
||||
} else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) {
|
||||
senderID = "";
|
||||
} else {
|
||||
senderID = userPublicKey;
|
||||
}
|
||||
final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
// Make sure we have a valid ttl; otherwise default to 2 days
|
||||
if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); }
|
||||
final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular);
|
||||
final int __ttl = ttl;
|
||||
final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false);
|
||||
SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1<Set<Promise<Map<?, ?>, Exception>>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Set<Promise<Map<?, ?>, Exception>> promises) {
|
||||
final boolean[] isSuccess = { false };
|
||||
final int[] promiseCount = {promises.size()};
|
||||
final int[] errorCount = { 0 };
|
||||
for (Promise<Map<?, ?>, Exception> promise : promises) {
|
||||
promise.success(new Function1<Map<?, ?>, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Map<?, ?> map) {
|
||||
if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageSent", timestamp);
|
||||
}
|
||||
isSuccess[0] = true;
|
||||
if (notifyPNServer) {
|
||||
PushNotificationAPI.shared.notify(messageInfo);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.set(Unit.INSTANCE);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(new Function1<Exception, Unit>() {
|
||||
|
||||
@Override
|
||||
public Unit invoke(Exception exception) {
|
||||
errorCount[0] += 1;
|
||||
if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed
|
||||
if (__ttl == regularMessageTTL) {
|
||||
broadcaster.broadcast("messageFailed", timestamp);
|
||||
}
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
});
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}).fail(exception -> {
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
f.setException(exception);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
|
||||
try {
|
||||
f.get(1, TimeUnit.MINUTES);
|
||||
return SendMessageResult.success(recipient, false, true);
|
||||
} catch (Exception exception) {
|
||||
Throwable underlyingError = exception.getCause();
|
||||
if (underlyingError instanceof SnodeAPI.Error) {
|
||||
return SendMessageResult.lokiAPIError(recipient, (SnodeAPI.Error)underlyingError);
|
||||
} else {
|
||||
return SendMessageResult.networkFailure(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AttachmentPointer> createAttachmentPointers(Optional<List<SignalServiceAttachment>> attachments, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
List<AttachmentPointer> pointers = new LinkedList<>();
|
||||
|
||||
if (!attachments.isPresent() || attachments.get().isEmpty()) {
|
||||
Log.w(TAG, "No attachments present...");
|
||||
return pointers;
|
||||
}
|
||||
|
||||
for (SignalServiceAttachment attachment : attachments.get()) {
|
||||
if (attachment.isStream()) {
|
||||
Log.w(TAG, "Found attachment, creating pointer...");
|
||||
pointers.add(createAttachmentPointer(attachment.asStream(), recipient));
|
||||
} else if (attachment.isPointer()) {
|
||||
Log.w(TAG, "Including existing attachment pointer...");
|
||||
pointers.add(createAttachmentPointer(attachment.asPointer()));
|
||||
}
|
||||
}
|
||||
|
||||
return pointers;
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
|
||||
.setContentType(attachment.getContentType())
|
||||
.setId(attachment.getId())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get())
|
||||
.setUrl(attachment.getUrl());
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
|
||||
if (attachment.getPreview().isPresent()) {
|
||||
builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get()));
|
||||
}
|
||||
|
||||
if (attachment.getWidth() > 0) {
|
||||
builder.setWidth(attachment.getWidth());
|
||||
}
|
||||
|
||||
if (attachment.getHeight() > 0) {
|
||||
builder.setHeight(attachment.getHeight());
|
||||
}
|
||||
|
||||
if (attachment.getVoiceNote()) {
|
||||
builder.setFlags(AttachmentPointer.Flags.VOICE_MESSAGE_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.getCaption().isPresent()) {
|
||||
builder.setCaption(attachment.getCaption().get());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
return createAttachmentPointer(attachment, false, recipient);
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, boolean usePadding, SignalServiceAddress recipient)
|
||||
throws IOException
|
||||
{
|
||||
SignalServiceAttachmentPointer pointer = uploadAttachment(attachment, usePadding, recipient);
|
||||
return createAttachmentPointer(pointer);
|
||||
}
|
||||
|
||||
private OutgoingPushMessageList getSessionProtocolEncryptedMessage(SignalServiceAddress recipient, long timestamp, byte[] plaintext)
|
||||
{
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
|
||||
String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group
|
||||
boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey);
|
||||
String encryptionPublicKey;
|
||||
if (isClosedGroup) {
|
||||
ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey);
|
||||
encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair);
|
||||
} else {
|
||||
encryptionPublicKey = publicKey;
|
||||
}
|
||||
byte[] ciphertext = sessionProtocolImpl.encrypt(PushTransportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey);
|
||||
String body = Base64.encodeBytes(ciphertext);
|
||||
int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE :
|
||||
SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE;
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(type, body);
|
||||
messages.add(message);
|
||||
|
||||
return new OutgoingPushMessageList(publicKey, timestamp, messages, false);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.session.libsignal.service.internal.push;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class OutgoingPushMessage {
|
||||
|
||||
@JsonProperty
|
||||
public int type;
|
||||
@JsonProperty
|
||||
public String content;
|
||||
|
||||
public OutgoingPushMessage(int type, String content)
|
||||
{
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2016 Open Whisper Systems
|
||||
*
|
||||
* Licensed according to the LICENSE file in this repository.
|
||||
*/
|
||||
|
||||
package org.session.libsignal.service.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OutgoingPushMessageList {
|
||||
|
||||
@JsonProperty
|
||||
private String destination;
|
||||
|
||||
@JsonProperty
|
||||
private long timestamp;
|
||||
|
||||
@JsonProperty
|
||||
private List<OutgoingPushMessage> messages;
|
||||
|
||||
@JsonProperty
|
||||
private boolean online;
|
||||
|
||||
public OutgoingPushMessageList(String destination,
|
||||
long timestamp,
|
||||
List<OutgoingPushMessage> messages,
|
||||
boolean online)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.destination = destination;
|
||||
this.messages = messages;
|
||||
this.online = online;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public List<OutgoingPushMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public boolean isOnline() {
|
||||
return online;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user