diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index abf1a29063..e86543ae8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -268,7 +268,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { "SendReadReceiptJob", "TypingSendJob", "AttachmentUploadJob", - "RequestGroupInfoJob"); + "RequestGroupInfoJob", + "ClosedGroupUpdateMessageSendJobV2"); } db.setTransactionSuccessful(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 66cf7e68b1..e7d6f33e7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 9efbac7f72..a3644d8ba7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -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 factoryHashMap = new HashMap() {{ 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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 62a87e3769..1657515782 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt deleted file mode 100644 index ba692a041e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ /dev/null @@ -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, val admins: Collection) : Kind() - class Update(val name: String, val members: Collection) : Kind() - object Leave : Kind() - class RemoveMembers(val members: Collection) : Kind() - class AddMembers(val members: Collection) : Kind() - class NameChange(val name: String) : Kind() - class EncryptionKeyPair(val wrappers: Collection, 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 { - - 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 = 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() { } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index d9e26ba4f0..6e03cb148f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -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>() - - 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): Promise { - val deferred = deferred() - 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 { - val deferred = deferred() - 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) { - 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) { - 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) { - // 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, 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) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java deleted file mode 100644 index 154ab3c025..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ /dev/null @@ -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, - 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 recipients, - List> 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, - 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 sendMessage(long messageID, - List recipients, - List> 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 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 createSharedContactContent(List contacts, SignalServiceAddress recipient) - throws IOException - { - List 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 sendMessage(long messageID, - List recipients, - List> unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean isClosedGroup, - boolean notifyPNServer) - { - List results = new LinkedList<>(); - Iterator recipientIterator = recipients.iterator(); - Iterator> 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, - 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, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isClosedGroup, - boolean notifyPNServer, - Optional 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() }; - 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 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() { - - @Override - public Unit invoke(PublicChatMessage message) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - messageDatabase.setServerID(messageID, message.getServerID()); - f.set(Unit.INSTANCE); - return Unit.INSTANCE; - } - }).fail(new Function1() { - - @Override - public Unit invoke(Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } - }); - } catch (Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - } - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)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, - final long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isClosedGroup, - final boolean notifyPNServer, - Optional syncTarget) - { - final SettableFuture[] future = { new SettableFuture() }; - 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, Exception>>, Unit>() { - - @Override - public Unit invoke(Set, Exception>> promises) { - final boolean[] isSuccess = { false }; - final int[] promiseCount = {promises.size()}; - final int[] errorCount = { 0 }; - for (Promise, Exception> promise : promises) { - promise.success(new Function1, 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 f = (SettableFuture)future[0]; - f.set(Unit.INSTANCE); - return Unit.INSTANCE; - } - }).fail(new Function1() { - - @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 f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } - }); - } - return Unit.INSTANCE; - } - }).fail(exception -> { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - }); - - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)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 createAttachmentPointers(Optional> attachments, SignalServiceAddress recipient) - throws IOException - { - List 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 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); - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java deleted file mode 100644 index 2744ee97a5..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java +++ /dev/null @@ -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; - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java deleted file mode 100644 index 9a23aae9d5..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java +++ /dev/null @@ -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 messages; - - @JsonProperty - private boolean online; - - public OutgoingPushMessageList(String destination, - long timestamp, - List messages, - boolean online) - { - this.timestamp = timestamp; - this.destination = destination; - this.messages = messages; - this.online = online; - } - - public String getDestination() { - return destination; - } - - public List getMessages() { - return messages; - } - - public long getTimestamp() { - return timestamp; - } - - public boolean isOnline() { - return online; - } -}