diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index ed48a59fdd..f7e7a51240 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -590,7 +590,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter .setTitle("Clear all profile keys?") .setMessage("Are you sure? Never do this on a non-test device.") .setPositiveButton(android.R.string.ok) { _, _ -> - SignalDatabase.recipients.debugClearServiceIds() + SignalDatabase.recipients.debugClearProfileData() Toast.makeText(context, "Cleared all profile keys.", Toast.LENGTH_SHORT).show() } .setNegativeButton(android.R.string.cancel) { d, _ -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt index a06fe057c0..c93c6b7cf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.Hex import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient @@ -169,6 +171,28 @@ class InternalConversationSettingsFragment : DSLSettingsFragment( ) } + clickPref( + title = DSLSettingsText.from("Clear recipient data"), + summary = DSLSettingsText.from("Clears service id, profile data, sessions, identities, and thread."), + onClick = { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Are you sure?") + .setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() } + .setPositiveButton(android.R.string.ok) { _, _ -> + if (recipient.hasServiceId()) { + SignalDatabase.recipients.debugClearServiceIds(recipient.id) + SignalDatabase.recipients.debugClearProfileData(recipient.id) + SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString()) + ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireServiceId().toString()) + ApplicationDependencies.getProtocolStore().pni().identities().delete(recipient.requireServiceId().toString()) + SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id)) + } + startActivity(MainActivity.clearTop(requireContext())) + } + .show() + } + ) + if (recipient.isSelf) { sectionHeaderPref(DSLSettingsText.from("Donations")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index 016387fc43..642392e387 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.util.Stopwatch import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.services.CdshV2Service import java.io.IOException -import java.lang.NumberFormatException import java.util.Optional /** @@ -90,7 +89,7 @@ object ContactDiscoveryRefreshV2 { val inputIds: Set = recipients.map { it.id }.toSet() val inputE164s: Set = recipients.mapNotNull { it.e164.orElse(null) }.toSet() - if (recipients.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) { + if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) { Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.") val fullResult: ContactDiscovery.RefreshResult = refreshAll(context) @@ -100,7 +99,12 @@ object ContactDiscoveryRefreshV2 { ) } - Log.i(TAG, "Doing a one-off request for ${recipients.size} recipients.") + if (inputE164s.isEmpty()) { + Log.w(TAG, "No numbers to refresh!") + return ContactDiscovery.RefreshResult(emptySet(), emptyMap()) + } else { + Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.") + } val response: CdshV2Service.Response = makeRequest( previousE164s = emptySet(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 0d9e7faaf9..e771e2bf71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -13,7 +13,9 @@ import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; import com.google.protobuf.InvalidProtocolBufferException; +import org.signal.core.util.CursorUtil; import org.signal.core.util.SetUtil; +import org.signal.core.util.SqlUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; @@ -33,8 +35,6 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.signal.core.util.CursorUtil; -import org.signal.core.util.SqlUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; @@ -83,6 +83,7 @@ public class GroupDatabase extends Database { private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members"; private static final String DISTRIBUTION_ID = "distribution_id"; private static final String DISPLAY_AS_STORY = "display_as_story"; + private static final String AUTH_SERVICE_ID = "auth_service_id"; /* V2 Group columns */ @@ -112,18 +113,19 @@ public class GroupDatabase extends Database { EXPECTED_V2_ID + " TEXT DEFAULT NULL, " + UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " + DISTRIBUTION_ID + " TEXT DEFAULT NULL, " + - DISPLAY_AS_STORY + " INTEGER DEFAULT 0);"; + DISPLAY_AS_STORY + " INTEGER DEFAULT 0, " + + AUTH_SERVICE_ID + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", "CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");", "CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");", "CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON " + TABLE_NAME + "(" + DISTRIBUTION_ID + ");" -}; + }; -private static final String[] GROUP_PROJECTION = { + 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, AUTH_SERVICE_ID }; static final List TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList(); @@ -459,23 +461,25 @@ private static final String[] GROUP_PROJECTION = { if (groupExists(groupId.deriveV2MigrationGroupId())) { throw new LegacyGroupInsertException(groupId); } - create(groupId, title, members, avatar, relay, null, null); + create(null, groupId, title, members, avatar, relay, null, null); } public void create(@NonNull GroupId.Mms groupId, @Nullable String title, @NonNull Collection members) { - create(groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null); + create(null, groupId, Util.isEmpty(title) ? null : title, members, null, null, null, null); } - public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey, + public GroupId.V2 create(@Nullable ServiceId authServiceId, + @NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup groupState) { - return create(groupMasterKey, groupState, false); + return create(authServiceId, groupMasterKey, groupState, false); } - public GroupId.V2 create(@NonNull GroupMasterKey groupMasterKey, + public GroupId.V2 create(@Nullable ServiceId authServiceId, + @NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup groupState, boolean force) { @@ -487,7 +491,7 @@ private static final String[] GROUP_PROJECTION = { Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!"); } - create(groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState); + create(authServiceId, groupId, groupState.getTitle(), Collections.emptyList(), null, null, groupMasterKey, groupState); return groupId; } @@ -496,7 +500,7 @@ private static final String[] GROUP_PROJECTION = { * There was a point in time where we weren't properly responding to group creates on linked devices. This would result in us having a Recipient entry for the * group, but we'd either be missing the group entry, or that entry would be missing a master key. This method fixes this scenario. */ - public void fixMissingMasterKey(@NonNull GroupMasterKey groupMasterKey) { + public void fixMissingMasterKey(@Nullable ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) { GroupId.V2 groupId = GroupId.v2(groupMasterKey); if (getGroupV1ByExpectedV2(groupId).isPresent()) { @@ -517,7 +521,8 @@ private static final String[] GROUP_PROJECTION = { if (updated < 1) { Log.w(TAG, "No group entry. Creating restore placeholder for " + groupId); - create(groupMasterKey, + create(authServiceId, + groupMasterKey, DecryptedGroup.newBuilder() .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) .build(), @@ -538,7 +543,8 @@ private static final String[] GROUP_PROJECTION = { /** * @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version). */ - private void create(@NonNull GroupId groupId, + private void create(@Nullable ServiceId authServiceId, + @NonNull GroupId groupId, @Nullable String title, @NonNull Collection memberCollection, @Nullable SignalServiceAttachmentPointer avatar, @@ -553,6 +559,7 @@ private static final String[] GROUP_PROJECTION = { Collections.sort(members); ContentValues contentValues = new ContentValues(); + contentValues.put(AUTH_SERVICE_ID, authServiceId != null ? authServiceId.toString() : null); contentValues.put(RECIPIENT_ID, groupRecipientId.serialize()); contentValues.put(GROUP_ID, groupId.toString()); contentValues.put(TITLE, title); @@ -964,6 +971,13 @@ private static final String[] GROUP_PROJECTION = { return result; } + public void setAuthServiceId(@Nullable ServiceId authServiceId, @NonNull GroupId groupId) { + ContentValues values = new ContentValues(1); + values.put(AUTH_SERVICE_ID, authServiceId == null ? null : authServiceId.toString()); + + getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId)); + } + public static class Reader implements Closeable { public final Cursor cursor; @@ -1008,7 +1022,8 @@ private static final String[] GROUP_PROJECTION = { 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.requireString(cursor, AUTH_SERVICE_ID)); } @Override @@ -1034,6 +1049,7 @@ private static final String[] GROUP_PROJECTION = { private final boolean mms; @Nullable private final V2GroupProperties v2GroupProperties; private final DistributionId distributionId; + @Nullable private final String authServiceId; public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, @@ -1050,7 +1066,8 @@ private static final String[] GROUP_PROJECTION = { @Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes, - @Nullable DistributionId distributionId) + @Nullable DistributionId distributionId, + @Nullable String authServiceId) { this.id = id; this.recipientId = recipientId; @@ -1063,6 +1080,7 @@ private static final String[] GROUP_PROJECTION = { this.active = active; this.mms = mms; this.distributionId = distributionId; + this.authServiceId = authServiceId; V2GroupProperties v2GroupProperties = null; if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { @@ -1193,7 +1211,11 @@ private static final String[] GROUP_PROJECTION = { public MemberLevel memberLevel(@NonNull Recipient recipient) { if (isV2Group()) { - return requireV2GroupProperties().memberLevel(recipient); + MemberLevel memberLevel = requireV2GroupProperties().memberLevel(recipient.getServiceId()); + if (recipient.isSelf() && memberLevel == MemberLevel.NOT_A_MEMBER) { + memberLevel = requireV2GroupProperties().memberLevel(Optional.ofNullable(SignalStore.account().getPni())); + } + return memberLevel; } else if (isMms() && recipient.isSelf()) { return MemberLevel.FULL_MEMBER; } else { @@ -1247,6 +1269,10 @@ private static final String[] GROUP_PROJECTION = { } return false; } + + public @Nullable ServiceId getAuthServiceId() { + return ServiceId.parseOrNull(authServiceId); + } } public static class V2GroupProperties { @@ -1297,9 +1323,7 @@ private static final String[] GROUP_PROJECTION = { return members.stream().filter(this::isAdmin).collect(Collectors.toList()); } - public MemberLevel memberLevel(@NonNull Recipient recipient) { - Optional serviceId = recipient.getServiceId(); - + public MemberLevel memberLevel(@NonNull Optional serviceId) { if (!serviceId.isPresent()) { return MemberLevel.NOT_A_MEMBER; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 1d5b0ca4c8..48c7caf30c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -953,6 +953,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Log.i(TAG, "Creating restore placeholder for $groupId") groups.create( + null, masterKey, DecryptedGroup.newBuilder() .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) @@ -1597,9 +1598,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : return updated } - private fun clearProfileKeyCredential(id: RecipientId) { + fun clearProfileKeyCredential(id: RecipientId) { val values = ContentValues(1) values.putNull(PROFILE_KEY_CREDENTIAL) + values.putNull(PROFILE_KEY) + values.put(PROFILE_SHARING, 0) if (update(id, values)) { rotateStorageId(id) ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) @@ -2938,28 +2941,45 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : /** * Should only be used for debugging! A very destructive action that clears all known serviceIds. */ - fun debugClearServiceIds() { + fun debugClearServiceIds(recipientId: RecipientId? = null) { writableDatabase .update(TABLE_NAME) .values( SERVICE_ID to null, PNI_COLUMN to null ) - .where("$ID != ?", Recipient.self().id) + .run { + if (recipientId == null) { + where("$ID != ?", Recipient.self().id) + } else { + where("$ID = ?", recipientId) + } + } .run() } /** * Should only be used for debugging! A very destructive action that clears all known profile keys and credentials. */ - fun debugClearProfileKeys() { + fun debugClearProfileData(recipientId: RecipientId? = null) { writableDatabase .update(TABLE_NAME) .values( PROFILE_KEY to null, - PROFILE_KEY_CREDENTIAL to null + PROFILE_KEY_CREDENTIAL to null, + PROFILE_GIVEN_NAME to null, + PROFILE_FAMILY_NAME to null, + PROFILE_JOINED_NAME to null, + LAST_PROFILE_FETCH to 0, + SIGNAL_PROFILE_AVATAR to null ) - .where("$ID != ?", Recipient.self().id) + .run { + if (recipientId == null) { + where("$ID != ?", Recipient.self().id) + } else { + where("$ID = ?", recipientId) + } + } .run() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 79b2164980..ec3b0c8132 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -194,8 +194,9 @@ object SignalDatabaseMigrations { private const val CLEAN_DELETED_DISTRIBUTION_LISTS = 138 private const val REMOVE_KNOWN_UNKNOWNS = 139 private const val CDS_V2 = 140 + private const val GROUP_SERVICE_ID = 141 - const val DATABASE_VERSION = 140 + const val DATABASE_VERSION = 141 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2523,6 +2524,10 @@ object SignalDatabaseMigrations { """ ) } + + if (oldVersion < GROUP_SERVICE_ID) { + db.execSQL("ALTER TABLE groups ADD COLUMN auth_service_id TEXT DEFAULT NULL") + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index 91473d20ce..7988bbc3fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -1,12 +1,8 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; -import android.graphics.Color; -import android.text.PrecomputedText; import android.text.Spannable; -import android.text.SpannableString; import android.text.SpannableStringBuilder; -import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -17,6 +13,7 @@ import androidx.annotation.VisibleForTesting; import com.google.protobuf.ByteString; +import org.signal.core.util.StringUtil; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedApproveMember; @@ -30,16 +27,15 @@ import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.EnabledState; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; +import org.thoughtcrime.securesms.keyvalue.ServiceIds; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.SpanUtil; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -55,15 +51,13 @@ import java.util.stream.Collectors; final class GroupsV2UpdateMessageProducer { - @NonNull private final Context context; - @NonNull private final UUID selfUuid; - @NonNull private final ByteString selfUuidBytes; + @NonNull private final Context context; + @NonNull private final ServiceIds selfIds; @Nullable private final Consumer recipientClickHandler; - GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull UUID selfUuid, @Nullable Consumer recipientClickHandler) { + GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull ServiceIds selfIds, @Nullable Consumer recipientClickHandler) { this.context = context; - this.selfUuid = selfUuid; - this.selfUuidBytes = UuidUtil.toByteString(selfUuid); + this.selfIds = selfIds; this.recipientClickHandler = recipientClickHandler; } @@ -77,21 +71,25 @@ final class GroupsV2UpdateMessageProducer { * When the revision of the group is 0, the change is very noisy and only the editor is useful. */ UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) { - Optional selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid); + Optional selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfIds.getAci().uuid()); + if (!selfPending.isPresent()) { + selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfIds.getPni().uuid()); + } + if (selfPending.isPresent()) { return updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, selfPending.get().getAddedByUuid(), R.drawable.ic_update_group_add_16); } ByteString foundingMemberUuid = decryptedGroupChange.getEditor(); if (!foundingMemberUuid.isEmpty()) { - if (selfUuidBytes.equals(foundingMemberUuid)) { + if (selfIds.matches(foundingMemberUuid)) { return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_16); } else { return updateDescription(R.string.MessageRecord_s_added_you, foundingMemberUuid, R.drawable.ic_update_group_add_16); } } - if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfUuid).isPresent()) { + if (DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getAci().uuid()).isPresent() || DecryptedGroupUtil.findMemberByUuid(group.getMembersList(), selfIds.getPni().uuid()).isPresent()) { return updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16); } else { return updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16); @@ -163,7 +161,7 @@ final class GroupsV2UpdateMessageProducer { * Handles case of future protocol versions where we don't know what has changed. */ private void describeUnknownChange(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (editorIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16)); @@ -177,10 +175,10 @@ final class GroupsV2UpdateMessageProducer { } private void describeMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); for (DecryptedMember member : change.getNewMembersList()) { - boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(member.getUuid()); if (editorIsYou) { if (newMemberIsYou) { @@ -204,7 +202,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorMemberAdditions(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedMember member : change.getNewMembersList()) { - boolean newMemberIsYou = member.getUuid().equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(member.getUuid()); if (newMemberIsYou) { updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); @@ -215,10 +213,10 @@ final class GroupsV2UpdateMessageProducer { } private void describeMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); for (ByteString member : change.getDeleteMembersList()) { - boolean removedMemberIsYou = member.equals(selfUuidBytes); + boolean removedMemberIsYou = selfIds.matches(member); if (editorIsYou) { if (removedMemberIsYou) { @@ -242,7 +240,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorMemberRemovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (ByteString member : change.getDeleteMembersList()) { - boolean removedMemberIsYou = member.equals(selfUuidBytes); + boolean removedMemberIsYou = selfIds.matches(member); if (removedMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_16)); @@ -253,10 +251,10 @@ final class GroupsV2UpdateMessageProducer { } private void describeModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) { - boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes); + boolean changedMemberIsYou = selfIds.matches(roleChange.getUuid()); if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (editorIsYou) { updates.add(updateDescription(R.string.MessageRecord_you_made_s_an_admin, roleChange.getUuid(), R.drawable.ic_update_group_role_16)); @@ -284,7 +282,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorModifyMemberRoles(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedModifyMemberRole roleChange : change.getModifyMemberRolesList()) { - boolean changedMemberIsYou = roleChange.getUuid().equals(selfUuidBytes); + boolean changedMemberIsYou = selfIds.matches(roleChange.getUuid()); if (roleChange.getRole() == Member.Role.ADMINISTRATOR) { if (changedMemberIsYou) { @@ -303,11 +301,11 @@ final class GroupsV2UpdateMessageProducer { } private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); int notYouInviteCount = 0; for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { - boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(invitee.getUuid()); if (newMemberIsYou) { updates.add(0, updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, change.getEditor(), R.drawable.ic_update_group_add_16)); @@ -321,8 +319,7 @@ final class GroupsV2UpdateMessageProducer { } if (notYouInviteCount > 0) { - final int notYouInviteCountFinalCopy = notYouInviteCount; - updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, notYouInviteCountFinalCopy, change.getEditor(), notYouInviteCountFinalCopy, R.drawable.ic_update_group_add_16)); + updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, notYouInviteCount, change.getEditor(), notYouInviteCount, R.drawable.ic_update_group_add_16)); } } @@ -330,7 +327,7 @@ final class GroupsV2UpdateMessageProducer { int notYouInviteCount = 0; for (DecryptedPendingMember invitee : change.getNewPendingMembersList()) { - boolean newMemberIsYou = invitee.getUuid().equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(invitee.getUuid()); if (newMemberIsYou) { UUID uuid = UuidUtil.fromByteStringOrUnknown(invitee.getAddedByUuid()); @@ -351,7 +348,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeRevokedInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); int notDeclineCount = 0; for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { @@ -362,7 +359,7 @@ final class GroupsV2UpdateMessageProducer { } else { updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); } - } else if (invitee.getUuid().equals(selfUuidBytes)) { + } else if (selfIds.matches(invitee.getUuid())) { updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, change.getEditor(), R.drawable.ic_update_group_decline_16)); } else { notDeclineCount++; @@ -373,8 +370,7 @@ final class GroupsV2UpdateMessageProducer { if (editorIsYou) { updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notDeclineCount, notDeclineCount), R.drawable.ic_update_group_decline_16)); } else { - final int notDeclineCountFinalCopy = notDeclineCount; - updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notDeclineCountFinalCopy, change.getEditor(), notDeclineCountFinalCopy, R.drawable.ic_update_group_decline_16)); + updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notDeclineCount, change.getEditor(), notDeclineCount, R.drawable.ic_update_group_decline_16)); } } } @@ -383,7 +379,7 @@ final class GroupsV2UpdateMessageProducer { int notDeclineCount = 0; for (DecryptedPendingMemberRemoval invitee : change.getDeletePendingMembersList()) { - boolean inviteeWasYou = invitee.getUuid().equals(selfUuidBytes); + boolean inviteeWasYou = selfIds.matches(invitee.getUuid()); if (inviteeWasYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); @@ -398,11 +394,11 @@ final class GroupsV2UpdateMessageProducer { } private void describePromotePending(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); for (DecryptedMember newMember : change.getPromotePendingMembersList()) { ByteString uuid = newMember.getUuid(); - boolean newMemberIsYou = uuid.equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(uuid); if (editorIsYou) { if (newMemberIsYou) { @@ -427,7 +423,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorPromotePending(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedMember newMember : change.getPromotePendingMembersList()) { ByteString uuid = newMember.getUuid(); - boolean newMemberIsYou = uuid.equals(selfUuidBytes); + boolean newMemberIsYou = selfIds.matches(uuid); if (newMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); @@ -438,7 +434,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeNewTitle(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.hasNewTitle()) { String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue()); @@ -451,7 +447,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeNewDescription(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.hasNewDescription()) { if (editorIsYou) { @@ -475,7 +471,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.hasNewAvatar()) { if (editorIsYou) { @@ -493,7 +489,7 @@ final class GroupsV2UpdateMessageProducer { } void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.hasNewTimer()) { String time = ExpirationUtil.getExpirationDisplayValue(context, change.getNewTimer().getDuration()); @@ -513,7 +509,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeNewAttributeAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.getNewAttributeAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewAttributeAccess()); @@ -533,7 +529,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeNewMembershipAccess(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.getNewMemberAccess() != AccessControl.AccessRequired.UNKNOWN) { String accessLevel = GV2AccessLevelUtil.toString(context, change.getNewMemberAccess()); @@ -562,7 +558,7 @@ final class GroupsV2UpdateMessageProducer { previousAccessControl = previousGroupState.getAccessControl().getAddFromInviteLink(); } - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); boolean groupLinkEnabled = false; switch (change.getNewInviteLinkAccess()) { @@ -655,7 +651,7 @@ final class GroupsV2UpdateMessageProducer { Set deleteRequestingUuids = new HashSet<>(change.getDeleteRequestingMembersList()); for (DecryptedRequestingMember member : change.getNewRequestingMembersList()) { - boolean requestingMemberIsYou = member.getUuid().equals(selfUuidBytes); + boolean requestingMemberIsYou = selfIds.matches(member.getUuid()); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_16)); @@ -675,12 +671,12 @@ final class GroupsV2UpdateMessageProducer { private void describeRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) { - boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes); + boolean requestingMemberIsYou = selfIds.matches(requestingMember.getUuid()); if (requestingMemberIsYou) { updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, change.getEditor(), R.drawable.ic_update_group_accept_16)); } else { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (editorIsYou) { updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, requestingMember.getUuid(), R.drawable.ic_update_group_accept_16)); @@ -693,7 +689,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorRequestingMembersApprovals(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (DecryptedApproveMember requestingMember : change.getPromoteRequestingMembersList()) { - boolean requestingMemberIsYou = requestingMember.getUuid().equals(selfUuidBytes); + boolean requestingMemberIsYou = selfIds.matches(requestingMember.getUuid()); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_16)); @@ -704,16 +700,16 @@ final class GroupsV2UpdateMessageProducer { } private void describeRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List updates) { - Set newRequestingUuids = change.getNewRequestingMembersList().stream().map(r -> r.getUuid()).collect(Collectors.toSet()); + Set newRequestingUuids = change.getNewRequestingMembersList().stream().map(DecryptedRequestingMember::getUuid).collect(Collectors.toSet()); - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); for (ByteString requestingMember : change.getDeleteRequestingMembersList()) { if (newRequestingUuids.contains(requestingMember)) { continue; } - boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes); + boolean requestingMemberIsYou = selfIds.matches(requestingMember); if (requestingMemberIsYou) { if (editorIsYou) { @@ -735,7 +731,7 @@ final class GroupsV2UpdateMessageProducer { private void describeUnknownEditorRequestingMembersDeletes(@NonNull DecryptedGroupChange change, @NonNull List updates) { for (ByteString requestingMember : change.getDeleteRequestingMembersList()) { - boolean requestingMemberIsYou = requestingMember.equals(selfUuidBytes); + boolean requestingMemberIsYou = selfIds.matches(requestingMember); if (requestingMemberIsYou) { updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_16)); @@ -746,7 +742,7 @@ final class GroupsV2UpdateMessageProducer { } private void describeAnnouncementGroupChange(@NonNull DecryptedGroupChange change, @NonNull List updates) { - boolean editorIsYou = change.getEditor().equals(selfUuidBytes); + boolean editorIsYou = selfIds.matches(change.getEditor()); if (change.getNewIsAnnouncementGroup() == EnabledState.ENABLED) { if (editorIsYou) { @@ -858,8 +854,7 @@ final class GroupsV2UpdateMessageProducer { } private static @NonNull Object[] makePlaceholders(@NonNull List recipientIds, @Nullable List formatArgs) { - List placeholders = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList()); - List args = new LinkedList<>(placeholders); + List args = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList()); if (formatArgs != null) { args.addAll(formatArgs); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index f172508216..8854633a2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -33,6 +33,7 @@ import androidx.core.content.ContextCompat; import com.annimon.stream.Stream; import com.google.protobuf.ByteString; +import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; @@ -58,7 +59,6 @@ import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.GroupUtil; -import org.signal.core.util.StringUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; @@ -270,7 +270,7 @@ public abstract class MessageRecord extends DisplayRecord { try { byte[] decoded = Base64.decode(body); DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded); - GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().requireAci().uuid(), recipientClickHandler); + GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().requireServiceIds(), recipientClickHandler); if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) { return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 8f3fed31be..2fe13c63e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -6,6 +6,7 @@ import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import org.signal.core.util.Hex; import org.signal.core.util.concurrent.DeadlockDetector; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.thoughtcrime.securesms.KbsEnclave; @@ -38,7 +39,6 @@ import org.thoughtcrime.securesms.shakereport.ShakeToReport; import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.EarlyMessageCache; import org.thoughtcrime.securesms.util.FrameRateTracker; -import org.signal.core.util.Hex; import org.thoughtcrime.securesms.util.IasKeyStore; import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache; import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool; @@ -152,8 +152,10 @@ public class ApplicationDependencies { if (groupsV2Authorization == null) { synchronized (LOCK) { if (groupsV2Authorization == null) { - GroupsV2Authorization.ValueCache authCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AuthorizationCache()); - groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), authCache); + GroupsV2Authorization.ValueCache aciAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2AciAuthorizationCache()); + GroupsV2Authorization.ValueCache pniAuthCache = new GroupsV2AuthorizationMemoryValueCache(SignalStore.groupsV2PniAuthorizationCache()); + + groupsV2Authorization = new GroupsV2Authorization(getSignalServiceAccountManager().getGroupsV2Api(), aciAuthCache, pniAuthCache); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index e5ef6efdd8..88dea3d009 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -6,8 +6,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import com.google.protobuf.ByteString; - import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; @@ -24,7 +22,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.api.push.ServiceId; import java.io.IOException; import java.util.Collection; @@ -40,7 +38,8 @@ public final class GroupManager { private static final String TAG = Log.tag(GroupManager.class); @WorkerThread - public static @NonNull GroupActionResult createGroup(@NonNull Context context, + public static @NonNull GroupActionResult createGroup(@NonNull ServiceId authServiceId, + @NonNull Context context, @NonNull Set members, @Nullable byte[] avatar, @Nullable String name, @@ -54,7 +53,7 @@ public final class GroupManager { if (shouldAttemptToCreateV2) { try { try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) { - return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer); + return groupCreator.createGroup(authServiceId, memberIds, name, avatar, disappearingMessagesTimer); } } catch (MembershipNotSuitableForV2Exception e) { Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e); @@ -178,6 +177,7 @@ public final class GroupManager { */ @WorkerThread public static void updateGroupFromServer(@NonNull Context context, + @NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey, int revision, long timestamp, @@ -185,17 +185,18 @@ public final class GroupManager { throws GroupChangeBusyException, IOException, GroupNotAMemberException { try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) { - updater.updateLocalToServerRevision(revision, timestamp, signedGroupChange); + updater.updateLocalToServerRevision(authServiceId, revision, timestamp, signedGroupChange); } } @WorkerThread public static V2GroupServerStatus v2GroupStatus(@NonNull Context context, + @NonNull ServiceId authServiceserviceId, @NonNull GroupMasterKey groupMasterKey) throws IOException { try { - new GroupManagerV2(context).groupServerQuery(groupMasterKey); + new GroupManagerV2(context).groupServerQuery(authServiceserviceId, groupMasterKey); return V2GroupServerStatus.FULL_OR_PENDING_MEMBER; } catch (GroupNotAMemberException e) { return V2GroupServerStatus.NOT_A_MEMBER; @@ -210,11 +211,12 @@ public final class GroupManager { * If it fails to get the exact version, it will give the latest. */ @WorkerThread - public static DecryptedGroup addedGroupVersion(@NonNull Context context, + public static DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, + @NonNull Context context, @NonNull GroupMasterKey groupMasterKey) throws IOException, GroupDoesNotExistException, GroupNotAMemberException { - return new GroupManagerV2(context).addedGroupVersion(groupMasterKey); + return new GroupManagerV2(context).addedGroupVersion(authServiceId, groupMasterKey); } @WorkerThread @@ -268,12 +270,13 @@ public final class GroupManager { @WorkerThread public static void revokeInvites(@NonNull Context context, + @NonNull ServiceId authServiceId, @NonNull GroupId.V2 groupId, @NonNull Collection uuidCipherTexts) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException { try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) { - editor.revokeInvites(uuidCipherTexts); + editor.revokeInvites(authServiceId, uuidCipherTexts, true); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index b3816d62d2..6d709c24d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -62,6 +62,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; @@ -97,6 +98,7 @@ final class GroupManagerV2 { private final GroupsV2Authorization authorization; private final GroupsV2StateProcessor groupsV2StateProcessor; private final ACI selfAci; + private final PNI selfPni; private final GroupCandidateHelper groupCandidateHelper; private final SendGroupUpdateHelper sendGroupUpdateHelper; @@ -108,6 +110,7 @@ final class GroupManagerV2 { ApplicationDependencies.getGroupsV2Authorization(), ApplicationDependencies.getGroupsV2StateProcessor(), SignalStore.account().requireAci(), + SignalStore.account().requirePni(), new GroupCandidateHelper(context), new SendGroupUpdateHelper(context)); } @@ -119,6 +122,7 @@ final class GroupManagerV2 { GroupsV2Authorization authorization, GroupsV2StateProcessor groupsV2StateProcessor, ACI selfAci, + PNI selfPni, GroupCandidateHelper groupCandidateHelper, SendGroupUpdateHelper sendGroupUpdateHelper) { @@ -129,6 +133,7 @@ final class GroupManagerV2 { this.authorization = authorization; this.groupsV2StateProcessor = groupsV2StateProcessor; this.selfAci = selfAci; + this.selfPni = selfPni; this.groupCandidateHelper = groupCandidateHelper; this.sendGroupUpdateHelper = sendGroupUpdateHelper; } @@ -204,18 +209,18 @@ final class GroupManagerV2 { } @WorkerThread - void groupServerQuery(@NonNull GroupMasterKey groupMasterKey) + void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) throws GroupNotAMemberException, IOException, GroupDoesNotExistException { - new GroupsV2StateProcessor(context).forGroup(groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey) .getCurrentGroupStateFromServer(); } @WorkerThread - @NonNull DecryptedGroup addedGroupVersion(@NonNull GroupMasterKey groupMasterKey) + @NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey) throws GroupNotAMemberException, IOException, GroupDoesNotExistException { - GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(groupMasterKey); + GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey); DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer(); if (latest.getRevision() == 0) { @@ -233,7 +238,7 @@ final class GroupManagerV2 { if (joinedVersion != null) { return joinedVersion; } else { - Log.w(TAG, "Unable to retreive exact version joined at, using latest"); + Log.w(TAG, "Unable to retrieve exact version joined at, using latest"); return latest; } } @@ -269,7 +274,8 @@ final class GroupManagerV2 { } @WorkerThread - @NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection members, + @NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId, + @NonNull Collection members, @Nullable String name, @Nullable byte[] avatar, int disappearingMessagesTimer) @@ -285,7 +291,7 @@ final class GroupManagerV2 { } GroupMasterKey masterKey = groupSecretParams.getMasterKey(); - GroupId.V2 groupId = groupDatabase.create(masterKey, decryptedGroup); + GroupId.V2 groupId = groupDatabase.create(authServiceId, masterKey, decryptedGroup); RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); @@ -306,6 +312,7 @@ final class GroupManagerV2 { } } + @SuppressWarnings("UnusedReturnValue") final class GroupEditor extends LockOwner { private final GroupId.V2 groupId; @@ -340,35 +347,35 @@ final class GroupManagerV2 { groupCandidates = GroupCandidate.withoutProfileKeyCredentials(groupCandidates); } - return commitChangeWithConflictResolution(groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupMembershipChange(groupCandidates, bannedMembers, selfAci.uuid())); } @WorkerThread @NonNull GroupManager.GroupActionResult updateGroupTimer(int expirationTime) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createModifyGroupTimerChange(expirationTime)); + return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupTimerChange(expirationTime)); } @WorkerThread @NonNull GroupManager.GroupActionResult updateAttributesRights(@NonNull GroupAccessControl newRights) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights))); + return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeAttributesRights(rightsToAccessControl(newRights))); } @WorkerThread @NonNull GroupManager.GroupActionResult updateMembershipRights(@NonNull GroupAccessControl newRights) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights))); + return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights))); } @WorkerThread @NonNull GroupManager.GroupActionResult updateAnnouncementGroup(boolean isAnnouncementGroup) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createAnnouncementGroupChange(isAnnouncementGroup)); + return commitChangeWithConflictResolution(selfAci, groupOperations.createAnnouncementGroupChange(isAnnouncementGroup)); } @WorkerThread @@ -390,7 +397,7 @@ final class GroupManagerV2 { .setAvatar(cdnKey)); } - GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(change); + GroupManager.GroupActionResult groupActionResult = commitChangeWithConflictResolution(selfAci, change); if (avatarChanged) { AvatarHelper.setAvatar(context, Recipient.externalGroupExact(context, groupId).getId(), avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); @@ -404,10 +411,10 @@ final class GroupManagerV2 { } @WorkerThread - @NonNull GroupManager.GroupActionResult revokeInvites(@NonNull Collection uuidCipherTexts) + @NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection uuidCipherTexts, boolean sendToMembers) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts))); + return commitChangeWithConflictResolution(authServiceId, groupOperations.createRemoveInvitationChange(new HashSet<>(uuidCipherTexts)), false, sendToMembers); } @WorkerThread @@ -418,7 +425,7 @@ final class GroupManagerV2 { .map(r -> Recipient.resolved(r).requireServiceId().uuid()) .collect(Collectors.toSet()); - return commitChangeWithConflictResolution(groupOperations.createApproveGroupJoinRequest(uuids)); + return commitChangeWithConflictResolution(selfAci, groupOperations.createApproveGroupJoinRequest(uuids)); } @WorkerThread @@ -429,7 +436,7 @@ final class GroupManagerV2 { .map(r -> Recipient.resolved(r).requireServiceId().uuid()) .collect(Collectors.toSet()); - return commitChangeWithConflictResolution(groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().getBannedMembersList())); } @WorkerThread @@ -438,33 +445,47 @@ final class GroupManagerV2 { throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { Recipient recipient = Recipient.resolved(recipientId); - return commitChangeWithConflictResolution(groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT)); + return commitChangeWithConflictResolution(selfAci, groupOperations.createChangeMemberRole(recipient.requireServiceId().uuid(), admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT)); } @WorkerThread - @NonNull GroupManager.GroupActionResult leaveGroup() + void leaveGroup() throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - GroupDatabase.GroupRecord groupRecord = groupDatabase.getGroup(groupId).get(); - List pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList(); - Optional selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid()); + GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); + DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup(); + Optional selfMember = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), selfAci.uuid()); + Optional aciPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfAci.uuid()); + Optional pniPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfPni.uuid()); + Optional selfPendingMember = Optional.empty(); + ServiceId serviceId = selfAci; + + if (aciPendingMember.isPresent()) { + selfPendingMember = aciPendingMember; + } else if (pniPendingMember.isPresent() && !selfMember.isPresent()) { + selfPendingMember = pniPendingMember; + serviceId = selfPni; + } if (selfPendingMember.isPresent()) { try { - return revokeInvites(Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray()))); + revokeInvites(serviceId, Collections.singleton(new UuidCiphertext(selfPendingMember.get().getUuidCipherText().toByteArray())), false); } catch (InvalidInputException e) { throw new AssertionError(e); } + } else if (selfMember.isPresent()) { + ejectMember(serviceId, true, false); } else { - return ejectMember(ServiceId.from(selfAci.uuid()), true, false); + Log.i(TAG, "Unable to leave group we are not pending or in"); } } @WorkerThread - @NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId serviceId, boolean allowWhenBlocked, boolean ban) + @NonNull GroupManager.GroupActionResult ejectMember(@NonNull ServiceId authServiceId, boolean allowWhenBlocked, boolean ban) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException { - return commitChangeWithConflictResolution(groupOperations.createRemoveMembersChange(Collections.singleton(serviceId.uuid()), + return commitChangeWithConflictResolution(authServiceId, + groupOperations.createRemoveMembersChange(Collections.singleton(authServiceId.uuid()), ban, ban ? v2GroupProperties.getDecryptedGroup().getBannedMembersList() : Collections.emptyList()), @@ -475,10 +496,9 @@ final class GroupManagerV2 { @NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection newAdmins) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { - Recipient self = Recipient.self(); List newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).requireServiceId().uuid()).toList(); - return commitChangeWithConflictResolution(groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(), + return commitChangeWithConflictResolution(selfAci, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(), newAdminRecipients)); } @@ -510,7 +530,7 @@ final class GroupManagerV2 { return null; } - return commitChangeWithConflictResolution(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.requireProfileKeyCredential())); } @WorkerThread @@ -532,7 +552,7 @@ final class GroupManagerV2 { return null; } - return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createAcceptInviteChange(groupCandidate.requireProfileKeyCredential())); } public GroupManager.GroupActionResult ban(UUID uuid) @@ -541,20 +561,20 @@ final class GroupManagerV2 { ByteString uuidByteString = UuidUtil.toByteString(uuid); boolean rejectJoinRequest = v2GroupProperties.getDecryptedGroup().getRequestingMembersList().stream().anyMatch(m -> m.getUuid().equals(uuidByteString)); - return commitChangeWithConflictResolution(groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createBanUuidsChange(Collections.singleton(uuid), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().getBannedMembersList())); } public GroupManager.GroupActionResult unban(Set uuids) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { - return commitChangeWithConflictResolution(groupOperations.createUnbanUuidsChange(uuids)); + return commitChangeWithConflictResolution(selfAci, groupOperations.createUnbanUuidsChange(uuids)); } @WorkerThread public GroupManager.GroupActionResult cycleGroupLinkPassword() throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { - return commitChangeWithConflictResolution(groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize())); + return commitChangeWithConflictResolution(selfAci, groupOperations.createModifyGroupLinkPasswordChange(GroupLinkPassword.createNew().serialize())); } @WorkerThread @@ -581,7 +601,7 @@ final class GroupManagerV2 { } } - commitChangeWithConflictResolution(change); + commitChangeWithConflictResolution(selfAci, change); if (state != GroupManager.GroupLinkState.DISABLED) { GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.requireGroup(groupId).requireV2GroupProperties(); @@ -594,26 +614,32 @@ final class GroupManagerV2 { } } - private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change) + private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { - return commitChangeWithConflictResolution(change, false); + return commitChangeWithConflictResolution(authServiceId, change, false); } - private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked) + private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked) throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException { - change.setSourceUuid(UuidUtil.toByteString(selfAci.uuid())); + return commitChangeWithConflictResolution(authServiceId, change, allowWhenBlocked, true); + } + + private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers) + throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException + { + change.setSourceUuid(UuidUtil.toByteString(authServiceId.uuid())); for (int attempt = 0; attempt < 5; attempt++) { try { - return commitChange(change, allowWhenBlocked); + return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers); } catch (GroupPatchNotAcceptedException e) { throw new GroupChangeFailedException(e); } catch (ConflictException e) { Log.w(TAG, "Invalid group patch or conflict", e); - change = resolveConflict(change); + change = resolveConflict(authServiceId, change); if (GroupChangeUtil.changeIsEmpty(change.build())) { Log.i(TAG, "Change is empty after conflict resolution"); @@ -628,10 +654,10 @@ final class GroupManagerV2 { throw new GroupChangeFailedException("Unable to apply change to group after conflicts"); } - private GroupChange.Actions.Builder resolveConflict(@NonNull GroupChange.Actions.Builder change) + private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change) throws IOException, GroupNotAMemberException, GroupChangeFailedException { - GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(groupMasterKey) + GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(authServiceId, groupMasterKey) .updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null); if (groupUpdateResult.getLatestServer() == null) { @@ -652,14 +678,14 @@ final class GroupManagerV2 { GroupChange.Actions changeActions = change.build(); return GroupChangeUtil.resolveConflict(groupUpdateResult.getLatestServer(), - groupOperations.decryptChange(changeActions, selfAci.uuid()), + groupOperations.decryptChange(changeActions, authServiceId.uuid()), changeActions); } catch (VerificationFailedException | InvalidGroupStateException ex) { throw new GroupChangeFailedException(ex); } } - private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked) + private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers) throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId); @@ -676,8 +702,9 @@ final class GroupManagerV2 { previousGroupState = v2GroupProperties.getDecryptedGroup(); - GroupChange signedGroupChange = commitToServer(changeActions); + GroupChange signedGroupChange = commitToServer(authServiceId, changeActions); try { + //noinspection OptionalGetWithoutIsPresent decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { @@ -688,18 +715,18 @@ final class GroupManagerV2 { groupDatabase.update(groupId, decryptedGroupState); GroupMutation groupMutation = new GroupMutation(previousGroupState, decryptedChange, decryptedGroupState); - RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange); + RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, groupMutation, signedGroupChange, sendToMembers); int newMembersCount = decryptedChange.getNewMembersCount(); List newPendingMembers = getPendingMemberRecipientIds(decryptedChange.getNewPendingMembersList()); return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers); } - private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change) + private @NonNull GroupChange commitToServer(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions change) throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException { try { - return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(selfAci, groupSecretParams), Optional.empty()); + return groupsV2Api.patchGroup(change, authorization.getAuthorizationForToday(authServiceId, groupSecretParams), Optional.empty()); } catch (NotInGroupException e) { Log.w(TAG, e); throw new GroupNotAMemberException(e); @@ -724,10 +751,10 @@ final class GroupManagerV2 { } @WorkerThread - void updateLocalToServerRevision(int revision, long timestamp, @Nullable byte[] signedGroupChange) + void updateLocalToServerRevision(@NonNull ServiceId authServiceId, int revision, long timestamp, @Nullable byte[] signedGroupChange) throws IOException, GroupNotAMemberException { - new GroupsV2StateProcessor(context).forGroup(groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(authServiceId, groupMasterKey) .updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange)); } @@ -880,7 +907,7 @@ final class GroupManagerV2 { groupDatabase.update(groupId, updatedGroup); } else { - groupDatabase.create(groupMasterKey, decryptedGroup); + groupDatabase.create(selfAci, groupMasterKey, decryptedGroup); Log.i(TAG, "Created local group with placeholder"); } @@ -924,7 +951,7 @@ final class GroupManagerV2 { throws GroupChangeFailedException, IOException { try { - new GroupsV2StateProcessor(context).forGroup(groupMasterKey) + new GroupsV2StateProcessor(context).forGroup(selfAci, groupMasterKey) .updateLocalGroupToRevision(decryptedChange.getRevision(), System.currentTimeMillis(), decryptedChange); @@ -956,6 +983,7 @@ final class GroupManagerV2 { throws GroupChangeFailedException { try { + //noinspection OptionalGetWithoutIsPresent return groupOperations.decryptChange(signedGroupChange, false).get(); } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) { Log.w(TAG, e); @@ -1006,7 +1034,7 @@ final class GroupManagerV2 { throw new MembershipNotSuitableForV2Exception("No profile key credential for self"); } - ProfileKeyCredential profileKeyCredential = self.getProfileKeyCredential().get(); + ProfileKeyCredential profileKeyCredential = self.requireProfileKeyCredential(); GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential) : groupOperations.createGroupJoinDirect(profileKeyCredential); @@ -1123,6 +1151,7 @@ final class GroupManagerV2 { DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup(); try { + //noinspection OptionalGetWithoutIsPresent DecryptedGroupChange decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get(); DecryptedGroup newGroup = DecryptedGroupUtil.applyWithoutRevisionCheck(decryptedGroup, decryptedChange); @@ -1226,6 +1255,7 @@ final class GroupManagerV2 { return new RecipientAndThread(groupRecipient, -1); } else { + //noinspection IfStatementWithIdenticalBranches if (sendToMembers) { long threadId = MessageSender.send(context, outgoingMessage, -1, false, null, null); return new RecipientAndThread(groupRecipient, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupNotAMemberException.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupNotAMemberException.java index 672bb6429c..5c8bc02157 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupNotAMemberException.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupNotAMemberException.java @@ -2,10 +2,23 @@ package org.thoughtcrime.securesms.groups; public final class GroupNotAMemberException extends GroupChangeException { + private final boolean likelyPendingMember; + public GroupNotAMemberException(Throwable throwable) { super(throwable); + this.likelyPendingMember = false; + } + + public GroupNotAMemberException(GroupNotAMemberException throwable, boolean likelyPendingMember) { + super(throwable.getCause() != null ? throwable.getCause() : throwable); + this.likelyPendingMember = likelyPendingMember; } GroupNotAMemberException() { + this.likelyPendingMember = false; + } + + public boolean isLikelyPendingMember() { + return likelyPendingMember; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java index 8fcd5a6ad6..50a7a547ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV1MigrationUtil.java @@ -72,7 +72,7 @@ public final class GroupsV1MigrationUtil { throw new InvalidMigrationStateException(); } - switch (GroupManager.v2GroupStatus(context, gv2MasterKey)) { + switch (GroupManager.v2GroupStatus(context, SignalStore.account().getAci(), gv2MasterKey)) { case DOES_NOT_EXIST: Log.i(TAG, "Group does not exist on the service."); @@ -172,7 +172,7 @@ public final class GroupsV1MigrationUtil { try (Closeable ignored = GroupsV2ProcessingLock.acquireGroupProcessingLock()){ DecryptedGroup decryptedGroup; try { - decryptedGroup = GroupManager.addedGroupVersion(context, gv1Id.deriveV2MigrationMasterKey()); + decryptedGroup = GroupManager.addedGroupVersion(SignalStore.account().requireAci(), context, gv1Id.deriveV2MigrationMasterKey()); } catch (GroupDoesNotExistException e) { throw new IOException("[Local] The group should exist already!"); } catch (GroupNotAMemberException e) { @@ -186,7 +186,7 @@ public final class GroupsV1MigrationUtil { Log.i(TAG, "[Local] Applying all changes since V" + decryptedGroup.getRevision()); try { - GroupManager.updateGroupFromServer(context, gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null); + GroupManager.updateGroupFromServer(context, SignalStore.account().requireAci(), gv1Id.deriveV2MigrationMasterKey(), LATEST, System.currentTimeMillis(), null); } catch (GroupChangeBusyException | GroupNotAMemberException e) { Log.w(TAG, e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java index ab1b99c62b..60961b642e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2Authorization.java @@ -6,37 +6,53 @@ import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; -import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.ServiceId; import java.io.IOException; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; public class GroupsV2Authorization { private static final String TAG = Log.tag(GroupsV2Authorization.class); - private final ValueCache cache; + private final ValueCache aciCache; + private final ValueCache pniCache; private final GroupsV2Api groupsV2Api; - public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache cache) { + public GroupsV2Authorization(@NonNull GroupsV2Api groupsV2Api, @NonNull ValueCache aciCache, @NonNull ValueCache pniCache) { this.groupsV2Api = groupsV2Api; - this.cache = cache; + this.aciCache = aciCache; + this.pniCache = pniCache; } - public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ACI self, + public GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId, @NonNull GroupSecretParams groupSecretParams) throws IOException, VerificationFailedException + { + boolean isPni = Objects.equals(authServiceId, SignalStore.account().getPni()); + ValueCache cache = isPni ? pniCache : aciCache; + + return getAuthorizationForToday(authServiceId, cache, groupSecretParams, !isPni); + } + + private GroupsV2AuthorizationString getAuthorizationForToday(@NonNull ServiceId authServiceId, + @NonNull ValueCache cache, + @NonNull GroupSecretParams groupSecretParams, + boolean isAci) + throws IOException, VerificationFailedException { final int today = currentTimeDays(); Map credentials = cache.read(); try { - return getAuthorization(self, groupSecretParams, credentials, today); + return getAuthorization(authServiceId, groupSecretParams, credentials, today); } catch (NoCredentialForRedemptionTimeException e) { Log.i(TAG, "Auth out of date, will update auth and try again"); cache.clear(); @@ -46,11 +62,11 @@ public class GroupsV2Authorization { } Log.i(TAG, "Getting new auth credential responses"); - credentials = groupsV2Api.getCredentials(today); + credentials = groupsV2Api.getCredentials(today, isAci); cache.write(credentials); try { - return getAuthorization(self, groupSecretParams, credentials, today); + return getAuthorization(authServiceId, groupSecretParams, credentials, today); } catch (NoCredentialForRedemptionTimeException e) { Log.w(TAG, "The credentials returned did not include the day requested"); throw new IOException("Failed to get credentials"); @@ -58,14 +74,15 @@ public class GroupsV2Authorization { } public void clear() { - cache.clear(); + aciCache.clear(); + pniCache.clear(); } private static int currentTimeDays() { return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); } - private GroupsV2AuthorizationString getAuthorization(ACI self, + private GroupsV2AuthorizationString getAuthorization(ServiceId authServiceId, GroupSecretParams groupSecretParams, Map credentials, int today) @@ -77,7 +94,7 @@ public class GroupsV2Authorization { throw new NoCredentialForRedemptionTimeException(); } - return groupsV2Api.getGroupsV2AuthorizationString(self, today, groupSecretParams, authCredentialResponse); + return groupsV2Api.getGroupsV2AuthorizationString(authServiceId, today, groupSecretParams, authCredentialResponse); } public interface ValueCache { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java index 17c8b6b2c8..a2a78d169b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java @@ -58,7 +58,8 @@ final class AddGroupDetailsRepository { Set recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList()); try { - GroupManager.GroupActionResult result = GroupManager.createGroup(context, + GroupManager.GroupActionResult result = GroupManager.createGroup(SignalStore.account().requireAci(), + context, recipients, avatar, name, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invited/PendingMemberInvitesRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invited/PendingMemberInvitesRepository.java index c2547e6511..d4e76286a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invited/PendingMemberInvitesRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invited/PendingMemberInvitesRepository.java @@ -100,7 +100,7 @@ final class PendingMemberInvitesRepository { @WorkerThread boolean revokeInvites(@NonNull Collection uuidCipherTexts) { try { - GroupManager.revokeInvites(context, groupId, uuidCipherTexts); + GroupManager.revokeInvites(context, SignalStore.account().requireAci(), groupId, uuidCipherTexts); return true; } catch (GroupChangeException | IOException e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index d0637a0b1c..8587e266ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -18,6 +18,7 @@ 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.DecryptedPendingMember; +import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.MessageDatabase; @@ -52,7 +53,6 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup; -import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException; @@ -67,7 +67,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; /** * Advances a groups state to a specified revision. @@ -104,11 +103,10 @@ public class GroupsV2StateProcessor { this.groupDatabase = SignalDatabase.groups(); } - public StateProcessorForGroup forGroup(@NonNull GroupMasterKey groupMasterKey) { - ACI selfAci = SignalStore.account().requireAci(); - ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, selfAci, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase); + public StateProcessorForGroup forGroup(@NonNull ServiceId serviceId, @NonNull GroupMasterKey groupMasterKey) { + ProfileAndMessageHelper profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceId, groupMasterKey, GroupId.v2(groupMasterKey), recipientDatabase); - return new StateProcessorForGroup(selfAci, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper); + return new StateProcessorForGroup(serviceId, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, profileAndMessageHelper); } public enum GroupState { @@ -142,7 +140,7 @@ public class GroupsV2StateProcessor { } public static final class StateProcessorForGroup { - private final ACI selfAci; + private final ServiceId serviceId; private final Context context; private final GroupDatabase groupDatabase; private final GroupsV2Api groupsV2Api; @@ -152,7 +150,7 @@ public class GroupsV2StateProcessor { private final GroupSecretParams groupSecretParams; private final ProfileAndMessageHelper profileAndMessageHelper; - @VisibleForTesting StateProcessorForGroup(@NonNull ACI selfAci, + @VisibleForTesting StateProcessorForGroup(@NonNull ServiceId serviceId, @NonNull Context context, @NonNull GroupDatabase groupDatabase, @NonNull GroupsV2Api groupsV2Api, @@ -160,7 +158,7 @@ public class GroupsV2StateProcessor { @NonNull GroupMasterKey groupMasterKey, @NonNull ProfileAndMessageHelper profileAndMessageHelper) { - this.selfAci = selfAci; + this.serviceId = serviceId; this.context = context; this.groupDatabase = groupDatabase; this.groupsV2Api = groupsV2Api; @@ -197,7 +195,7 @@ public class GroupsV2StateProcessor { revision == signedGroupChange.getRevision()) { - if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange)) { + if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) { Log.w(TAG, "Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch."); } else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) { Log.w(TAG, "Ignoring P2P group change by setting"); @@ -233,8 +231,9 @@ public class GroupsV2StateProcessor { } if (inputGroupState == null) { - if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, selfAci.uuid())) { + if (localState != null && DecryptedGroupUtil.isPendingOrRequesting(localState, serviceId.uuid())) { 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"); + throw new GroupNotAMemberException(e, true); } else { Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); insertGroupLeave(); @@ -277,26 +276,34 @@ public class GroupsV2StateProcessor { .map(DecryptedMember::getUuid) .map(UuidUtil::fromByteStringOrNull) .filter(Objects::nonNull) - .collect(Collectors.toSet()) - .contains(selfAci.uuid()); + .anyMatch(u -> u.equals(serviceId.uuid())); boolean addedAsPendingMember = signedGroupChange.getNewPendingMembersList() .stream() .map(DecryptedPendingMember::getUuid) .map(UuidUtil::fromByteStringOrNull) .filter(Objects::nonNull) - .collect(Collectors.toSet()) - .contains(selfAci.uuid()); + .anyMatch(u -> u.equals(serviceId.uuid())); return !currentlyInGroup && !addedAsMember && !addedAsPendingMember; } + private boolean notHavingInviteRevoked(@NonNull DecryptedGroupChange signedGroupChange) { + boolean havingInviteRevoked = signedGroupChange.getDeletePendingMembersList() + .stream() + .map(DecryptedPendingMemberRemoval::getUuid) + .map(UuidUtil::fromByteStringOrNull) + .filter(Objects::nonNull) + .anyMatch(u -> u.equals(serviceId.uuid())); + + return !havingInviteRevoked; + } + /** * Using network, attempt to bring the local copy of the group up to the revision specified via paging. */ private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException { boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION); - ACI selfAci = this.selfAci; Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly); @@ -304,37 +311,38 @@ public class GroupsV2StateProcessor { GlobalGroupState inputGroupState; try { - latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams)); } catch (NotInGroupException | GroupNotFoundException e) { throw new GroupNotAMemberException(e); } catch (VerificationFailedException | InvalidGroupStateException e) { throw new IOException(e); } - if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList())) { + if (localState != null && localState.getRevision() >= latestServerGroup.getRevision() && GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList())) { Log.i(TAG, "Local state is at or later than server"); return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); } - if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) { + if (latestRevisionOnly || !GroupProtoUtil.isMember(serviceId.uuid(), latestServerGroup.getMembersList())) { Log.i(TAG, "Latest revision or not a member, use latest only"); inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null))); } else { - int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid()); - int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded; - boolean includeFirstState = forceIncludeFirst || - localState == null || - localState.getRevision() < 0 || - localState.getRevision() == revisionWeWereAdded || - !GroupProtoUtil.isMember(selfAci.uuid(), localState.getMembersList()) || - (revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision()); + int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, serviceId.uuid()); + int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded; + + boolean includeFirstState = forceIncludeFirst || + localState == null || + localState.getRevision() < 0 || + localState.getRevision() == revisionWeWereAdded || + !GroupProtoUtil.isMember(serviceId.uuid(), localState.getMembersList()) || + (revision == LATEST && localState.getRevision() + 1 < latestServerGroup.getRevision()); Log.i(TAG, "Requesting from server currentRevision: " + (localState != null ? localState.getRevision() : "null") + " logsNeededFrom: " + logsNeededFrom + " includeFirstState: " + includeFirstState + " forceIncludeFirst: " + forceIncludeFirst); - inputGroupState = getFullMemberHistoryPage(localState, selfAci, logsNeededFrom, includeFirstState); + inputGroupState = getFullMemberHistoryPage(localState, serviceId, logsNeededFrom, includeFirstState); } ProfileKeySet profileKeys = new ProfileKeySet(); @@ -350,7 +358,7 @@ public class GroupsV2StateProcessor { if (newLocalState != null && !inputGroupState.hasMore() && !forceIncludeFirst) { int newLocalRevision = newLocalState.getRevision(); - int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision; + int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision; if (newLocalRevision < requestRevision) { Log.w(TAG, "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]"); return updateLocalGroupFromServerPaged(revision, localState, timestamp, true); @@ -382,7 +390,7 @@ public class GroupsV2StateProcessor { if (hasMore) { Log.i(TAG, "Request next page from server revision: " + finalState.getRevision() + " nextPageRevision: " + inputGroupState.getNextPageRevision()); - inputGroupState = getFullMemberHistoryPage(finalState, selfAci, inputGroupState.getNextPageRevision(), false); + inputGroupState = getFullMemberHistoryPage(finalState, serviceId, inputGroupState.getNextPageRevision(), false); } } @@ -406,7 +414,7 @@ public class GroupsV2StateProcessor { throws IOException, GroupNotAMemberException, GroupDoesNotExistException { try { - return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams)); } catch (GroupNotFoundException e) { throw new GroupDoesNotExistException(e); } catch (NotInGroupException e) { @@ -421,7 +429,7 @@ public class GroupsV2StateProcessor { throws IOException, GroupNotAMemberException, GroupDoesNotExistException { try { - return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), true) + return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), true) .getResults() .get(0) .getGroup() @@ -442,7 +450,7 @@ public class GroupsV2StateProcessor { } Recipient groupRecipient = Recipient.externalGroupExact(context, groupId); - UUID selfUuid = selfAci.uuid(); + UUID selfUuid = serviceId.uuid(); DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId) .requireV2GroupProperties() @@ -501,7 +509,7 @@ public class GroupsV2StateProcessor { boolean needsAvatarFetch; if (inputGroupState.getLocalState() == null) { - groupDatabase.create(masterKey, newLocalState); + groupDatabase.create(serviceId, masterKey, newLocalState); needsAvatarFetch = !TextUtils.isEmpty(newLocalState.getAvatar()); } else { groupDatabase.update(masterKey, newLocalState); @@ -515,9 +523,9 @@ public class GroupsV2StateProcessor { profileAndMessageHelper.determineProfileSharing(inputGroupState, newLocalState); } - private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ACI selfAci, int logsNeededFromRevision, boolean includeFirstState) throws IOException { + private GlobalGroupState getFullMemberHistoryPage(DecryptedGroup localState, @NonNull ServiceId serviceId, int logsNeededFromRevision, boolean includeFirstState) throws IOException { try { - GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams), includeFirstState); + GroupHistoryPage groupHistoryPage = groupsV2Api.getGroupHistoryPage(groupSecretParams, logsNeededFromRevision, groupsV2Authorization.getAuthorizationForToday(serviceId, groupSecretParams), includeFirstState); ArrayList history = new ArrayList<>(groupHistoryPage.getResults().size()); boolean ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(); @@ -545,14 +553,14 @@ public class GroupsV2StateProcessor { static class ProfileAndMessageHelper { private final Context context; - private final ACI selfAci; + private final ServiceId serviceId; private final GroupMasterKey masterKey; private final GroupId.V2 groupId; private final RecipientDatabase recipientDatabase; - ProfileAndMessageHelper(@NonNull Context context, @NonNull ACI selfAci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) { + ProfileAndMessageHelper(@NonNull Context context, @NonNull ServiceId serviceId, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientDatabase recipientDatabase) { this.context = context; - this.selfAci = selfAci; + this.serviceId = serviceId; this.masterKey = masterKey; this.groupId = groupId; this.recipientDatabase = recipientDatabase; @@ -560,15 +568,15 @@ public class GroupsV2StateProcessor { void determineProfileSharing(@NonNull GlobalGroupState inputGroupState, @NonNull DecryptedGroup newLocalState) { if (inputGroupState.getLocalState() != null) { - boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), selfAci.uuid()).isPresent(); + boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), serviceId.uuid()).isPresent(); if (wasAMemberAlready) { return; } } - Optional selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), selfAci.uuid()); - Optional selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), selfAci.uuid()); + Optional selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), serviceId.uuid()); + Optional selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), serviceId.uuid()); if (selfAsMemberOptional.isPresent()) { DecryptedMember selfAsMember = selfAsMemberOptional.get(); @@ -587,7 +595,7 @@ public class GroupsV2StateProcessor { Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId())); - if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), selfAci.uuid()))) { + if (addedBy.isBlocked() && (inputGroupState.getLocalState() == null || !DecryptedGroupUtil.isRequesting(inputGroupState.getLocalState(), serviceId.uuid()))) { Log.i(TAG, "Added by a blocked user. Leaving group."); ApplicationDependencies.getJobManager().add(new LeaveGroupV2Job(groupId)); //noinspection UnnecessaryReturnStatement @@ -671,7 +679,7 @@ public class GroupsV2StateProcessor { void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) { Optional editor = getEditor(decryptedGroupV2Context).map(ServiceId::from); - boolean outgoing = !editor.isPresent() || selfAci.equals(editor.get()); + boolean outgoing = !editor.isPresent() || serviceId.equals(editor.get()); if (outgoing) { try { @@ -709,7 +717,7 @@ public class GroupsV2StateProcessor { if (changeEditor.isPresent()) { return changeEditor; } else { - Optional pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), selfAci.uuid()); + Optional pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), serviceId.uuid()); if (pendingByUuid.isPresent()) { return Optional.ofNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java index d4e26e0128..cf346c0169 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoWorkerJob.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; +import org.signal.libsignal.zkgroup.VerificationFailedException; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -15,8 +16,10 @@ 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.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; @@ -89,7 +92,24 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob { return; } - GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); + ServiceId authServiceId = group.get().getAuthServiceId() != null ? group.get().getAuthServiceId() : SignalStore.account().requireAci(); + + try { + GroupManager.updateGroupFromServer(context, authServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); + } catch (GroupNotAMemberException | IOException e) { + ServiceId otherServiceId = authServiceId.equals(SignalStore.account().getPni()) ? SignalStore.account().getAci() : SignalStore.account().getPni(); + boolean isNotAMemberOrPending = e instanceof GroupNotAMemberException && !((GroupNotAMemberException) e).isLikelyPendingMember(); + boolean verificationFailed = e.getCause() instanceof VerificationFailedException; + + if (otherServiceId != null && (isNotAMemberOrPending || verificationFailed)) { + Log.i(TAG, "Request failed, attempting with other id"); + GroupManager.updateGroupFromServer(context, otherServiceId, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null); + Log.i(TAG, "Request succeeded with other credential. Associating " + otherServiceId + " with group " + groupId); + SignalDatabase.groups().setAuthServiceId(otherServiceId, groupId); + } else { + throw e; + } + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 7cc436dfc9..e6847b04a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -111,6 +111,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal putString(KEY_PNI, pni.toString()) } + fun requireServiceIds(): ServiceIds { + return ServiceIds(requireAci(), requirePni()) + } + /** The local user's E164. */ val e164: String? get() = getString(KEY_E164, null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java index 4cd616b8d2..068570c7e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/GroupsV2AuthorizationSignalStoreCache.java @@ -21,37 +21,49 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth private static final String TAG = Log.tag(GroupsV2AuthorizationSignalStoreCache.class); - private static final String PREFIX = "gv2:auth_token_cache"; - private static final int VERSION = 2; - private static final String KEY = PREFIX + ":" + VERSION; + private static final String ACI_PREFIX = "gv2:auth_token_cache"; + private static final int ACI_VERSION = 2; + private static final String PNI_PREFIX = "gv2:auth_token_cache:pni"; + private static final int PNI_VERSION = 1; + + private final String key; private final KeyValueStore store; - GroupsV2AuthorizationSignalStoreCache(KeyValueStore store) { - this.store = store; - - if (store.containsKey(PREFIX)) { + public static GroupsV2AuthorizationSignalStoreCache createAciCache(@NonNull KeyValueStore store) { + if (store.containsKey(ACI_PREFIX)) { store.beginWrite() - .remove(PREFIX) + .remove(ACI_PREFIX) .commit(); } + + return new GroupsV2AuthorizationSignalStoreCache(store, ACI_PREFIX + ":" + ACI_VERSION); + } + + public static GroupsV2AuthorizationSignalStoreCache createPniCache(@NonNull KeyValueStore store) { + return new GroupsV2AuthorizationSignalStoreCache(store, PNI_PREFIX + ":" + PNI_VERSION); + } + + private GroupsV2AuthorizationSignalStoreCache(@NonNull KeyValueStore store, @NonNull String key) { + this.store = store; + this.key = key; } @Override public void clear() { store.beginWrite() - .remove(KEY) + .remove(key) .commit(); - Log.i(TAG, "Cleared local response cache"); + info("Cleared local response cache"); } @Override public @NonNull Map read() { - byte[] credentialBlob = store.getBlob(KEY, null); + byte[] credentialBlob = store.getBlob(key, null); if (credentialBlob == null) { - Log.i(TAG, "No credentials responses are cached locally"); + info("No credentials responses are cached locally"); return Collections.emptyMap(); } @@ -63,7 +75,7 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth result.put(credential.getDate(), new AuthCredentialResponse(credential.getAuthCredentialResponse().toByteArray())); } - Log.i(TAG, String.format(Locale.US, "Loaded %d credentials from local storage", result.size())); + info(String.format(Locale.US, "Loaded %d credentials from local storage", result.size())); return result; } catch (InvalidProtocolBufferException | InvalidInputException e) { @@ -82,9 +94,13 @@ public final class GroupsV2AuthorizationSignalStoreCache implements GroupsV2Auth } store.beginWrite() - .putBlob(KEY, builder.build().toByteArray()) + .putBlob(key, builder.build().toByteArray()) .commit(); - Log.i(TAG, String.format(Locale.US, "Written %d credentials to local storage", values.size())); + info(String.format(Locale.US, "Written %d credentials to local storage", values.size())); + } + + private void info(String message) { + Log.i(TAG, (key.startsWith(PNI_PREFIX) ? "[PNI]" : "[ACI]") + " " + message); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt new file mode 100644 index 0000000000..ec484c0e29 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ServiceIds.kt @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.keyvalue + +import com.google.protobuf.ByteString +import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI +import org.whispersystems.signalservice.api.util.UuidUtil +import java.util.UUID + +/** + * Helper for dealing with [ServiceId] matching when you only care that either of your + * service ids match but don't care which one. + */ +data class ServiceIds(val aci: ACI, val pni: PNI) { + + private val aciByteString: ByteString by lazy { UuidUtil.toByteString(aci.uuid()) } + private val pniByteString: ByteString by lazy { UuidUtil.toByteString(pni.uuid()) } + + fun matches(uuid: UUID): Boolean { + return uuid == aci.uuid() || uuid == pni.uuid() + } + + fun matches(uuid: ByteString): Boolean { + return uuid == aciByteString || uuid == pniByteString + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index 73a1dfc11c..ebff9ed62c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -261,8 +261,12 @@ public final class SignalStore { return getInstance().storyValues; } - public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { - return new GroupsV2AuthorizationSignalStoreCache(getStore()); + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AciAuthorizationCache() { + return GroupsV2AuthorizationSignalStoreCache.createAciCache(getStore()); + } + + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2PniAuthorizationCache() { + return GroupsV2AuthorizationSignalStoreCache.createPniCache(getStore()); } public static @NonNull PreferenceDataStore getPreferenceDataStore() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 048ca92c8f..77d4711762 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -12,6 +12,7 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.mobilecoin.lib.exceptions.SerializationException; +import org.signal.core.util.Hex; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.ecc.ECPublicKey; @@ -126,7 +127,6 @@ import org.thoughtcrime.securesms.stories.Stories; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.GroupUtil; -import org.signal.core.util.Hex; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; @@ -168,6 +168,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.payments.Money; import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -532,8 +533,13 @@ public final class MessageContentProcessor { throws IOException, GroupChangeBusyException { try { - long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; - GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); + ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid()); + if (authServiceId == null) { + warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI"); + authServiceId = SignalStore.account().requireAci(); + } + long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; + GroupManager.updateGroupFromServer(context, authServiceId, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); return true; } catch (GroupNotAMemberException e) { warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in"); @@ -816,7 +822,12 @@ public final class MessageContentProcessor { } } else if (group.getGroupV2().isPresent()) { warn(content.getTimestamp(), "Received a GV2 message for a group we have no knowledge of -- attempting to fix this state."); - SignalDatabase.groups().fixMissingMasterKey(group.getGroupV2().get().getMasterKey()); + ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid()); + if (authServiceId == null) { + warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI"); + authServiceId = SignalStore.account().requireAci(); + } + SignalDatabase.groups().fixMissingMasterKey(authServiceId, group.getGroupV2().get().getMasterKey()); } else { warn(content.getTimestamp(), "Received a message for a group we don't know about without a group context. Ignoring."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index a8b8a605aa..13656db383 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -154,7 +154,7 @@ public class RecipientUtil { recipient = recipient.resolve(); - if (recipient.isGroup() && recipient.getGroupId().get().isPush() && recipient.isActiveGroup()) { + if (recipient.isGroup() && recipient.getGroupId().get().isPush()) { GroupManager.leaveGroupFromBlockOrMessageRequest(context, recipient.getGroupId().get().requirePush()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java index 34131f5b4b..0745b822b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java @@ -57,7 +57,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< return StorageSyncModels.localToRemoteRecord(settings); } else { Log.w(TAG, "No local master key. Assuming it matches remote since the groupIds match. Enqueuing a fetch to fix the bad state."); - groupDatabase.fixMissingMasterKey(record.getMasterKeyOrThrow()); + groupDatabase.fixMissingMasterKey(null, record.getMasterKeyOrThrow()); return StorageSyncModels.localToRemoteRecord(settings, record.getMasterKeyOrThrow()); } }) diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt index 5ffb597956..e845284c2e 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.AppSignatureUtil import shark.AndroidReferenceMatchers +import java.util.Locale class SpinnerApplicationContext : ApplicationContext() { override fun onCreate() { @@ -36,7 +37,8 @@ class SpinnerApplicationContext : ApplicationContext() { "Profile Name" to (if (SignalStore.account().isRegistered) Recipient.self().profileName.toString() else "none"), "E164" to (SignalStore.account().e164 ?: "none"), "ACI" to (SignalStore.account().aci?.toString() ?: "none"), - "PNI" to (SignalStore.account().pni?.toString() ?: "none") + "PNI" to (SignalStore.account().pni?.toString() ?: "none"), + Spinner.KEY_ENVIRONMENT to BuildConfig.FLAVOR_environment.toUpperCase(Locale.US) ), linkedMapOf( "signal" to DatabaseConfig( diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2Transformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2Transformer.kt index cb730fd50b..2095a6edac 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2Transformer.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2Transformer.kt @@ -1,10 +1,16 @@ package org.thoughtcrime.securesms.database import android.database.Cursor +import com.google.protobuf.ByteString import org.signal.core.util.requireBlob import org.signal.core.util.requireString import org.signal.spinner.ColumnTransformer +import org.signal.storageservice.protos.groups.local.DecryptedBannedMember import org.signal.storageservice.protos.groups.local.DecryptedGroup +import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember +import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember +import org.whispersystems.signalservice.api.util.UuidUtil object GV2Transformer : ColumnTransformer { override fun matches(tableName: String?, columnName: String): Boolean { @@ -24,6 +30,11 @@ object GV2Transformer : ColumnTransformer { } private fun DecryptedGroup.formatAsHtml(): String { + val members: String = describeList(membersList, DecryptedMember::getUuid) + val pending: String = describeList(pendingMembersList, DecryptedPendingMember::getUuid) + val requesting: String = describeList(requestingMembersList, DecryptedRequestingMember::getUuid) + val banned: String = describeList(bannedMembersList, DecryptedBannedMember::getUuid) + return """ Revision: $revision Title: $title @@ -32,9 +43,24 @@ private fun DecryptedGroup.formatAsHtml(): String { Description: "$description" Announcement: $isAnnouncementGroup Access: attributes(${accessControl.attributes}) members(${accessControl.members}) link(${accessControl.addFromInviteLink}) - Members: $membersCount - Pending: $pendingMembersCount - Requesting: $requestingMembersCount - Banned: $bannedMembersCount + Members: $members + Pending: $pending + Requesting: $requesting + Banned: $banned """.trimIndent().replace("\n", "
") } + +private fun describeList(list: List, getUuid: (T) -> ByteString): String { + return if (list.isNotEmpty() && list.size < 10) { + var pendingMembers = "${list.size}\n" + list.forEachIndexed { i, pendingMember -> + pendingMembers += " ${UuidUtil.fromByteString(getUuid(pendingMember))}" + if (i != list.lastIndex) { + pendingMembers += "\n" + } + } + pendingMembers + } else { + list.size.toString() + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt index ae1485bbca..7431955f97 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt @@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.ServiceId import java.util.Optional +import java.util.UUID fun DecryptedGroupChange.Builder.setNewDescription(description: String) { newDescription = DecryptedString.newBuilder().setValue(description).build() @@ -107,6 +108,7 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou var requestedRevision: Int = 0 fun localState( + active: Boolean = true, revision: Int = 0, title: String = "", avatar: String = "", @@ -116,10 +118,11 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou pendingMembers: List = emptyList(), requestingMembers: List = emptyList(), inviteLinkPassword: ByteArray = ByteArray(0), - disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance() + disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance(), + serviceId: String = ServiceId.from(UUID.randomUUID()).toString() ) { localState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer) - groupRecord = groupRecord(masterKey, localState!!) + groupRecord = groupRecord(masterKey, localState!!, active = active, serviceId = serviceId) } fun serverState( @@ -170,7 +173,8 @@ fun groupRecord( active: Boolean = true, avatarDigest: ByteArray = ByteArray(0), mms: Boolean = false, - distributionId: DistributionId? = null + distributionId: DistributionId? = null, + serviceId: String = ServiceId.from(UUID.randomUUID()).toString() ): Optional { return Optional.of( GroupDatabase.GroupRecord( @@ -189,7 +193,8 @@ fun groupRecord( masterKey.serialize(), decryptedGroup.revision, decryptedGroup.toByteArray(), - distributionId + distributionId, + serviceId ) ) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index 786c4ced5c..716b490940 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -24,8 +24,11 @@ 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.DecryptedPendingMember; +import org.thoughtcrime.securesms.keyvalue.ServiceIds; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -44,9 +47,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.signal.core.util.StringUtil.isolateBidi; import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy; import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown; -import static org.signal.core.util.StringUtil.isolateBidi; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -83,7 +86,7 @@ public final class GroupsV2UpdateMessageProducerTest { Recipient aliceRecipient = recipientWithName(aliceId, "Alice"); Recipient bobRecipient = recipientWithName(bobId, "Bob"); - producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), you, null); + producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), new ServiceIds(ACI.from(you), PNI.from(UUID.randomUUID())), null); recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(alice), null)).thenReturn(aliceId); recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(bob), null)).thenReturn(bobId); diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt index 373742aace..ff855c7570 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/GroupManagerV2Test_edit.kt @@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.ServiceId import java.util.UUID @@ -53,6 +54,7 @@ class GroupManagerV2Test_edit { val groupId: GroupId.V2 = GroupId.v2(masterKey) val selfAci: ACI = ACI.from(UUID.randomUUID()) + val selfPni: PNI = PNI.from(UUID.randomUUID()) val otherSid: ServiceId = ServiceId.from(UUID.randomUUID()) val selfAndOthers: List = listOf(member(selfAci), member(otherSid)) val others: List = listOf(member(otherSid)) @@ -102,6 +104,7 @@ class GroupManagerV2Test_edit { groupsV2Authorization, groupsV2StateProcessor, selfAci, + selfPni, groupCandidateHelper, sendGroupUpdateHelper ) @@ -114,7 +117,7 @@ class GroupManagerV2Test_edit { Mockito.doReturn(data.groupRecord).`when`(groupDatabase).getGroup(groupId) Mockito.doReturn(data.groupRecord.get()).`when`(groupDatabase).requireGroup(groupId) - Mockito.doReturn(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any()) + Mockito.doReturn(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any(), Mockito.anyBoolean()) Mockito.doReturn(data.groupChange!!).`when`(groupsV2API).patchGroup(Mockito.any(), Mockito.any(), Mockito.any()) } @@ -136,7 +139,8 @@ class GroupManagerV2Test_edit { members = listOf( member(selfAci, role = Member.Role.ADMINISTRATOR), member(otherSid) - ) + ), + serviceId = selfAci.toString() ) groupChange(6) { source(selfAci) diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt index 6360ece239..06cd39f629 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt @@ -51,11 +51,11 @@ import java.util.UUID class GroupsV2StateProcessorTest { companion object { - val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - val selfAci: ACI = ACI.from(UUID.randomUUID()) - val otherSid: ServiceId = ServiceId.from(UUID.randomUUID()) - val selfAndOthers: List = listOf(member(selfAci), member(otherSid)) - val others: List = listOf(member(otherSid)) + private val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + private val selfAci: ACI = ACI.from(UUID.randomUUID()) + private val otherSid: ServiceId = ServiceId.from(UUID.randomUUID()) + private val selfAndOthers: List = listOf(member(selfAci), member(otherSid)) + private val others: List = listOf(member(otherSid)) } private lateinit var groupDatabase: GroupDatabase @@ -89,8 +89,7 @@ class GroupsV2StateProcessorTest { } private fun given(init: GroupStateTestData.() -> Unit) { - val data = GroupStateTestData(masterKey) - data.init() + val data = givenData(init) doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java)) doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any()) @@ -109,6 +108,12 @@ class GroupsV2StateProcessorTest { } } + private fun givenData(init: GroupStateTestData.() -> Unit): GroupStateTestData { + val data = GroupStateTestData(masterKey) + data.init() + return data + } + @Test fun `when local revision matches server revision, then return consistent or ahead`() { given { diff --git a/app/src/testShared/org/thoughtcrime/securesms/database/model/databaseprotos/DecryptedGroupHelper.kt b/app/src/testShared/org/thoughtcrime/securesms/database/model/databaseprotos/DecryptedGroupHelper.kt index c65e61c8c2..29ff559c15 100644 --- a/app/src/testShared/org/thoughtcrime/securesms/database/model/databaseprotos/DecryptedGroupHelper.kt +++ b/app/src/testShared/org/thoughtcrime/securesms/database/model/databaseprotos/DecryptedGroupHelper.kt @@ -5,6 +5,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.signal.storageservice.protos.groups.Member import org.signal.storageservice.protos.groups.local.DecryptedGroupChange import org.signal.storageservice.protos.groups.local.DecryptedMember +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.util.UuidUtil @@ -62,3 +63,9 @@ fun requestingMember(serviceId: ServiceId): DecryptedRequestingMember { .setUuid(serviceId.toByteString()) .build() } + +fun pendingMember(serviceId: ServiceId): DecryptedPendingMember { + return DecryptedPendingMember.newBuilder() + .setUuid(serviceId.toByteString()) + .build() +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java index 14c33480c5..9510ecef5e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.groupsv2; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential; import java.util.ArrayList; @@ -37,6 +38,13 @@ public final class GroupCandidate { return profileKeyCredential; } + public ProfileKeyCredential requireProfileKeyCredential() { + if (profileKeyCredential.isPresent()) { + return profileKeyCredential.get(); + } + throw new IllegalStateException("no profile key credential"); + } + public boolean hasProfileKeyCredential() { return profileKeyCredential.isPresent(); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java index 3da639fcdb..0ffa9cc198 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -20,7 +20,7 @@ import org.signal.storageservice.protos.groups.GroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; -import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException; @@ -44,16 +44,16 @@ public class GroupsV2Api { /** * Provides 7 days of credentials, which you should cache. */ - public HashMap getCredentials(int today) + public HashMap getCredentials(int today, boolean isAci) throws IOException { - return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today)); + return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today, isAci)); } /** * Create an auth token from a credential response. */ - public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ACI self, + public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ServiceId self, int today, GroupSecretParams groupSecretParams, AuthCredentialResponse authCredentialResponse) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 2962fddb17..3980c979ea 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -76,6 +76,7 @@ public final class SignalServiceContent { private final SignalServiceContentProto serializedState; private final String serverUuid; private final Optional groupId; + private final String destinationUuid; private final Optional message; private final Optional synchronizeMessage; @@ -96,6 +97,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -106,6 +108,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.ofNullable(message); @@ -128,6 +131,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -138,6 +142,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -160,6 +165,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -170,6 +176,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -192,6 +199,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -202,6 +210,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -224,6 +233,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -234,6 +244,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -256,6 +267,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -266,6 +278,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -287,6 +300,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -297,6 +311,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -318,6 +333,7 @@ public final class SignalServiceContent { boolean needsReceipt, String serverUuid, Optional groupId, + String destinationUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -328,6 +344,7 @@ public final class SignalServiceContent { this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; this.groupId = groupId; + this.destinationUuid = destinationUuid; this.serializedState = serializedState; this.message = Optional.empty(); @@ -404,6 +421,10 @@ public final class SignalServiceContent { return groupId; } + public String getDestinationUuid() { + return destinationUuid; + } + public byte[] serialize() { return serializedState.toByteArray(); } @@ -443,6 +464,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { SignalServiceProtos.Content message = serviceContentProto.getContent(); @@ -467,6 +489,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()), @@ -479,6 +502,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasCallMessage()) { return new SignalServiceContent(createCallMessage(message.getCallMessage()), @@ -491,6 +515,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasReceiptMessage()) { return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()), @@ -503,6 +528,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasTypingMessage()) { return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()), @@ -515,6 +541,7 @@ public final class SignalServiceContent { false, metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasDecryptionErrorMessage()) { return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()), @@ -527,6 +554,7 @@ public final class SignalServiceContent { metadata.isNeedsReceipt(), metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (senderKeyDistributionMessage.isPresent()) { return new SignalServiceContent(senderKeyDistributionMessage.get(), @@ -538,6 +566,7 @@ public final class SignalServiceContent { false, metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } else if (message.hasStoryMessage()) { return new SignalServiceContent(createStoryMessage(message.getStoryMessage()), @@ -549,6 +578,7 @@ public final class SignalServiceContent { false, metadata.getServerGuid(), metadata.getGroupId(), + metadata.getDestinationUuid(), serviceContentProto); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java index 369cc0919c..2d70049023 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/ACI.java @@ -36,23 +36,4 @@ public final class ACI extends ServiceId { public byte[] toByteArray() { return UuidUtil.toByteArray(uuid); } - - @Override - public boolean equals(Object other) { - if (other instanceof ServiceId) { - return uuid.equals(((ServiceId) other).uuid); - } else { - return false; - } - } - - @Override - public int hashCode() { - return uuid.hashCode(); - } - - @Override - public String toString() { - return uuid.toString(); - } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java index 5a8504ce27..bb565789c5 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/PNI.java @@ -26,23 +26,4 @@ public final class PNI extends ServiceId { private PNI(UUID uuid) { super(uuid); } - - @Override - public int hashCode() { - return uuid.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (other instanceof PNI) { - return uuid.equals(((PNI) other).uuid); - } else { - return false; - } - } - - @Override - public String toString() { - return uuid.toString(); - } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index d74fbcddfe..e35e487d3a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -230,7 +230,7 @@ public class PushServiceSocket { private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto"; private static final String STICKER_PATH = "stickers/%s/full/%d"; - private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d"; + private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d?identity=%s"; private static final String GROUPSV2_GROUP = "/v1/groups/"; private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s"; private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false"; @@ -2310,11 +2310,11 @@ public class PushServiceSocket { public enum ClientSet { ContactDiscovery, KeyBackup } - public CredentialResponse retrieveGroupsV2Credentials(int today) + public CredentialResponse retrieveGroupsV2Credentials(int today, boolean isAci) throws IOException { int todayPlus7 = today + 7; - String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7), + String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7, isAci ? "aci" : "pni"), "GET", null, NO_HEADERS, diff --git a/spinner/lib/src/main/assets/partials/prefix.hbs b/spinner/lib/src/main/assets/partials/prefix.hbs index 6f0f2d3748..728a2c698b 100644 --- a/spinner/lib/src/main/assets/partials/prefix.hbs +++ b/spinner/lib/src/main/assets/partials/prefix.hbs @@ -1,4 +1,4 @@ -

SPINNER

+

SPINNER - {{environment}}

{{#each deviceInfo}} diff --git a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt index 77ff6e485e..0c2420c8e1 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt @@ -11,7 +11,10 @@ import java.io.IOException * A class to help initialize Spinner, our database debugging interface. */ object Spinner { - val TAG: String = Log.tag(Spinner::class.java) + internal const val KEY_PREFIX = "spinner" + const val KEY_ENVIRONMENT = "$KEY_PREFIX:environment" + + private val TAG: String = Log.tag(Spinner::class.java) private lateinit var server: SpinnerServer diff --git a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt index f81177d3b2..6594c6cb71 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt @@ -28,7 +28,7 @@ import kotlin.math.min */ internal class SpinnerServer( private val application: Application, - private val deviceInfo: Map, + deviceInfo: Map, private val databases: Map ) : NanoHTTPD(5000) { @@ -36,6 +36,9 @@ internal class SpinnerServer( private val TAG = Log.tag(SpinnerServer::class.java) } + private val deviceInfo: Map = deviceInfo.filterKeys { !it.startsWith(Spinner.KEY_PREFIX) } + private val environment: String = deviceInfo[Spinner.KEY_ENVIRONMENT] ?: "UNKNOWN" + private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(application)).apply { registerHelper("eq", ConditionalHelpers.eq) registerHelper("neq", ConditionalHelpers.neq) @@ -86,6 +89,7 @@ internal class SpinnerServer( return renderTemplate( "overview", OverviewPageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -101,6 +105,7 @@ internal class SpinnerServer( return renderTemplate( "browse", BrowsePageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -131,6 +136,7 @@ internal class SpinnerServer( return renderTemplate( "browse", BrowsePageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -153,6 +159,7 @@ internal class SpinnerServer( return renderTemplate( "query", QueryPageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -173,6 +180,7 @@ internal class SpinnerServer( return renderTemplate( "recent", RecentPageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -190,6 +198,7 @@ internal class SpinnerServer( return renderTemplate( "query", QueryPageModel( + environment = environment, deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), @@ -343,40 +352,51 @@ internal class SpinnerServer( return params[name] } + interface PrefixPageData { + val environment: String + val deviceInfo: Map + val database: String + val databases: List + } + data class OverviewPageModel( - val deviceInfo: Map, - val database: String, - val databases: List, + override val environment: String, + override val deviceInfo: Map, + override val database: String, + override val databases: List, val tables: List, val indices: List, val triggers: List, val queryResult: QueryResult? = null - ) + ) : PrefixPageData data class BrowsePageModel( - val deviceInfo: Map, - val database: String, - val databases: List, + override val environment: String, + override val deviceInfo: Map, + override val database: String, + override val databases: List, val tableNames: List, val table: String? = null, val queryResult: QueryResult? = null, val pagingData: PagingData? = null, - ) + ) : PrefixPageData data class QueryPageModel( - val deviceInfo: Map, - val database: String, - val databases: List, + override val environment: String, + override val deviceInfo: Map, + override val database: String, + override val databases: List, val query: String = "", val queryResult: QueryResult? = null - ) + ) : PrefixPageData data class RecentPageModel( - val deviceInfo: Map, - val database: String, - val databases: List, + override val environment: String, + override val deviceInfo: Map, + override val database: String, + override val databases: List, val recentSql: List? - ) + ) : PrefixPageData data class QueryResult( val columns: List,