Fix out-of-sync local state after rejoining a group via invite link.

fork-5.53.8
Cody Henthorne 2022-10-05 17:09:28 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 3895578d51
commit 26709177d2
13 zmienionych plików z 487 dodań i 76 usunięć

Wyświetl plik

@ -199,6 +199,7 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestio
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.invites.InviteReminderModel;
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
import org.thoughtcrime.securesms.jobs.ForceUpdateGroupV2Job;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
@ -594,6 +595,8 @@ public class ConversationParentFragment extends Fragment
.then(GroupV2UpdateSelfProfileKeyJob.withoutLimits(groupId))
.enqueue();
ForceUpdateGroupV2Job.enqueueIfNecessary(groupId);
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getChildFragmentManager(), groupId);
}

Wyświetl plik

@ -69,24 +69,25 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
private static final String TAG = Log.tag(GroupDatabase.class);
static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
public static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
private static final String AVATAR_RELAY = "avatar_relay";
private static final String AVATAR_DIGEST = "avatar_digest";
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
private static final String DISTRIBUTION_ID = "distribution_id";
private static final String SHOW_AS_STORY_STATE = "display_as_story";
static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
public static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
private static final String AVATAR_RELAY = "avatar_relay";
private static final String AVATAR_DIGEST = "avatar_digest";
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
private static final String DISTRIBUTION_ID = "distribution_id";
private static final String SHOW_AS_STORY_STATE = "display_as_story";
private static final String LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp";
/** Was temporarily used for PNP accept by pni but is no longer needed/updated */
@Deprecated
@ -101,27 +102,28 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
/** Serialized {@link DecryptedGroup} protobuf */
public static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
DISTRIBUTION_ID + " TEXT DEFAULT NULL, " +
SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " +
AUTH_SERVICE_ID + " TEXT DEFAULT NULL);";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " +
DISTRIBUTION_ID + " TEXT DEFAULT NULL, " +
SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " +
AUTH_SERVICE_ID + " TEXT DEFAULT NULL, " +
LAST_FORCE_UPDATE_TIMESTAMP + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
@ -132,7 +134,7 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP, LAST_FORCE_UPDATE_TIMESTAMP
};
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
@ -955,6 +957,12 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
}
public void setLastForceUpdateTimestamp(@NonNull GroupId groupId, long timestamp) {
ContentValues values = new ContentValues();
values.put(LAST_FORCE_UPDATE_TIMESTAMP, timestamp);
getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId));
}
@WorkerThread
public boolean isCurrentMember(@NonNull GroupId.Push groupId, @NonNull RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
@ -1114,7 +1122,8 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
CursorUtil.requireBlob(cursor, V2_MASTER_KEY),
CursorUtil.requireInt(cursor, V2_REVISION),
CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP),
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null));
CursorUtil.getString(cursor, DISTRIBUTION_ID).map(DistributionId::from).orElse(null),
CursorUtil.requireLong(cursor, LAST_FORCE_UPDATE_TIMESTAMP));
}
@Override
@ -1155,6 +1164,7 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
private final boolean mms;
@Nullable private final V2GroupProperties v2GroupProperties;
private final DistributionId distributionId;
private final long lastForceUpdateTimestamp;
public GroupRecord(@NonNull GroupId id,
@NonNull RecipientId recipientId,
@ -1171,19 +1181,21 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
@Nullable byte[] groupMasterKeyBytes,
int groupRevision,
@Nullable byte[] decryptedGroupBytes,
@Nullable DistributionId distributionId)
@Nullable DistributionId distributionId,
long lastForceUpdateTimestamp)
{
this.id = id;
this.recipientId = recipientId;
this.title = title;
this.avatarId = avatarId;
this.avatarKey = avatarKey;
this.avatarDigest = avatarDigest;
this.avatarContentType = avatarContentType;
this.relay = relay;
this.active = active;
this.mms = mms;
this.distributionId = distributionId;
this.id = id;
this.recipientId = recipientId;
this.title = title;
this.avatarId = avatarId;
this.avatarKey = avatarKey;
this.avatarDigest = avatarDigest;
this.avatarContentType = avatarContentType;
this.relay = relay;
this.active = active;
this.mms = mms;
this.distributionId = distributionId;
this.lastForceUpdateTimestamp = lastForceUpdateTimestamp;
V2GroupProperties v2GroupProperties = null;
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
@ -1292,6 +1304,10 @@ public class GroupDatabase extends Database implements RecipientIdDatabaseRefere
return distributionId;
}
public long getLastForceUpdateTimestamp() {
return lastForceUpdateTimestamp;
}
public boolean isV1Group() {
return !mms && !isV2Group();
}

Wyświetl plik

