mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 03:40:56 +01:00
Add ability to reject group invite by PNI.
This commit is contained in:
@@ -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, _ ->
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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<RecipientId> = recipients.map { it.id }.toSet()
|
||||
val inputE164s: Set<String> = 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(),
|
||||
|
||||
@@ -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<String> 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<RecipientId> 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<RecipientId> 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> serviceId = recipient.getServiceId();
|
||||
|
||||
public MemberLevel memberLevel(@NonNull Optional<ServiceId> serviceId) {
|
||||
if (!serviceId.isPresent()) {
|
||||
return MemberLevel.NOT_A_MEMBER;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<RecipientId> recipientClickHandler;
|
||||
|
||||
GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull UUID selfUuid, @Nullable Consumer<RecipientId> recipientClickHandler) {
|
||||
GroupsV2UpdateMessageProducer(@NonNull Context context, @NonNull ServiceIds selfIds, @Nullable Consumer<RecipientId> 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<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
|
||||
Optional<DecryptedPendingMember> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> 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<ByteString> 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<UpdateDescription> 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<UpdateDescription> 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<UpdateDescription> updates) {
|
||||
Set<ByteString> newRequestingUuids = change.getNewRequestingMembersList().stream().map(r -> r.getUuid()).collect(Collectors.toSet());
|
||||
Set<ByteString> 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<UpdateDescription> 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<UpdateDescription> 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<RecipientId> recipientIds, @Nullable List<Object> formatArgs) {
|
||||
List<String> placeholders = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList());
|
||||
List<Object> args = new LinkedList<>(placeholders);
|
||||
List<Object> args = recipientIds.stream().map(GroupsV2UpdateMessageProducer::makePlaceholder).collect(Collectors.toList());
|
||||
|
||||
if (formatArgs != null) {
|
||||
args.addAll(formatArgs);
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Recipient> 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<UuidCiphertext> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RecipientId> members,
|
||||
@NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId,
|
||||
@NonNull Collection<RecipientId> 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<UuidCiphertext> uuidCipherTexts)
|
||||
@NonNull GroupManager.GroupActionResult revokeInvites(@NonNull ServiceId authServiceId, @NonNull Collection<UuidCiphertext> 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<DecryptedPendingMember> pendingMembersList = groupRecord.requireV2GroupProperties().getDecryptedGroup().getPendingMembersList();
|
||||
Optional<DecryptedPendingMember> selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
|
||||
GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
Optional<DecryptedMember> selfMember = DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> aciPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> pniPendingMember = DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), selfPni.uuid());
|
||||
Optional<DecryptedPendingMember> 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<RecipientId> newAdmins)
|
||||
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
|
||||
{
|
||||
Recipient self = Recipient.self();
|
||||
List<UUID> 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<UUID> 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<RecipientId> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Integer, AuthCredentialResponse> 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<Integer, AuthCredentialResponse> 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 {
|
||||
|
||||
@@ -58,7 +58,8 @@ final class AddGroupDetailsRepository {
|
||||
Set<Recipient> 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,
|
||||
|
||||
@@ -100,7 +100,7 @@ final class PendingMemberInvitesRepository {
|
||||
@WorkerThread
|
||||
boolean revokeInvites(@NonNull Collection<UuidCiphertext> 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);
|
||||
|
||||
@@ -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<ServerGroupLogEntry> 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<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> selfAsPendingOptional = DecryptedGroupUtil.findPendingByUuid(newLocalState.getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), serviceId.uuid());
|
||||
Optional<DecryptedPendingMember> 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<ServiceId> 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<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), selfAci.uuid());
|
||||
Optional<DecryptedPendingMember> pendingByUuid = DecryptedGroupUtil.findPendingByUuid(decryptedGroupV2Context.getGroupState().getPendingMembersList(), serviceId.uuid());
|
||||
if (pendingByUuid.isPresent()) {
|
||||
return Optional.ofNullable(UuidUtil.fromByteStringOrNull(pendingByUuid.get().getAddedByUuid()));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Integer, AuthCredentialResponse> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user