2020-05-02 00:13:23 +02:00
package org.thoughtcrime.securesms.groups.v2.processing ;
import android.content.Context ;
2020-05-14 20:57:40 +02:00
import android.text.TextUtils ;
2020-05-02 00:13:23 +02:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
import androidx.annotation.WorkerThread ;
2020-09-05 15:09:31 +02:00
import com.annimon.stream.Stream ;
2020-05-02 00:13:23 +02:00
import org.signal.storageservice.protos.groups.local.DecryptedGroup ;
2020-05-13 18:36:57 +02:00
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange ;
2020-07-30 22:17:23 +02:00
import org.signal.storageservice.protos.groups.local.DecryptedMember ;
2020-05-21 22:04:57 +02:00
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember ;
2020-05-02 00:13:23 +02:00
import org.signal.zkgroup.VerificationFailedException ;
import org.signal.zkgroup.groups.GroupMasterKey ;
import org.signal.zkgroup.groups.GroupSecretParams ;
import org.thoughtcrime.securesms.database.DatabaseFactory ;
import org.thoughtcrime.securesms.database.GroupDatabase ;
2020-08-20 22:50:14 +02:00
import org.thoughtcrime.securesms.database.MessageDatabase ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.database.RecipientDatabase ;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context ;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies ;
import org.thoughtcrime.securesms.groups.GroupId ;
2020-10-06 16:21:56 +02:00
import org.thoughtcrime.securesms.groups.GroupMutation ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.groups.GroupNotAMemberException ;
import org.thoughtcrime.securesms.groups.GroupProtoUtil ;
2020-05-05 17:13:53 +02:00
import org.thoughtcrime.securesms.groups.GroupsV2Authorization ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet ;
2020-07-24 21:53:49 +02:00
import org.thoughtcrime.securesms.jobmanager.Job ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.jobmanager.JobManager ;
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob ;
2020-07-20 16:09:42 +02:00
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob ;
2020-07-08 23:10:58 +02:00
import org.thoughtcrime.securesms.keyvalue.SignalStore ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.logging.Log ;
import org.thoughtcrime.securesms.mms.MmsException ;
2020-05-06 18:42:54 +02:00
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage ;
2020-05-02 00:13:23 +02:00
import org.thoughtcrime.securesms.recipients.Recipient ;
import org.thoughtcrime.securesms.recipients.RecipientId ;
2020-05-13 18:36:57 +02:00
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage ;
import org.thoughtcrime.securesms.sms.IncomingTextMessage ;
import org.whispersystems.libsignal.util.guava.Optional ;
2020-05-02 00:13:23 +02:00
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry ;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil ;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api ;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException ;
2020-08-03 17:06:53 +02:00
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException ;
2020-05-13 18:36:57 +02:00
import org.whispersystems.signalservice.api.util.UuidUtil ;
2020-05-02 00:13:23 +02:00
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException ;
import java.io.IOException ;
import java.util.ArrayList ;
import java.util.Collection ;
import java.util.Collections ;
import java.util.List ;
import java.util.Locale ;
2020-07-24 21:53:49 +02:00
import java.util.Set ;
2020-05-02 00:13:23 +02:00
import java.util.UUID ;
/ * *
* Advances a groups state to a specified revision .
* /
public final class GroupsV2StateProcessor {
private static final String TAG = Log . tag ( GroupsV2StateProcessor . class ) ;
2020-09-09 16:59:09 +02:00
public static final int LATEST = GroupStateMapper . LATEST ;
/ * *
* Used to mark a group state as a placeholder when there is partial knowledge ( title and avater )
* gathered from a group join link .
* /
2020-08-26 17:51:25 +02:00
public static final int PLACEHOLDER_REVISION = GroupStateMapper . PLACEHOLDER_REVISION ;
2020-05-02 00:13:23 +02:00
2020-09-09 16:59:09 +02:00
/ * *
* Used to mark a group state as a placeholder when you have no knowledge at all of the group
* e . g . from a group master key from a storage service restore .
* /
public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper . RESTORE_PLACEHOLDER_REVISION ;
2020-05-02 00:13:23 +02:00
private final Context context ;
private final JobManager jobManager ;
private final RecipientDatabase recipientDatabase ;
private final GroupDatabase groupDatabase ;
private final GroupsV2Authorization groupsV2Authorization ;
private final GroupsV2Api groupsV2Api ;
public GroupsV2StateProcessor ( @NonNull Context context ) {
this . context = context . getApplicationContext ( ) ;
this . jobManager = ApplicationDependencies . getJobManager ( ) ;
this . groupsV2Authorization = ApplicationDependencies . getGroupsV2Authorization ( ) ;
this . groupsV2Api = ApplicationDependencies . getSignalServiceAccountManager ( ) . getGroupsV2Api ( ) ;
this . recipientDatabase = DatabaseFactory . getRecipientDatabase ( context ) ;
this . groupDatabase = DatabaseFactory . getGroupDatabase ( context ) ;
}
public StateProcessorForGroup forGroup ( @NonNull GroupMasterKey groupMasterKey ) {
return new StateProcessorForGroup ( groupMasterKey ) ;
}
public enum GroupState {
/ * *
* The message revision was inconsistent with server revision , should ignore
* /
INCONSISTENT ,
/ * *
* The local group was successfully updated to be consistent with the message revision
* /
GROUP_UPDATED ,
/ * *
* The local group is already consistent with the message revision or is ahead of the message revision
* /
GROUP_CONSISTENT_OR_AHEAD
}
public static class GroupUpdateResult {
private final GroupState groupState ;
2020-07-08 23:10:58 +02:00
@Nullable private final DecryptedGroup latestServer ;
2020-05-02 00:13:23 +02:00
GroupUpdateResult ( @NonNull GroupState groupState , @Nullable DecryptedGroup latestServer ) {
this . groupState = groupState ;
this . latestServer = latestServer ;
}
public GroupState getGroupState ( ) {
return groupState ;
}
public @Nullable DecryptedGroup getLatestServer ( ) {
return latestServer ;
}
}
public final class StateProcessorForGroup {
private final GroupMasterKey masterKey ;
private final GroupId . V2 groupId ;
private final GroupSecretParams groupSecretParams ;
private StateProcessorForGroup ( @NonNull GroupMasterKey groupMasterKey ) {
this . masterKey = groupMasterKey ;
this . groupId = GroupId . v2 ( masterKey ) ;
this . groupSecretParams = GroupSecretParams . deriveFromMasterKey ( groupMasterKey ) ;
}
/ * *
* Using network where required , will attempt to bring the local copy of the group up to the revision specified .
*
* @param revision use { @link # LATEST } to get latest .
* /
@WorkerThread
public GroupUpdateResult updateLocalGroupToRevision ( final int revision ,
2020-05-19 21:02:24 +02:00
final long timestamp ,
@Nullable DecryptedGroupChange signedGroupChange )
2020-05-02 00:13:23 +02:00
throws IOException , GroupNotAMemberException
{
if ( localIsAtLeast ( revision ) ) {
return new GroupUpdateResult ( GroupState . GROUP_CONSISTENT_OR_AHEAD , null ) ;
}
2020-05-19 21:02:24 +02:00
GlobalGroupState inputGroupState = null ;
DecryptedGroup localState = groupDatabase . getGroup ( groupId )
. transform ( g - > g . requireV2GroupProperties ( ) . getDecryptedGroup ( ) )
. orNull ( ) ;
2020-07-30 22:17:23 +02:00
if ( signedGroupChange ! = null & &
localState ! = null & &
2020-05-29 19:35:40 +02:00
localState . getRevision ( ) + 1 = = signedGroupChange . getRevision ( ) & &
revision = = signedGroupChange . getRevision ( ) )
2020-05-19 21:02:24 +02:00
{
2020-07-08 23:10:58 +02:00
if ( SignalStore . internalValues ( ) . gv2IgnoreP2PChanges ( ) ) {
Log . w ( TAG , "Ignoring P2P group change by setting" ) ;
} else {
try {
Log . i ( TAG , "Applying P2P group change" ) ;
DecryptedGroup newState = DecryptedGroupUtil . apply ( localState , signedGroupChange ) ;
inputGroupState = new GlobalGroupState ( localState , Collections . singletonList ( new ServerGroupLogEntry ( newState , signedGroupChange ) ) ) ;
2020-08-03 17:06:53 +02:00
} catch ( NotAbleToApplyGroupV2ChangeException e ) {
2020-07-08 23:10:58 +02:00
Log . w ( TAG , "Unable to apply P2P group change" , e ) ;
}
2020-05-19 21:02:24 +02:00
}
2020-05-13 18:36:57 +02:00
}
2020-05-19 21:02:24 +02:00
if ( inputGroupState = = null ) {
try {
2020-09-09 16:59:09 +02:00
boolean latestRevisionOnly = revision = = LATEST & & ( localState = = null | | localState . getRevision ( ) = = GroupsV2StateProcessor . RESTORE_PLACEHOLDER_REVISION ) ;
inputGroupState = queryServer ( localState , latestRevisionOnly ) ;
2020-05-19 21:02:24 +02:00
} catch ( GroupNotAMemberException e ) {
2020-08-26 17:51:25 +02:00
if ( localState ! = null & & signedGroupChange ! = null ) {
try {
Log . i ( TAG , "Applying P2P group change when not a member" ) ;
DecryptedGroup newState = DecryptedGroupUtil . applyWithoutRevisionCheck ( localState , signedGroupChange ) ;
inputGroupState = new GlobalGroupState ( localState , Collections . singletonList ( new ServerGroupLogEntry ( newState , signedGroupChange ) ) ) ;
} catch ( NotAbleToApplyGroupV2ChangeException failed ) {
Log . w ( TAG , "Unable to apply P2P group change when not a member" , failed ) ;
}
}
if ( inputGroupState = = null ) {
if ( localState ! = null & & DecryptedGroupUtil . isPendingOrRequesting ( localState , Recipient . self ( ) . getUuid ( ) . get ( ) ) ) {
Log . w ( TAG , "Unable to query server for group " + groupId + " server says we're not in group, but we think we are a pending or requesting member" ) ;
} else {
Log . w ( TAG , "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message" ) ;
insertGroupLeave ( ) ;
}
throw e ;
}
2020-05-19 21:02:24 +02:00
}
} else {
Log . i ( TAG , "Saved server query for group change" ) ;
}
2020-05-02 00:13:23 +02:00
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper . partiallyAdvanceGroupState ( inputGroupState , revision ) ;
DecryptedGroup newLocalState = advanceGroupStateResult . getNewGlobalGroupState ( ) . getLocalState ( ) ;
if ( newLocalState = = null | | newLocalState = = inputGroupState . getLocalState ( ) ) {
return new GroupUpdateResult ( GroupState . GROUP_CONSISTENT_OR_AHEAD , null ) ;
}
updateLocalDatabaseGroupState ( inputGroupState , newLocalState ) ;
2020-09-05 15:09:31 +02:00
determineProfileSharing ( inputGroupState , newLocalState ) ;
2020-09-09 16:59:09 +02:00
if ( localState ! = null & & localState . getRevision ( ) = = GroupsV2StateProcessor . RESTORE_PLACEHOLDER_REVISION ) {
Log . i ( TAG , "Inserting single update message for restore placeholder" ) ;
2020-10-06 16:21:56 +02:00
insertUpdateMessages ( timestamp , null , Collections . singleton ( new LocalGroupLogEntry ( newLocalState , null ) ) ) ;
2020-09-09 16:59:09 +02:00
} else {
2020-10-06 16:21:56 +02:00
insertUpdateMessages ( timestamp , localState , advanceGroupStateResult . getProcessedLogEntries ( ) ) ;
2020-09-09 16:59:09 +02:00
}
2020-05-02 00:13:23 +02:00
persistLearnedProfileKeys ( inputGroupState ) ;
GlobalGroupState remainingWork = advanceGroupStateResult . getNewGlobalGroupState ( ) ;
2020-07-08 23:10:58 +02:00
if ( remainingWork . getServerHistory ( ) . size ( ) > 0 ) {
2020-07-20 16:09:42 +02:00
Log . i ( TAG , String . format ( Locale . US , "There are more revisions on the server for this group, scheduling for later, V[%d..%d]" , newLocalState . getRevision ( ) + 1 , remainingWork . getLatestRevisionNumber ( ) ) ) ;
ApplicationDependencies . getJobManager ( ) . add ( new RequestGroupV2InfoJob ( groupId , remainingWork . getLatestRevisionNumber ( ) ) ) ;
2020-05-02 00:13:23 +02:00
}
return new GroupUpdateResult ( GroupState . GROUP_UPDATED , newLocalState ) ;
}
2020-05-13 18:36:57 +02:00
private void insertGroupLeave ( ) {
if ( ! groupDatabase . isActive ( groupId ) ) {
Log . w ( TAG , "Group has already been left." ) ;
return ;
}
Recipient groupRecipient = Recipient . externalGroup ( context , groupId ) ;
UUID selfUuid = Recipient . self ( ) . getUuid ( ) . get ( ) ;
DecryptedGroup decryptedGroup = groupDatabase . requireGroup ( groupId )
. requireV2GroupProperties ( )
. getDecryptedGroup ( ) ;
2020-05-29 19:35:40 +02:00
DecryptedGroup simulatedGroupState = DecryptedGroupUtil . removeMember ( decryptedGroup , selfUuid , decryptedGroup . getRevision ( ) + 1 ) ;
2020-05-13 18:36:57 +02:00
DecryptedGroupChange simulatedGroupChange = DecryptedGroupChange . newBuilder ( )
. setEditor ( UuidUtil . toByteString ( UuidUtil . UNKNOWN_UUID ) )
2020-05-29 19:35:40 +02:00
. setRevision ( simulatedGroupState . getRevision ( ) )
2020-05-13 18:36:57 +02:00
. addDeleteMembers ( UuidUtil . toByteString ( selfUuid ) )
. build ( ) ;
2020-10-06 16:21:56 +02:00
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil . createDecryptedGroupV2Context ( masterKey , new GroupMutation ( decryptedGroup , simulatedGroupChange , simulatedGroupState ) , null ) ;
2020-05-13 18:36:57 +02:00
OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage ( groupRecipient ,
decryptedGroupV2Context ,
null ,
System . currentTimeMillis ( ) ,
0 ,
false ,
null ,
Collections . emptyList ( ) ,
2020-08-05 22:45:52 +02:00
Collections . emptyList ( ) ,
2020-05-13 18:36:57 +02:00
Collections . emptyList ( ) ) ;
try {
2020-08-24 22:40:47 +02:00
MessageDatabase mmsDatabase = DatabaseFactory . getMmsDatabase ( context ) ;
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( groupRecipient ) ;
long id = mmsDatabase . insertMessageOutbox ( leaveMessage , threadId , false , null ) ;
2020-05-13 18:36:57 +02:00
mmsDatabase . markAsSent ( id , true ) ;
} catch ( MmsException e ) {
Log . w ( TAG , "Failed to insert leave message." , e ) ;
}
groupDatabase . setActive ( groupId , false ) ;
groupDatabase . remove ( groupId , Recipient . self ( ) . getId ( ) ) ;
}
2020-05-02 00:13:23 +02:00
/ * *
* @return true iff group exists locally and is at least the specified revision .
* /
private boolean localIsAtLeast ( int revision ) {
if ( groupDatabase . isUnknownGroup ( groupId ) | | revision = = LATEST ) {
return false ;
}
int dbRevision = groupDatabase . getGroup ( groupId ) . get ( ) . requireV2GroupProperties ( ) . getGroupRevision ( ) ;
return revision < = dbRevision ;
}
private void updateLocalDatabaseGroupState ( @NonNull GlobalGroupState inputGroupState ,
@NonNull DecryptedGroup newLocalState )
{
2020-05-14 20:57:40 +02:00
boolean needsAvatarFetch ;
2020-05-02 00:13:23 +02:00
if ( inputGroupState . getLocalState ( ) = = null ) {
groupDatabase . create ( masterKey , newLocalState ) ;
2020-05-14 20:57:40 +02:00
needsAvatarFetch = ! TextUtils . isEmpty ( newLocalState . getAvatar ( ) ) ;
2020-05-02 00:13:23 +02:00
} else {
groupDatabase . update ( masterKey , newLocalState ) ;
2020-05-14 20:57:40 +02:00
needsAvatarFetch = ! newLocalState . getAvatar ( ) . equals ( inputGroupState . getLocalState ( ) . getAvatar ( ) ) ;
2020-05-02 00:13:23 +02:00
}
2020-05-14 20:57:40 +02:00
if ( needsAvatarFetch ) {
jobManager . add ( new AvatarGroupsV2DownloadJob ( groupId , newLocalState . getAvatar ( ) ) ) ;
2020-05-02 00:13:23 +02:00
}
2020-09-05 15:09:31 +02:00
determineProfileSharing ( inputGroupState , newLocalState ) ;
}
private void determineProfileSharing ( @NonNull GlobalGroupState inputGroupState ,
@NonNull DecryptedGroup newLocalState )
{
if ( inputGroupState . getLocalState ( ) ! = null ) {
boolean wasAMemberAlready = DecryptedGroupUtil . findMemberByUuid ( inputGroupState . getLocalState ( ) . getMembersList ( ) , Recipient . self ( ) . getUuid ( ) . get ( ) ) . isPresent ( ) ;
if ( wasAMemberAlready ) {
Log . i ( TAG , "Skipping profile sharing detection as was already a full member before update" ) ;
return ;
}
}
Optional < DecryptedMember > selfAsMemberOptional = DecryptedGroupUtil . findMemberByUuid ( newLocalState . getMembersList ( ) , Recipient . self ( ) . getUuid ( ) . get ( ) ) ;
if ( selfAsMemberOptional . isPresent ( ) ) {
DecryptedMember selfAsMember = selfAsMemberOptional . get ( ) ;
int revisionJoinedAt = selfAsMember . getJoinedAtRevision ( ) ;
2020-07-30 22:17:23 +02:00
2020-09-05 15:09:31 +02:00
Optional < Recipient > addedByOptional = Stream . of ( inputGroupState . getServerHistory ( ) )
. map ( ServerGroupLogEntry : : getChange )
. filter ( c - > c ! = null & & c . getRevision ( ) = = revisionJoinedAt )
. findFirst ( )
. map ( c - > Optional . fromNullable ( UuidUtil . fromByteStringOrNull ( c . getEditor ( ) ) )
. transform ( a - > Recipient . externalPush ( context , UuidUtil . fromByteStringOrNull ( c . getEditor ( ) ) , null , false ) ) )
. orElse ( Optional . absent ( ) ) ;
2020-07-30 22:17:23 +02:00
2020-09-05 15:09:31 +02:00
if ( addedByOptional . isPresent ( ) ) {
Recipient addedBy = addedByOptional . get ( ) ;
2020-07-30 22:17:23 +02:00
2020-09-05 15:09:31 +02:00
Log . i ( TAG , String . format ( "Added as a full member of %s by %s" , groupId , addedBy . getId ( ) ) ) ;
if ( addedBy . isSystemContact ( ) | | addedBy . isProfileSharing ( ) ) {
Log . i ( TAG , "Group 'adder' is trusted. contact: " + addedBy . isSystemContact ( ) + ", profileSharing: " + addedBy . isProfileSharing ( ) ) ;
Log . i ( TAG , "Added to a group and auto-enabling profile sharing" ) ;
recipientDatabase . setProfileSharing ( Recipient . externalGroup ( context , groupId ) . getId ( ) , true ) ;
} else {
Log . i ( TAG , "Added to a group, but not enabling profile sharing, as 'adder' is not trusted" ) ;
2020-07-30 22:17:23 +02:00
}
} else {
2020-09-05 15:09:31 +02:00
Log . w ( TAG , "Could not find founding member during gv2 create. Not enabling profile sharing." ) ;
2020-07-30 22:17:23 +02:00
}
} else {
2020-09-05 15:09:31 +02:00
Log . i ( TAG , String . format ( "Added to %s, but not enabling profile sharing as not a fullMember." , groupId ) ) ;
2020-05-02 00:13:23 +02:00
}
}
2020-10-06 16:21:56 +02:00
private void insertUpdateMessages ( long timestamp ,
@Nullable DecryptedGroup previousGroupState ,
Collection < LocalGroupLogEntry > processedLogEntries )
{
2020-07-08 23:10:58 +02:00
for ( LocalGroupLogEntry entry : processedLogEntries ) {
if ( entry . getChange ( ) ! = null & & DecryptedGroupUtil . changeIsEmptyExceptForProfileKeyChanges ( entry . getChange ( ) ) & & ! DecryptedGroupUtil . changeIsEmpty ( entry . getChange ( ) ) ) {
2020-07-01 17:41:06 +02:00
Log . d ( TAG , "Skipping profile key changes only update message" ) ;
} else {
2020-10-06 16:21:56 +02:00
storeMessage ( GroupProtoUtil . createDecryptedGroupV2Context ( masterKey , new GroupMutation ( previousGroupState , entry . getChange ( ) , entry . getGroup ( ) ) , null ) , timestamp ) ;
2020-10-09 22:45:45 +02:00
timestamp + + ;
2020-07-01 17:41:06 +02:00
}
2020-10-06 16:21:56 +02:00
previousGroupState = entry . getGroup ( ) ;
2020-05-02 00:13:23 +02:00
}
}
private void persistLearnedProfileKeys ( @NonNull GlobalGroupState globalGroupState ) {
final ProfileKeySet profileKeys = new ProfileKeySet ( ) ;
2020-07-08 23:10:58 +02:00
for ( ServerGroupLogEntry entry : globalGroupState . getServerHistory ( ) ) {
2020-08-03 21:12:02 +02:00
if ( entry . getGroup ( ) ! = null ) {
profileKeys . addKeysFromGroupState ( entry . getGroup ( ) ) ;
}
if ( entry . getChange ( ) ! = null ) {
profileKeys . addKeysFromGroupChange ( entry . getChange ( ) ) ;
2020-05-21 22:04:57 +02:00
}
2020-05-02 00:13:23 +02:00
}
2020-07-24 21:53:49 +02:00
Set < RecipientId > updated = recipientDatabase . persistProfileKeySet ( profileKeys ) ;
2020-05-02 00:13:23 +02:00
if ( ! updated . isEmpty ( ) ) {
2020-07-24 21:53:49 +02:00
Log . i ( TAG , String . format ( Locale . US , "Learned %d new profile keys, fetching profiles" , updated . size ( ) ) ) ;
for ( Job job : RetrieveProfileJob . forRecipients ( updated ) ) {
jobManager . runSynchronously ( job , 5000 ) ;
}
2020-05-02 00:13:23 +02:00
}
}
2020-05-26 21:02:34 +02:00
private @NonNull GlobalGroupState queryServer ( @Nullable DecryptedGroup localState , boolean latestOnly )
2020-05-02 00:13:23 +02:00
throws IOException , GroupNotAMemberException
{
2020-07-08 23:10:58 +02:00
UUID selfUuid = Recipient . self ( ) . getUuid ( ) . get ( ) ;
DecryptedGroup latestServerGroup ;
List < ServerGroupLogEntry > history ;
2020-05-02 00:13:23 +02:00
try {
2020-05-05 17:13:53 +02:00
latestServerGroup = groupsV2Api . getGroup ( groupSecretParams , groupsV2Authorization . getAuthorizationForToday ( selfUuid , groupSecretParams ) ) ;
2020-05-02 00:13:23 +02:00
} catch ( NotInGroupException e ) {
throw new GroupNotAMemberException ( e ) ;
} catch ( VerificationFailedException | InvalidGroupStateException e ) {
throw new IOException ( e ) ;
}
2020-05-26 21:02:34 +02:00
if ( latestOnly | | ! GroupProtoUtil . isMember ( selfUuid , latestServerGroup . getMembersList ( ) ) ) {
2020-07-08 23:10:58 +02:00
history = Collections . singletonList ( new ServerGroupLogEntry ( latestServerGroup , null ) ) ;
2020-05-26 21:02:34 +02:00
} else {
2020-05-29 19:35:40 +02:00
int revisionWeWereAdded = GroupProtoUtil . findRevisionWeWereAdded ( latestServerGroup , selfUuid ) ;
int logsNeededFrom = localState ! = null ? Math . max ( localState . getRevision ( ) , revisionWeWereAdded ) : revisionWeWereAdded ;
2020-05-02 00:13:23 +02:00
history = getFullMemberHistory ( selfUuid , logsNeededFrom ) ;
}
return new GlobalGroupState ( localState , history ) ;
}
2020-07-08 23:10:58 +02:00
private List < ServerGroupLogEntry > getFullMemberHistory ( @NonNull UUID selfUuid , int logsNeededFromRevision ) throws IOException {
2020-05-02 00:13:23 +02:00
try {
2020-05-29 19:35:40 +02:00
Collection < DecryptedGroupHistoryEntry > groupStatesFromRevision = groupsV2Api . getGroupHistory ( groupSecretParams , logsNeededFromRevision , groupsV2Authorization . getAuthorizationForToday ( selfUuid , groupSecretParams ) ) ;
2020-07-08 23:10:58 +02:00
ArrayList < ServerGroupLogEntry > history = new ArrayList < > ( groupStatesFromRevision . size ( ) ) ;
boolean ignoreServerChanges = SignalStore . internalValues ( ) . gv2IgnoreServerChanges ( ) ;
if ( ignoreServerChanges ) {
Log . w ( TAG , "Server change logs are ignored by setting" ) ;
}
2020-05-02 00:13:23 +02:00
for ( DecryptedGroupHistoryEntry entry : groupStatesFromRevision ) {
2020-07-16 21:51:43 +02:00
DecryptedGroup group = entry . getGroup ( ) . orNull ( ) ;
DecryptedGroupChange change = ignoreServerChanges ? null : entry . getChange ( ) . orNull ( ) ;
if ( group ! = null | | change ! = null ) {
history . add ( new ServerGroupLogEntry ( group , change ) ) ;
}
2020-05-02 00:13:23 +02:00
}
return history ;
} catch ( InvalidGroupStateException | VerificationFailedException e ) {
throw new IOException ( e ) ;
}
}
private void storeMessage ( @NonNull DecryptedGroupV2Context decryptedGroupV2Context , long timestamp ) {
2020-05-21 22:04:57 +02:00
Optional < UUID > editor = getEditor ( decryptedGroupV2Context ) ;
2020-07-08 23:10:58 +02:00
boolean outgoing = ! editor . isPresent ( ) | | Recipient . self ( ) . requireUuid ( ) . equals ( editor . get ( ) ) ;
2020-05-13 18:36:57 +02:00
if ( outgoing ) {
try {
2020-08-24 22:40:47 +02:00
MessageDatabase mmsDatabase = DatabaseFactory . getMmsDatabase ( context ) ;
2020-05-13 18:36:57 +02:00
RecipientId recipientId = recipientDatabase . getOrInsertFromGroupId ( groupId ) ;
Recipient recipient = Recipient . resolved ( recipientId ) ;
2020-08-05 22:45:52 +02:00
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage ( recipient , decryptedGroupV2Context , null , timestamp , 0 , false , null , Collections . emptyList ( ) , Collections . emptyList ( ) , Collections . emptyList ( ) ) ;
2020-05-13 18:36:57 +02:00
long threadId = DatabaseFactory . getThreadDatabase ( context ) . getThreadIdFor ( recipient ) ;
long messageId = mmsDatabase . insertMessageOutbox ( outgoingMessage , threadId , false , null ) ;
mmsDatabase . markAsSent ( messageId , true ) ;
} catch ( MmsException e ) {
Log . w ( TAG , e ) ;
}
} else {
2020-08-20 22:50:14 +02:00
MessageDatabase smsDatabase = DatabaseFactory . getSmsDatabase ( context ) ;
2020-05-21 22:04:57 +02:00
RecipientId sender = RecipientId . from ( editor . get ( ) , null ) ;
2020-05-13 18:36:57 +02:00
IncomingTextMessage incoming = new IncomingTextMessage ( sender , - 1 , timestamp , timestamp , "" , Optional . of ( groupId ) , 0 , false ) ;
IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage ( incoming , decryptedGroupV2Context ) ;
2020-10-09 22:45:45 +02:00
if ( ! smsDatabase . insertMessageInbox ( groupMessage ) . isPresent ( ) ) {
Log . w ( TAG , "Could not insert update message" ) ;
}
2020-05-02 00:13:23 +02:00
}
}
2020-05-21 22:04:57 +02:00
private Optional < UUID > getEditor ( @NonNull DecryptedGroupV2Context decryptedGroupV2Context ) {
DecryptedGroupChange change = decryptedGroupV2Context . getChange ( ) ;
Optional < UUID > changeEditor = DecryptedGroupUtil . editorUuid ( change ) ;
if ( changeEditor . isPresent ( ) ) {
return changeEditor ;
} else {
Optional < DecryptedPendingMember > pendingByUuid = DecryptedGroupUtil . findPendingByUuid ( decryptedGroupV2Context . getGroupState ( ) . getPendingMembersList ( ) , Recipient . self ( ) . requireUuid ( ) ) ;
if ( pendingByUuid . isPresent ( ) ) {
return Optional . fromNullable ( UuidUtil . fromByteStringOrNull ( pendingByUuid . get ( ) . getAddedByUuid ( ) ) ) ;
}
}
return Optional . absent ( ) ;
}
2020-05-02 00:13:23 +02:00
}
}