@ -12,13 +12,14 @@ import org.thoughtcrime.securesms.database.helpers.migration.V154_PniSignaturesM
import org.thoughtcrime.securesms.database.helpers.migration.V155_SmsExporterMigration
import org.thoughtcrime.securesms.database.helpers.migration.V156_RecipientUnregisteredTimestampMigration
import org.thoughtcrime.securesms.database.helpers.migration.V157_RecipeintHiddenMigration
import org.thoughtcrime.securesms.database.helpers.migration.V158_GroupsLastForceUpdateTimestampMigration
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
*/
object SignalDatabaseMigrations {
const val DATABASE_VERSION = 157
const val DATABASE_VERSION = 158
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -57,6 +58,10 @@ object SignalDatabaseMigrations {
if (oldVersion < 157) {
V157_RecipeintHiddenMigration.migrate(context, db, oldVersion, newVersion)
}
if (oldVersion < 158) {
V158_GroupsLastForceUpdateTimestampMigration.migrate(context, db, oldVersion, newVersion)
}
}
@JvmStatic

Wyświetl plik

@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
/**
* Track last time we did a forced sanity check for this group with the server.
*/
object V158_GroupsLastForceUpdateTimestampMigration : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE groups ADD COLUMN last_force_update_timestamp INTEGER DEFAULT 0")
}
}

Wyświetl plik

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.DatabaseId;
import org.signal.core.util.Hex;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.zkgroup.InvalidInputException;
@ -14,7 +15,7 @@ import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.security.SecureRandom;
public abstract class GroupId {
public abstract class GroupId implements DatabaseId {
private static final String ENCODED_SIGNAL_GROUP_V1_PREFIX = "__textsecure_group__!";
private static final String ENCODED_SIGNAL_GROUP_V2_PREFIX = "__signal_group__v2__!";
@ -173,6 +174,11 @@ public abstract class GroupId {
return encodedId;
}
@Override
public @NonNull String serialize() {
return encodedId;
}
public abstract boolean isMms();
public abstract boolean isV1();

Wyświetl plik

@ -188,6 +188,17 @@ public final class GroupManager {
}
}
@WorkerThread
public static void forceSanityUpdateFromServer(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey,
long timestamp)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
updater.forceSanityUpdateFromServer(timestamp);
}
}
@WorkerThread
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
@NonNull ServiceId authServiceId,

Wyświetl plik

@ -798,6 +798,14 @@ final class GroupManagerV2 {
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
}
@WorkerThread
void forceSanityUpdateFromServer(long timestamp)
throws IOException, GroupNotAMemberException
{
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
.forceSanityUpdateFromServer(timestamp);
}
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
if (signedGroupChange != null) {
GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
@ -928,24 +936,6 @@ final class GroupManagerV2 {
if (group.isPresent()) {
Log.i(TAG, "Group already present locally");
DecryptedGroup currentGroupState = group.get()
.requireV2GroupProperties()
.getDecryptedGroup();
DecryptedGroup updatedGroup = currentGroupState;
try {
if (decryptedChange != null) {
updatedGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(updatedGroup, decryptedChange);
}
updatedGroup = resetRevision(updatedGroup, currentGroupState.getRevision());
} catch (NotAbleToApplyGroupV2ChangeException e) {
Log.w(TAG, e);
updatedGroup = decryptedGroup;
}
groupDatabase.update(groupId, updatedGroup);
} else {
groupDatabase.create(groupMasterKey, decryptedGroup);
Log.i(TAG, "Created local group with placeholder");

Wyświetl plik

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
@ -170,6 +171,52 @@ public class GroupsV2StateProcessor {
this.profileAndMessageHelper = profileAndMessageHelper;
}
@WorkerThread
public GroupUpdateResult forceSanityUpdateFromServer(long timestamp)
throws IOException, GroupNotAMemberException
{
Optional<GroupRecord> localRecord = groupDatabase.getGroup(groupId);
DecryptedGroup localState = localRecord.map(g -> g.requireV2GroupProperties().getDecryptedGroup()).orElse(null);
DecryptedGroup serverState;
if (localState == null) {
info("No local state to force update");
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
}
try {
serverState = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
} catch (NotInGroupException | GroupNotFoundException e) {
throw new GroupNotAMemberException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new IOException(e);
}
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(localState, serverState);
GlobalGroupState inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(serverState, decryptedGroupChange)));
AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, serverState.getRevision());
DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState();
if (newLocalState == null || newLocalState == inputGroupState.getLocalState()) {
info("Local state and server state are equal");
return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null);
} else {
info("Local state (revision: " + localState.getRevision() + ") does not match server state (revision: " + serverState.getRevision() + "), updating");
}
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
if (localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
info("Inserting single update message for restore placeholder");
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
} else {
info("Inserting force update messages");
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
}
profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState);
return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState);
}
/**
* Using network where required, will attempt to bring the local copy of the group up to the revision specified.
*
@ -560,10 +607,12 @@ public class GroupsV2StateProcessor {
private final Context context;
private final ServiceId serviceId;
private final GroupMasterKey masterKey;
private final GroupId.V2 groupId;
private final RecipientDatabase recipientDatabase;
@VisibleForTesting
GroupMasterKey masterKey;
ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) {
this.context = context;
this.serviceId = serviceId;

Wyświetl plik

@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Schedules a {@link ForceUpdateGroupV2WorkerJob} to happen after message queues are drained.
*/
public final class ForceUpdateGroupV2Job extends BaseJob {
public static final String KEY = "ForceUpdateGroupV2Job";
private static final long FORCE_UPDATE_INTERVAL = TimeUnit.DAYS.toMillis(7);
private static final String KEY_GROUP_ID = "group_id";
private final GroupId.V2 groupId;
public static void enqueueIfNecessary(@NonNull GroupId.V2 groupId) {
SignalExecutors.BOUNDED.execute(() -> {
Optional<GroupDatabase.GroupRecord> group = SignalDatabase.groups().getGroup(groupId);
if (group.isPresent() &&
group.get().isV2Group() &&
group.get().getLastForceUpdateTimestamp() + FORCE_UPDATE_INTERVAL < System.currentTimeMillis()
) {
ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2Job(groupId));
}
});
}
private ForceUpdateGroupV2Job(@NonNull GroupId.V2 groupId) {
this(new Parameters.Builder().setQueue("ForceUpdateGroupV2Job_" + groupId)
.setMaxInstancesForQueue(1)
.addConstraint(DecryptionsDrainedConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
groupId);
}
private ForceUpdateGroupV2Job(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) {
super(parameters);
this.groupId = groupId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString()).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() {
ApplicationDependencies.getJobManager().add(new ForceUpdateGroupV2WorkerJob(groupId));
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return false;
}
@Override
public void onFailure() {
}
public static final class Factory implements Job.Factory<ForceUpdateGroupV2Job> {
@Override
public @NonNull ForceUpdateGroupV2Job create(@NonNull Parameters parameters, @NonNull Data data) {
return new ForceUpdateGroupV2Job(parameters, GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2());
}
}
}

