mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 17:27:42 +00:00
sync dev
This commit is contained in:
@@ -30,7 +30,6 @@ import android.widget.TextView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsignal.service.loki.utilities.toHexString
|
||||
@@ -239,6 +240,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
val members = this.members.map {
|
||||
Recipient.from(this, Address.fromSerialized(it), false)
|
||||
}.toSet()
|
||||
val originalMembers = this.originalMembers.map {
|
||||
Recipient.from(this, Address.fromSerialized(it), false)
|
||||
}.toSet()
|
||||
|
||||
val admins = members.toSet() //TODO For now, consider all the users to be admins.
|
||||
|
||||
@@ -272,11 +276,25 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
if (isSSKBasedClosedGroup) {
|
||||
isLoading = true
|
||||
loaderContainer.fadeIn()
|
||||
val promise: Promise<Unit, Exception>
|
||||
if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||
promise = ClosedGroupsProtocolV2.leave(this, groupPublicKey!!)
|
||||
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||
ClosedGroupsProtocolV2.leave(this, groupPublicKey!!)
|
||||
} else {
|
||||
promise = ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
|
||||
// TODO: uncomment when we switch to sending new explicit updates after clients update
|
||||
// task {
|
||||
// val name =
|
||||
// if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name)
|
||||
// else Promise.of(Unit)
|
||||
// name.get()
|
||||
// members.filterNot { it in originalMembers }.let { adds ->
|
||||
// if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
|
||||
// else Promise.of(Unit)
|
||||
// }.get()
|
||||
// originalMembers.filterNot { it in members }.let { removes ->
|
||||
// if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
|
||||
// else Promise.of(Unit)
|
||||
// }.get()
|
||||
// }
|
||||
ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
|
||||
}
|
||||
promise.successUi {
|
||||
loaderContainer.fadeOut()
|
||||
|
@@ -31,6 +31,10 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
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>) : Kind() // The new encryption key pair encrypted for each member individually
|
||||
}
|
||||
|
||||
@@ -88,6 +92,23 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
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) }
|
||||
@@ -123,6 +144,21 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
|
||||
kind = Kind.EncryptionKeyPair(wrappers)
|
||||
}
|
||||
"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)
|
||||
@@ -154,6 +190,21 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR
|
||||
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
|
||||
}
|
||||
Kind.Leave -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT
|
||||
}
|
||||
is Kind.RemoveMembers -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.AddMembers -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.NameChange -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE
|
||||
closedGroupUpdate.name = kind.name
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupUpdateV2 = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
@@ -162,10 +213,9 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
|
||||
val address = SignalServiceAddress(destination)
|
||||
val recipient = recipient(context, destination)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl: Int
|
||||
when (kind) {
|
||||
is Kind.EncryptionKeyPair -> ttl = 4 * 24 * 60 * 60 * 1000
|
||||
else -> ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
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
|
||||
|
@@ -5,6 +5,7 @@ import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.task
|
||||
import org.session.libsignal.libsignal.ecc.Curve
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
|
||||
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
|
||||
@@ -12,21 +13,25 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||
import org.session.libsignal.libsignal.util.guava.Optional
|
||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||
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
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
|
||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
|
||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.session.libsignal.utilities.Hex
|
||||
|
||||
import org.session.libsession.messaging.threads.Address
|
||||
import org.session.libsession.messaging.threads.GroupRecord
|
||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@@ -36,7 +41,7 @@ import java.util.*
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
object ClosedGroupsProtocolV2 {
|
||||
val groupSizeLimit = 100
|
||||
const val groupSizeLimit = 100
|
||||
|
||||
sealed class Error(val description: String) : Exception() {
|
||||
object NoThread : Error("Couldn't find a thread associated with the given group public key")
|
||||
@@ -44,7 +49,7 @@ object ClosedGroupsProtocolV2 {
|
||||
object InvalidUpdate : Error("Invalid group update.")
|
||||
}
|
||||
|
||||
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
|
||||
val deferred = deferred<String, Exception>()
|
||||
ThreadUtils.queue {
|
||||
// Prepare
|
||||
@@ -60,7 +65,7 @@ object ClosedGroupsProtocolV2 {
|
||||
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) }))
|
||||
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Send a closed group update message to all members individually
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
|
||||
@@ -76,7 +81,7 @@ object ClosedGroupsProtocolV2 {
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
// Fulfill the promise
|
||||
@@ -87,7 +92,155 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun leave(context: Context, groupPublicKey: String): Promise<Unit, Exception> {
|
||||
fun explicitLeave(context: Context, groupPublicKey: String): 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 = 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
|
||||
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)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Remove the group private key and unsubscribe from PNs
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
// Notify the user
|
||||
val infoType = GroupContext.Type.QUIT
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>): Promise<Any, java.lang.Exception> {
|
||||
return task {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@task Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
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 encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
return@task Error.NoKeyPair
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// 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)
|
||||
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>): Promise<Any, Exception> {
|
||||
return task {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@task Error.NoThread
|
||||
}
|
||||
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
|
||||
// Save the new group members
|
||||
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
|
||||
if (encryptionKeyPair == null) {
|
||||
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
|
||||
return@task 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.")
|
||||
return@task Error.InvalidUpdate
|
||||
}
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
val members = group.members.map { it.serialize() }.toSet()
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't leave nonexistent closed group.")
|
||||
return@queue deferred.reject(Error.NoThread)
|
||||
}
|
||||
// Send the update to the group
|
||||
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
|
||||
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, newName)
|
||||
// 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)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun leave(context: Context, groupPublicKey: String): Promise<Unit, Exception> {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
@@ -108,7 +261,7 @@ object ClosedGroupsProtocolV2 {
|
||||
return update(context, groupPublicKey, newMembers, name)
|
||||
}
|
||||
|
||||
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||
fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
ThreadUtils.queue {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
@@ -181,7 +334,7 @@ object ClosedGroupsProtocolV2 {
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||
deferred.resolve(Unit)
|
||||
@@ -223,29 +376,41 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(closedGroupUpdate)) { return; }
|
||||
fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
if (!isValid(closedGroupUpdate, senderPublicKey)) { return }
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
|
||||
else -> {
|
||||
// Do nothing
|
||||
Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2): Boolean {
|
||||
when (closedGroupUpdate.type) {
|
||||
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean {
|
||||
return when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
|
||||
return !closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0
|
||||
(!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
&& !(closedGroupUpdate.encryptionKeyPair.publicKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0)
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> {
|
||||
return !closedGroupUpdate.name.isNullOrEmpty()
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED,
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> {
|
||||
closedGroupUpdate.membersCount > 0
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> return true
|
||||
else -> return false
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> {
|
||||
senderPublicKey.isNotEmpty()
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE,
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> {
|
||||
!closedGroupUpdate.name.isNullOrEmpty()
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,12 +442,144 @@ object ClosedGroupsProtocolV2 {
|
||||
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
|
||||
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||
// Notify the user
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
|
||||
public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
fun handleClosedGroupMembersRemoved(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.any { it in updateMembers }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members - updateMembers
|
||||
// user should be posting MEMBERS_LEFT so this should not be encountered
|
||||
val senderLeft = senderPublicKey in updateMembers
|
||||
if (senderLeft) {
|
||||
Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
|
||||
}
|
||||
val wasCurrentUserRemoved = userPublicKey in updateMembers
|
||||
|
||||
// admin should send a MEMBERS_LEFT message but handled here in case
|
||||
if (didAdminLeave || wasCurrentUserRemoved) {
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
|
||||
}
|
||||
}
|
||||
val (contextType, signalType) =
|
||||
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
|
||||
}
|
||||
|
||||
fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
|
||||
// Users that are part of this remove update
|
||||
val updateMembers = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
// newMembers to save is old members minus removed members
|
||||
val newMembers = members + updateMembers
|
||||
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
|
||||
fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val name = closedGroupUpdate.name
|
||||
groupDB.updateTitle(groupID, name)
|
||||
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
|
||||
}
|
||||
|
||||
private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Check the user leaving isn't us, will already be handled
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
if (senderPublicKey == userPublicKey) {
|
||||
return
|
||||
}
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = doubleEncodeGroupID(groupPublicKey)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
// Check common group update logic
|
||||
val members = group.members.map { it.serialize() }
|
||||
val admins = group.admins.map { it.toString() }
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// If admin leaves the group is disbanded
|
||||
val didAdminLeave = admins.contains(senderPublicKey)
|
||||
val updatedMemberList = members - senderPublicKey
|
||||
|
||||
if (didAdminLeave) {
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
} else {
|
||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
if (isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
|
||||
}
|
||||
}
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
|
||||
}
|
||||
|
||||
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
@@ -297,15 +594,8 @@ object ClosedGroupsProtocolV2 {
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
val newMembers = members.toSet().minus(oldMembers)
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.createAt > sentTimestamp) {
|
||||
Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
return
|
||||
}
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (!oldMembers.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
// Check common group update logic
|
||||
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
|
||||
return
|
||||
}
|
||||
// Check that the admin wasn't removed unless the group was destroyed entirely
|
||||
@@ -316,20 +606,13 @@ object ClosedGroupsProtocolV2 {
|
||||
// Remove the group from the user's set of public keys to poll for if the current user was removed
|
||||
val wasCurrentUserRemoved = !members.contains(userPublicKey)
|
||||
if (wasCurrentUserRemoved) {
|
||||
apiDB.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
|
||||
}
|
||||
// Generate and distribute a new encryption key pair if needed
|
||||
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
|
||||
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
|
||||
if (wasAnyUserRemoved && isCurrentUserAdmin) {
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members.minus(newMembers))
|
||||
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
@@ -339,11 +622,39 @@ object ClosedGroupsProtocolV2 {
|
||||
}
|
||||
// Notify the user
|
||||
val wasSenderRemoved = !members.contains(senderPublicKey)
|
||||
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
|
||||
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
|
||||
}
|
||||
|
||||
private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) {
|
||||
apiDB.removeClosedGroupPublicKey(groupPublicKey)
|
||||
// Remove the key pairs
|
||||
apiDB.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
// Mark the group as inactive
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
}
|
||||
|
||||
private fun isValidGroupUpdate(group: GroupRecord,
|
||||
sentTimestamp: Long,
|
||||
senderPublicKey: String): Boolean {
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the message isn't from before the group was created
|
||||
if (group.createdAt > sentTimestamp) {
|
||||
Log.d("Loki", "Ignoring closed group update from before thread was created.")
|
||||
return false
|
||||
}
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (senderPublicKey !in oldMembers) {
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleGroupEncryptionKeyPair(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
@@ -373,9 +684,9 @@ object ClosedGroupsProtocolV2 {
|
||||
Log.d("Loki", "Received a new closed group encryption key pair")
|
||||
}
|
||||
|
||||
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type,
|
||||
name: String, members: Collection<String>, admins: Collection<String>) {
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type0)
|
||||
.setName(name)
|
||||
@@ -388,10 +699,10 @@ object ClosedGroupsProtocolV2 {
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
|
||||
.setType(type)
|
||||
.setName(name)
|
||||
|
Reference in New Issue
Block a user