@ -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 ( !is Valid ( closedGroupUpdate )) { return ; }
fun handleMessage ( context : Context , closedGroupUpdate : SignalServiceProtos . ClosedGroupUpdateV2 , sentTimestamp : Long , groupPublicKey : String , senderPublicKey : String ) {
if ( !is Valid ( 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 . MEMBERS _ADDED ,
SignalServiceProtos . ClosedGroupUpdateV2 . Type . MEMBERS _REMOVED -> {
closedGroupUpdate . membersCount > 0
}
SignalServiceProtos . ClosedGroupUpdateV2 . Type . UPDATE -> {
return ! closedGroupUpdate . name . isNullOrEmpty ( )
SignalServiceProtos . ClosedGroupUpdateV2 . Type . MEMBER_LEFT -> {
senderPublicKey . isNot Empty( )
}
SignalServiceProtos . ClosedGroupUpdateV2 . Type . ENCRYPTION _KEY _PAIR -> return true
else -> return false
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 ( !is ValidGroupUpdate ( 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 ( !is ValidGroupUpdate ( 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 ( !is ValidGroupUpdate ( 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 ( !is ValidGroupUpdate ( 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 ( !is ValidGroupUpdate ( 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 )