Wyświetl plik

@ -0,0 +1,103 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Scheduled by {@link ForceUpdateGroupV2Job} after message queues are drained.
*
* Forces a sanity check between local state and server state, and updates local state
* as necessary.
*/
final class ForceUpdateGroupV2WorkerJob extends BaseJob {
public static final String KEY = "ForceUpdateGroupV2WorkerJob";
private static final String TAG = Log.tag(ForceUpdateGroupV2WorkerJob.class);
private static final String KEY_GROUP_ID = "group_id";
private final GroupId.V2 groupId;
ForceUpdateGroupV2WorkerJob(@NonNull GroupId.V2 groupId) {
this(new Parameters.Builder().setQueue(PushProcessMessageJob.getQueueName(Recipient.externalGroupExact(groupId).getId()))
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
groupId);
}
private ForceUpdateGroupV2WorkerJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) {
super(parameters);
this.groupId = groupId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString())
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException {
Optional<GroupDatabase.GroupRecord> group = SignalDatabase.groups().getGroup(groupId);
if (!group.isPresent()) {
Log.w(TAG, "Group not found");
return;
}
if (Recipient.externalGroupExact(groupId).isBlocked()) {
Log.i(TAG, "Not fetching group info for blocked group " + groupId);
return;
}
GroupManager.forceSanityUpdateFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), System.currentTimeMillis());
SignalDatabase.groups().setLastForceUpdateTimestamp(group.get().getId(), System.currentTimeMillis());
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException ||
e instanceof NoCredentialForRedemptionTimeException ||
e instanceof GroupChangeBusyException;
}
@Override
public void onFailure() {
}
public static final class Factory implements Job.Factory<ForceUpdateGroupV2WorkerJob> {
@Override
public @NonNull ForceUpdateGroupV2WorkerJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new ForceUpdateGroupV2WorkerJob(parameters,
GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2());
}
}
}

Wyświetl plik

@ -97,6 +97,8 @@ public final class JobManagerFactories {
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
put(FetchRemoteMegaphoneImageJob.KEY, new FetchRemoteMegaphoneImageJob.Factory());
put(FontDownloaderJob.KEY, new FontDownloaderJob.Factory());
put(ForceUpdateGroupV2Job.KEY, new ForceUpdateGroupV2Job.Factory());
put(ForceUpdateGroupV2WorkerJob.KEY, new ForceUpdateGroupV2WorkerJob.Factory());
put(GiftSendJob.KEY, new GiftSendJob.Factory());
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
@ -136,7 +138,7 @@ public final class JobManagerFactories {
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
put(PaymentSendJob.KEY, new PaymentSendJob.Factory());
put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory());
put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory());
put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory());
put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory());
put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory());
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());

