mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 02:08:40 +00: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());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.AppSignatureUtil
|
||||
import shark.AndroidReferenceMatchers
|
||||
import java.util.Locale
|
||||
|
||||
class SpinnerApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
@@ -36,7 +37,8 @@ class SpinnerApplicationContext : ApplicationContext() {
|
||||
"Profile Name" to (if (SignalStore.account().isRegistered) Recipient.self().profileName.toString() else "none"),
|
||||
"E164" to (SignalStore.account().e164 ?: "none"),
|
||||
"ACI" to (SignalStore.account().aci?.toString() ?: "none"),
|
||||
"PNI" to (SignalStore.account().pni?.toString() ?: "none")
|
||||
"PNI" to (SignalStore.account().pni?.toString() ?: "none"),
|
||||
Spinner.KEY_ENVIRONMENT to BuildConfig.FLAVOR_environment.toUpperCase(Locale.US)
|
||||
),
|
||||
linkedMapOf(
|
||||
"signal" to DatabaseConfig(
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.Cursor
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.spinner.ColumnTransformer
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
|
||||
object GV2Transformer : ColumnTransformer {
|
||||
override fun matches(tableName: String?, columnName: String): Boolean {
|
||||
@@ -24,6 +30,11 @@ object GV2Transformer : ColumnTransformer {
|
||||
}
|
||||
|
||||
private fun DecryptedGroup.formatAsHtml(): String {
|
||||
val members: String = describeList(membersList, DecryptedMember::getUuid)
|
||||
val pending: String = describeList(pendingMembersList, DecryptedPendingMember::getUuid)
|
||||
val requesting: String = describeList(requestingMembersList, DecryptedRequestingMember::getUuid)
|
||||
val banned: String = describeList(bannedMembersList, DecryptedBannedMember::getUuid)
|
||||
|
||||
return """
|
||||
Revision: $revision
|
||||
Title: $title
|
||||
@@ -32,9 +43,24 @@ private fun DecryptedGroup.formatAsHtml(): String {
|
||||
Description: "$description"
|
||||
Announcement: $isAnnouncementGroup
|
||||
Access: attributes(${accessControl.attributes}) members(${accessControl.members}) link(${accessControl.addFromInviteLink})
|
||||
Members: $membersCount
|
||||
Pending: $pendingMembersCount
|
||||
Requesting: $requestingMembersCount
|
||||
Banned: $bannedMembersCount
|
||||
Members: $members
|
||||
Pending: $pending
|
||||
Requesting: $requesting
|
||||
Banned: $banned
|
||||
""".trimIndent().replace("\n", "<br>")
|
||||
}
|
||||
|
||||
private fun <T> describeList(list: List<T>, getUuid: (T) -> ByteString): String {
|
||||
return if (list.isNotEmpty() && list.size < 10) {
|
||||
var pendingMembers = "${list.size}\n"
|
||||
list.forEachIndexed { i, pendingMember ->
|
||||
pendingMembers += " ${UuidUtil.fromByteString(getUuid(pendingMember))}"
|
||||
if (i != list.lastIndex) {
|
||||
pendingMembers += "\n"
|
||||
}
|
||||
}
|
||||
pendingMembers
|
||||
} else {
|
||||
list.size.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
fun DecryptedGroupChange.Builder.setNewDescription(description: String) {
|
||||
newDescription = DecryptedString.newBuilder().setValue(description).build()
|
||||
@@ -107,6 +108,7 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou
|
||||
var requestedRevision: Int = 0
|
||||
|
||||
fun localState(
|
||||
active: Boolean = true,
|
||||
revision: Int = 0,
|
||||
title: String = "",
|
||||
avatar: String = "",
|
||||
@@ -116,10 +118,11 @@ class GroupStateTestData(private val masterKey: GroupMasterKey, private val grou
|
||||
pendingMembers: List<DecryptedPendingMember> = emptyList(),
|
||||
requestingMembers: List<DecryptedRequestingMember> = emptyList(),
|
||||
inviteLinkPassword: ByteArray = ByteArray(0),
|
||||
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance()
|
||||
disappearingMessageTimer: DecryptedTimer = DecryptedTimer.getDefaultInstance(),
|
||||
serviceId: String = ServiceId.from(UUID.randomUUID()).toString()
|
||||
) {
|
||||
localState = decryptedGroup(revision, title, avatar, description, accessControl, members, pendingMembers, requestingMembers, inviteLinkPassword, disappearingMessageTimer)
|
||||
groupRecord = groupRecord(masterKey, localState!!)
|
||||
groupRecord = groupRecord(masterKey, localState!!, active = active, serviceId = serviceId)
|
||||
}
|
||||
|
||||
fun serverState(
|
||||
@@ -170,7 +173,8 @@ fun groupRecord(
|
||||
active: Boolean = true,
|
||||
avatarDigest: ByteArray = ByteArray(0),
|
||||
mms: Boolean = false,
|
||||
distributionId: DistributionId? = null
|
||||
distributionId: DistributionId? = null,
|
||||
serviceId: String = ServiceId.from(UUID.randomUUID()).toString()
|
||||
): Optional<GroupDatabase.GroupRecord> {
|
||||
return Optional.of(
|
||||
GroupDatabase.GroupRecord(
|
||||
@@ -189,7 +193,8 @@ fun groupRecord(
|
||||
masterKey.serialize(),
|
||||
decryptedGroup.revision,
|
||||
decryptedGroup.toByteArray(),
|
||||
distributionId
|
||||
distributionId,
|
||||
serviceId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,11 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.thoughtcrime.securesms.keyvalue.ServiceIds;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
@@ -44,9 +47,9 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.signal.core.util.StringUtil.isolateBidi;
|
||||
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeBy;
|
||||
import static org.thoughtcrime.securesms.groups.v2.ChangeBuilder.changeByUnknown;
|
||||
import static org.signal.core.util.StringUtil.isolateBidi;
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
@@ -83,7 +86,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
||||
Recipient aliceRecipient = recipientWithName(aliceId, "Alice");
|
||||
Recipient bobRecipient = recipientWithName(bobId, "Bob");
|
||||
|
||||
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), you, null);
|
||||
producer = new GroupsV2UpdateMessageProducer(ApplicationProvider.getApplicationContext(), new ServiceIds(ACI.from(you), PNI.from(UUID.randomUUID())), null);
|
||||
|
||||
recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(alice), null)).thenReturn(aliceId);
|
||||
recipientIdMockedStatic.when(() -> RecipientId.from(ServiceId.from(bob), null)).thenReturn(bobId);
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@@ -53,6 +54,7 @@ class GroupManagerV2Test_edit {
|
||||
val groupId: GroupId.V2 = GroupId.v2(masterKey)
|
||||
|
||||
val selfAci: ACI = ACI.from(UUID.randomUUID())
|
||||
val selfPni: PNI = PNI.from(UUID.randomUUID())
|
||||
val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
|
||||
val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
|
||||
val others: List<DecryptedMember> = listOf(member(otherSid))
|
||||
@@ -102,6 +104,7 @@ class GroupManagerV2Test_edit {
|
||||
groupsV2Authorization,
|
||||
groupsV2StateProcessor,
|
||||
selfAci,
|
||||
selfPni,
|
||||
groupCandidateHelper,
|
||||
sendGroupUpdateHelper
|
||||
)
|
||||
@@ -114,7 +117,7 @@ class GroupManagerV2Test_edit {
|
||||
Mockito.doReturn(data.groupRecord).`when`(groupDatabase).getGroup(groupId)
|
||||
Mockito.doReturn(data.groupRecord.get()).`when`(groupDatabase).requireGroup(groupId)
|
||||
|
||||
Mockito.doReturn(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any())
|
||||
Mockito.doReturn(GroupManagerV2.RecipientAndThread(Recipient.UNKNOWN, 1)).`when`(sendGroupUpdateHelper).sendGroupUpdate(Mockito.eq(masterKey), Mockito.any(), Mockito.any(), Mockito.anyBoolean())
|
||||
|
||||
Mockito.doReturn(data.groupChange!!).`when`(groupsV2API).patchGroup(Mockito.any(), Mockito.any(), Mockito.any())
|
||||
}
|
||||
@@ -136,7 +139,8 @@ class GroupManagerV2Test_edit {
|
||||
members = listOf(
|
||||
member(selfAci, role = Member.Role.ADMINISTRATOR),
|
||||
member(otherSid)
|
||||
)
|
||||
),
|
||||
serviceId = selfAci.toString()
|
||||
)
|
||||
groupChange(6) {
|
||||
source(selfAci)
|
||||
|
||||
@@ -51,11 +51,11 @@ import java.util.UUID
|
||||
class GroupsV2StateProcessorTest {
|
||||
|
||||
companion object {
|
||||
val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
val selfAci: ACI = ACI.from(UUID.randomUUID())
|
||||
val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
|
||||
val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
|
||||
val others: List<DecryptedMember> = listOf(member(otherSid))
|
||||
private val masterKey = GroupMasterKey(fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val selfAci: ACI = ACI.from(UUID.randomUUID())
|
||||
private val otherSid: ServiceId = ServiceId.from(UUID.randomUUID())
|
||||
private val selfAndOthers: List<DecryptedMember> = listOf(member(selfAci), member(otherSid))
|
||||
private val others: List<DecryptedMember> = listOf(member(otherSid))
|
||||
}
|
||||
|
||||
private lateinit var groupDatabase: GroupDatabase
|
||||
@@ -89,8 +89,7 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
|
||||
private fun given(init: GroupStateTestData.() -> Unit) {
|
||||
val data = GroupStateTestData(masterKey)
|
||||
data.init()
|
||||
val data = givenData(init)
|
||||
|
||||
doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java))
|
||||
doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any())
|
||||
@@ -109,6 +108,12 @@ class GroupsV2StateProcessorTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun givenData(init: GroupStateTestData.() -> Unit): GroupStateTestData {
|
||||
val data = GroupStateTestData(masterKey)
|
||||
data.init()
|
||||
return data
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when local revision matches server revision, then return consistent or ahead`() {
|
||||
given {
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
@@ -62,3 +63,9 @@ fun requestingMember(serviceId: ServiceId): DecryptedRequestingMember {
|
||||
.setUuid(serviceId.toByteString())
|
||||
.build()
|
||||
}
|
||||
|
||||
fun pendingMember(serviceId: ServiceId): DecryptedPendingMember {
|
||||
return DecryptedPendingMember.newBuilder()
|
||||
.setUuid(serviceId.toByteString())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -37,6 +38,13 @@ public final class GroupCandidate {
|
||||
return profileKeyCredential;
|
||||
}
|
||||
|
||||
public ProfileKeyCredential requireProfileKeyCredential() {
|
||||
if (profileKeyCredential.isPresent()) {
|
||||
return profileKeyCredential.get();
|
||||
}
|
||||
throw new IllegalStateException("no profile key credential");
|
||||
}
|
||||
|
||||
public boolean hasProfileKeyCredential() {
|
||||
return profileKeyCredential.isPresent();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.signal.storageservice.protos.groups.GroupJoinInfo;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
|
||||
|
||||
@@ -44,16 +44,16 @@ public class GroupsV2Api {
|
||||
/**
|
||||
* Provides 7 days of credentials, which you should cache.
|
||||
*/
|
||||
public HashMap<Integer, AuthCredentialResponse> getCredentials(int today)
|
||||
public HashMap<Integer, AuthCredentialResponse> getCredentials(int today, boolean isAci)
|
||||
throws IOException
|
||||
{
|
||||
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today));
|
||||
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(today, isAci));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an auth token from a credential response.
|
||||
*/
|
||||
public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ACI self,
|
||||
public GroupsV2AuthorizationString getGroupsV2AuthorizationString(ServiceId self,
|
||||
int today,
|
||||
GroupSecretParams groupSecretParams,
|
||||
AuthCredentialResponse authCredentialResponse)
|
||||
|
||||
@@ -76,6 +76,7 @@ public final class SignalServiceContent {
|
||||
private final SignalServiceContentProto serializedState;
|
||||
private final String serverUuid;
|
||||
private final Optional<byte[]> groupId;
|
||||
private final String destinationUuid;
|
||||
|
||||
private final Optional<SignalServiceDataMessage> message;
|
||||
private final Optional<SignalServiceSyncMessage> synchronizeMessage;
|
||||
@@ -96,6 +97,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -106,6 +108,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.ofNullable(message);
|
||||
@@ -128,6 +131,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -138,6 +142,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -160,6 +165,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -170,6 +176,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -192,6 +199,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -202,6 +210,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -224,6 +233,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -234,6 +244,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -256,6 +267,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -266,6 +278,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -287,6 +300,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -297,6 +311,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -318,6 +333,7 @@ public final class SignalServiceContent {
|
||||
boolean needsReceipt,
|
||||
String serverUuid,
|
||||
Optional<byte[]> groupId,
|
||||
String destinationUuid,
|
||||
SignalServiceContentProto serializedState)
|
||||
{
|
||||
this.sender = sender;
|
||||
@@ -328,6 +344,7 @@ public final class SignalServiceContent {
|
||||
this.needsReceipt = needsReceipt;
|
||||
this.serverUuid = serverUuid;
|
||||
this.groupId = groupId;
|
||||
this.destinationUuid = destinationUuid;
|
||||
this.serializedState = serializedState;
|
||||
|
||||
this.message = Optional.empty();
|
||||
@@ -404,6 +421,10 @@ public final class SignalServiceContent {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public String getDestinationUuid() {
|
||||
return destinationUuid;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return serializedState.toByteArray();
|
||||
}
|
||||
@@ -443,6 +464,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) {
|
||||
SignalServiceProtos.Content message = serviceContentProto.getContent();
|
||||
@@ -467,6 +489,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) {
|
||||
return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()),
|
||||
@@ -479,6 +502,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasCallMessage()) {
|
||||
return new SignalServiceContent(createCallMessage(message.getCallMessage()),
|
||||
@@ -491,6 +515,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasReceiptMessage()) {
|
||||
return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()),
|
||||
@@ -503,6 +528,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasTypingMessage()) {
|
||||
return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()),
|
||||
@@ -515,6 +541,7 @@ public final class SignalServiceContent {
|
||||
false,
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasDecryptionErrorMessage()) {
|
||||
return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()),
|
||||
@@ -527,6 +554,7 @@ public final class SignalServiceContent {
|
||||
metadata.isNeedsReceipt(),
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (senderKeyDistributionMessage.isPresent()) {
|
||||
return new SignalServiceContent(senderKeyDistributionMessage.get(),
|
||||
@@ -538,6 +566,7 @@ public final class SignalServiceContent {
|
||||
false,
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
} else if (message.hasStoryMessage()) {
|
||||
return new SignalServiceContent(createStoryMessage(message.getStoryMessage()),
|
||||
@@ -549,6 +578,7 @@ public final class SignalServiceContent {
|
||||
false,
|
||||
metadata.getServerGuid(),
|
||||
metadata.getGroupId(),
|
||||
metadata.getDestinationUuid(),
|
||||
serviceContentProto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,23 +36,4 @@ public final class ACI extends ServiceId {
|
||||
public byte[] toByteArray() {
|
||||
return UuidUtil.toByteArray(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof ServiceId) {
|
||||
return uuid.equals(((ServiceId) other).uuid);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,23 +26,4 @@ public final class PNI extends ServiceId {
|
||||
private PNI(UUID uuid) {
|
||||
super(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof PNI) {
|
||||
return uuid.equals(((PNI) other).uuid);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ public class PushServiceSocket {
|
||||
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
||||
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
|
||||
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d?identity=%s";
|
||||
private static final String GROUPSV2_GROUP = "/v1/groups/";
|
||||
private static final String GROUPSV2_GROUP_PASSWORD = "/v1/groups/?inviteLinkPassword=%s";
|
||||
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
|
||||
@@ -2310,11 +2310,11 @@ public class PushServiceSocket {
|
||||
|
||||
public enum ClientSet { ContactDiscovery, KeyBackup }
|
||||
|
||||
public CredentialResponse retrieveGroupsV2Credentials(int today)
|
||||
public CredentialResponse retrieveGroupsV2Credentials(int today, boolean isAci)
|
||||
throws IOException
|
||||
{
|
||||
int todayPlus7 = today + 7;
|
||||
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7),
|
||||
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7, isAci ? "aci" : "pni"),
|
||||
"GET",
|
||||
null,
|
||||
NO_HEADERS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1>SPINNER</h1>
|
||||
<h1>SPINNER - {{environment}}</h1>
|
||||
|
||||
<table class="device-info">
|
||||
{{#each deviceInfo}}
|
||||
|
||||
@@ -11,7 +11,10 @@ import java.io.IOException
|
||||
* A class to help initialize Spinner, our database debugging interface.
|
||||
*/
|
||||
object Spinner {
|
||||
val TAG: String = Log.tag(Spinner::class.java)
|
||||
internal const val KEY_PREFIX = "spinner"
|
||||
const val KEY_ENVIRONMENT = "$KEY_PREFIX:environment"
|
||||
|
||||
private val TAG: String = Log.tag(Spinner::class.java)
|
||||
|
||||
private lateinit var server: SpinnerServer
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import kotlin.math.min
|
||||
*/
|
||||
internal class SpinnerServer(
|
||||
private val application: Application,
|
||||
private val deviceInfo: Map<String, String>,
|
||||
deviceInfo: Map<String, String>,
|
||||
private val databases: Map<String, DatabaseConfig>
|
||||
) : NanoHTTPD(5000) {
|
||||
|
||||
@@ -36,6 +36,9 @@ internal class SpinnerServer(
|
||||
private val TAG = Log.tag(SpinnerServer::class.java)
|
||||
}
|
||||
|
||||
private val deviceInfo: Map<String, String> = deviceInfo.filterKeys { !it.startsWith(Spinner.KEY_PREFIX) }
|
||||
private val environment: String = deviceInfo[Spinner.KEY_ENVIRONMENT] ?: "UNKNOWN"
|
||||
|
||||
private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(application)).apply {
|
||||
registerHelper("eq", ConditionalHelpers.eq)
|
||||
registerHelper("neq", ConditionalHelpers.neq)
|
||||
@@ -86,6 +89,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"overview",
|
||||
OverviewPageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -101,6 +105,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"browse",
|
||||
BrowsePageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -131,6 +136,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"browse",
|
||||
BrowsePageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -153,6 +159,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"query",
|
||||
QueryPageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -173,6 +180,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"recent",
|
||||
RecentPageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -190,6 +198,7 @@ internal class SpinnerServer(
|
||||
return renderTemplate(
|
||||
"query",
|
||||
QueryPageModel(
|
||||
environment = environment,
|
||||
deviceInfo = deviceInfo,
|
||||
database = dbName,
|
||||
databases = databases.keys.toList(),
|
||||
@@ -343,40 +352,51 @@ internal class SpinnerServer(
|
||||
return params[name]
|
||||
}
|
||||
|
||||
interface PrefixPageData {
|
||||
val environment: String
|
||||
val deviceInfo: Map<String, String>
|
||||
val database: String
|
||||
val databases: List<String>
|
||||
}
|
||||
|
||||
data class OverviewPageModel(
|
||||
val deviceInfo: Map<String, String>,
|
||||
val database: String,
|
||||
val databases: List<String>,
|
||||
override val environment: String,
|
||||
override val deviceInfo: Map<String, String>,
|
||||
override val database: String,
|
||||
override val databases: List<String>,
|
||||
val tables: List<TableInfo>,
|
||||
val indices: List<IndexInfo>,
|
||||
val triggers: List<TriggerInfo>,
|
||||
val queryResult: QueryResult? = null
|
||||
)
|
||||
) : PrefixPageData
|
||||
|
||||
data class BrowsePageModel(
|
||||
val deviceInfo: Map<String, String>,
|
||||
val database: String,
|
||||
val databases: List<String>,
|
||||
override val environment: String,
|
||||
override val deviceInfo: Map<String, String>,
|
||||
override val database: String,
|
||||
override val databases: List<String>,
|
||||
val tableNames: List<String>,
|
||||
val table: String? = null,
|
||||
val queryResult: QueryResult? = null,
|
||||
val pagingData: PagingData? = null,
|
||||
)
|
||||
) : PrefixPageData
|
||||
|
||||
data class QueryPageModel(
|
||||
val deviceInfo: Map<String, String>,
|
||||
val database: String,
|
||||
val databases: List<String>,
|
||||
override val environment: String,
|
||||
override val deviceInfo: Map<String, String>,
|
||||
override val database: String,
|
||||
override val databases: List<String>,
|
||||
val query: String = "",
|
||||
val queryResult: QueryResult? = null
|
||||
)
|
||||
) : PrefixPageData
|
||||
|
||||
data class RecentPageModel(
|
||||
val deviceInfo: Map<String, String>,
|
||||
val database: String,
|
||||
val databases: List<String>,
|
||||
override val environment: String,
|
||||
override val deviceInfo: Map<String, String>,
|
||||
override val database: String,
|
||||
override val databases: List<String>,
|
||||
val recentSql: List<RecentQuery>?
|
||||
)
|
||||
) : PrefixPageData
|
||||
|
||||
data class QueryResult(
|
||||
val columns: List<String>,
|
||||
|
||||
Reference in New Issue
Block a user