Wyświetl plik

@ -190,7 +190,8 @@ fun groupRecord(
masterKey.serialize(),
decryptedGroup.revision,
decryptedGroup.toByteArray(),
distributionId
distributionId,
System.currentTimeMillis()
)
)
}

Wyświetl plik

@ -3,19 +3,28 @@ package org.thoughtcrime.securesms.groups.v2.processing
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.both
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.hasProperty
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.notNullValue
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.isA
import org.mockito.Mockito.mock
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doNothing
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.signal.core.util.Hex.fromStringCondensed
@ -25,11 +34,13 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.storageservice.protos.groups.local.DecryptedString
import org.signal.storageservice.protos.groups.local.DecryptedTimer
import org.thoughtcrime.securesms.SignalStoreRule
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.GroupStateTestData
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.member
import org.thoughtcrime.securesms.database.model.databaseprotos.requestingMember
import org.thoughtcrime.securesms.database.setNewDescription
@ -104,6 +115,7 @@ class GroupsV2StateProcessorTest {
}
}
doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any())
doReturn(serverState).`when`(groupsV2API).getGroup(any(), any())
}
data.changeSet?.let { changeSet ->
@ -453,4 +465,118 @@ class GroupsV2StateProcessorTest {
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
assertThat("revision matches latest revision on server", result.latestServer!!.revision, `is`(101))
}
/**
* If for some reason we missed a member being added in our local state, and then we preform a multi-revision update,
* we should now know about the member and add update messages to the chat.
*/
@Test
fun missedMemberAddResolvesWithMultipleRevisionUpdate() {
val secondOther = member(ServiceId.from(UUID.randomUUID()))
val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java)
profileAndMessageHelper.masterKey = masterKey
doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any())
doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong())
given {
localState(
revision = 8,
title = "Whatever",
members = selfAndOthers
)
serverState(
revision = 10,
title = "Changed",
members = selfAndOthers + secondOther
)
changeSet {
changeLog(9) {
change {
setNewTitle("Mid-Change")
}
fullSnapshot(
title = "Mid-Change",
members = selfAndOthers + secondOther
)
}
changeLog(10) {
change {
setNewTitle("Changed")
}
}
}
apiCallParameters(requestedRevision = 8, includeFirst = true)
}
val result = processor.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, 0, null)
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther))
val allUpdateMessageContexts = updateMessageContextCapture.allValues
assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther)))
}
/**
* If for some reason we missed a member being added in our local state, and then we preform a forced sanity update,
* we should now know about the member and any other changes, and add update messages to the chat.
*/
@Test
fun missedMemberAddResolvesWithForcedUpdate() {
val secondOther = member(ServiceId.from(UUID.randomUUID()))
val updateMessageContextCapture = ArgumentCaptor.forClass(DecryptedGroupV2Context::class.java)
profileAndMessageHelper.masterKey = masterKey
doCallRealMethod().`when`(profileAndMessageHelper).insertUpdateMessages(anyLong(), anyOrNull(), any())
doNothing().`when`(profileAndMessageHelper).storeMessage(updateMessageContextCapture.capture(), anyLong())
given {
localState(
revision = 10,
title = "Title",
members = selfAndOthers
)
serverState(
revision = 10,
title = "Changed",
members = selfAndOthers + secondOther
)
}
val result = processor.forceSanityUpdateFromServer(0)
assertThat("local should update to server", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_UPDATED))
assertThat("members contains second other", result.latestServer!!.membersList, hasItem(secondOther))
assertThat("title should be updated", result.latestServer!!.title, `is`("Changed"))
val allUpdateMessageContexts = updateMessageContextCapture.allValues
assertThat("group update messages contains new member add", allUpdateMessageContexts.map { it.change.newMembersList }, hasItem(hasItem(secondOther)))
assertThat(
"group update messages contains title change",
allUpdateMessageContexts.map { it.change.newTitle },
hasItem(both<DecryptedString>(notNullValue()).and(hasProperty("value", `is`("Changed"))))
)
}
/**
* If we preform a forced sanity update, with no differences between local and server, then it should be no-op.
*/
@Test
fun noDifferencesNoOpsWithForcedUpdate() {
given {
localState(
revision = 10,
title = "Title",
members = selfAndOthers
)
serverState(
revision = 10,
title = "Title",
members = selfAndOthers
)
}
val result = processor.forceSanityUpdateFromServer(0)
assertThat("local should be unchanged", result.groupState, `is`(GroupsV2StateProcessor.GroupState.GROUP_CONSISTENT_OR_AHEAD))
}
}