Cleanup group management code.

This commit is contained in:
Cody Henthorne 2024-05-03 16:11:10 -04:00 committed by Alex Hart
parent 34faa9003f
commit a71faf674d
11 changed files with 131 additions and 859 deletions

View file

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@ -75,21 +74,6 @@ class GroupTableTest {
assertEquals(2, groups.size)
}
@Test
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
insertPushGroup()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.queryGroupsByMembership(
setOf(harness.self.id, harness.others[1]),
includeInactive = false,
excludeV1 = false,
excludeMms = false
)
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
insertPushGroup()
@ -181,68 +165,6 @@ class GroupTableTest {
assertFalse(actual)
}
@Test
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
val v2Group = insertPushGroup()
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
val groupRecord = groupTable.getGroup(v2Group)
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
}
@Test
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
val other = insertMmsGroup(members + listOf(harness.others[1]))
val mmsGroup = insertMmsGroup(members)
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
assertNotEquals(other, actual)
assertEquals(mmsGroup, actual)
}
@Test
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val groupMembers: List<RecipientId> = listOf(harness.self.id)
val group: GroupId = insertMmsGroup(groupMembers)
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
assertEquals(group, groupResult)
}
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(listOf())

View file

@ -60,13 +60,13 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemo
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.ACI.Companion.parseOrNull
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.io.Closeable
import java.security.SecureRandom
import java.util.Optional
import java.util.stream.Collectors
import javax.annotation.CheckReturnValue
import kotlin.math.abs
class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
@ -155,7 +155,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.toList()
//language=sql
private val JOINED_GROUP_SELECT = """
private const val JOINED_GROUP_SELECT = """
SELECT
DISTINCT $TABLE_NAME.*,
(
@ -178,8 +178,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
const val RECIPIENT_ID = "recipient_id"
//language=sql
@JvmField
val CREATE_TABLE = """
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE,
@ -221,7 +220,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.filterNot { (old, new) -> new == null || old == new }
if (oldToNew.isNotEmpty()) {
writableDatabase.withinTransaction { db ->
writableDatabase.withinTransaction {
oldToNew.forEach { remapRecipient(it.first, it.second) }
}
}
@ -261,22 +260,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.run()
}
/**
* @return A gv1 group whose expected v2 ID matches the one provided.
*/
fun getGroupV1ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ?", buildArgs(gv2Id)))
}
/**
* @return A gv1 group whose expected v2 ID matches the one provided or a gv2 group whose ID matches the one provided.
*
* If a gv1 group is present, it will be returned first.
*/
fun getGroupV1OrV2ByExpectedV2(gv2Id: GroupId.V2): Optional<GroupRecord> {
return getGroup(SqlUtil.Query("$TABLE_NAME.$EXPECTED_V2_ID = ? OR $TABLE_NAME.$GROUP_ID = ? ORDER BY $TABLE_NAME.$EXPECTED_V2_ID DESC", buildArgs(gv2Id, gv2Id)))
}
fun getGroupByDistributionId(distributionId: DistributionId): Optional<GroupRecord> {
return getGroup(SqlUtil.Query("$TABLE_NAME.$DISTRIBUTION_ID = ?", buildArgs(distributionId)))
}
@ -295,7 +278,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
* Removes the specified members from the list of 'unmigrated V1 members' -- the list of members
* that were either dropped or had to be invited when migrating the group from V1->V2.
*/
fun removeUnmigratedV1Members(id: GroupId.V2, toRemove: List<RecipientId>) {
private fun removeUnmigratedV1Members(id: GroupId.V2, toRemove: List<RecipientId>) {
val group = getGroup(id)
if (group.isAbsent()) {
Log.w(TAG, "Couldn't find the group!", Throwable())
@ -382,54 +365,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
return Reader(cursor)
}
fun queryGroupsByMembership(recipientIds: Set<RecipientId>, includeInactive: Boolean, excludeV1: Boolean, excludeMms: Boolean): Reader {
var recipientIds = recipientIds
if (recipientIds.isEmpty()) {
return Reader(null)
}
if (recipientIds.size > 30) {
Log.w(TAG, "[queryGroupsByMembership] Large set of recipientIds (${recipientIds.size})! Using the first 30.")
recipientIds = recipientIds.take(30).toSet()
}
val membershipQuery = SqlUtil.buildSingleCollectionQuery("${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}", recipientIds)
var query: String
val queryArgs: Array<String>
if (includeInactive) {
query = "${membershipQuery.where} AND ($TABLE_NAME.$ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1))"
queryArgs = membershipQuery.whereArgs + buildArgs(1)
} else {
query = "${membershipQuery.where} AND $TABLE_NAME.$ACTIVE = ?"
queryArgs = membershipQuery.whereArgs + buildArgs(1)
}
if (excludeV1) {
query += " AND $EXPECTED_V2_ID IS NULL"
}
if (excludeMms) {
query += " AND $MMS = 0"
}
val selection = """
SELECT DISTINCT
$TABLE_NAME.*,
(
SELECT GROUP_CONCAT(${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID})
FROM ${MembershipTable.TABLE_NAME}
WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
) as $MEMBER_GROUP_CONCAT
FROM ${MembershipTable.TABLE_NAME}
INNER JOIN $TABLE_NAME ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
WHERE $query
"""
return Reader(readableDatabase.query(selection, queryArgs))
}
private fun queryGroupsByRecency(groupQuery: GroupQuery): Reader {
val query = getGroupQueryWhereStatement(groupQuery.searchQuery, groupQuery.includeInactive, !groupQuery.includeV1, !groupQuery.includeMms)
val sql = """
@ -505,41 +440,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
}
fun getOrCreateMmsGroupForMembers(members: Set<RecipientId>): GroupId.Mms {
val joinedTestMembers = members
.toList()
.map { it.toLong() }
.sorted()
.joinToString(separator = ",")
//language=sql
val statement = """
SELECT
$TABLE_NAME.$GROUP_ID as gid,
(
SELECT GROUP_CONCAT(${MembershipTable.RECIPIENT_ID}, ',')
FROM (
SELECT ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID}
FROM ${MembershipTable.TABLE_NAME}
WHERE ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID
ORDER BY ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} ASC
)
) as $MEMBER_GROUP_CONCAT
FROM $TABLE_NAME
WHERE $MEMBER_GROUP_CONCAT = ?
"""
return readableDatabase.rawQuery(statement, buildArgs(joinedTestMembers)).use { cursor ->
if (cursor.moveToNext()) {
return GroupId.parseOrThrow(cursor.requireNonNullString("gid")).requireMms()
} else {
val groupId = GroupId.createMms(SecureRandom())
create(groupId, null, members)
groupId
}
}
}
@WorkerThread
fun getPushGroupNamesContainingMember(recipientId: RecipientId): List<String> {
return getPushGroupsContainingMember(recipientId)
@ -654,7 +554,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
.orElse(null)
if (invitedByAci != null) {
val serviceId: ServiceId? = parseOrNull(invitedByAci)
val serviceId: ServiceId? = ACI.parseOrNull(invitedByAci)
if (serviceId != null) {
return Recipient.externalPush(serviceId)
}
@ -678,7 +578,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null)
}
@JvmOverloads
@CheckReturnValue
fun create(groupMasterKey: GroupMasterKey, groupState: DecryptedGroup): GroupId.V2? {
val groupId = GroupId.v2(groupMasterKey)
@ -934,36 +833,13 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
}
fun updateTitle(groupId: GroupId.V1, title: String?) {
updateTitle(groupId as GroupId, title)
}
fun updateTitle(groupId: GroupId.Mms, title: String?) {
updateTitle(groupId as GroupId, if (title.isNullOrEmpty()) null else title)
}
private fun updateTitle(groupId: GroupId, title: String?) {
if (!groupId.isV1 && !groupId.isMms) {
throw AssertionError()
}
writableDatabase
.update(TABLE_NAME)
.values(TITLE to title)
.where("$GROUP_ID = ?", groupId)
.run()
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
Recipient.live(groupRecipient).refresh()
}
/**
* Used to bust the Glide cache when an avatar changes.
*/
fun onAvatarUpdated(groupId: GroupId, hasAvatar: Boolean) {
writableDatabase
.update(TABLE_NAME)
.values(AVATAR_ID to if (hasAvatar) Math.abs(SecureRandom().nextLong()) else 0)
.values(AVATAR_ID to if (hasAvatar) abs(SecureRandom().nextLong()) else 0)
.where("$GROUP_ID = ?", groupId)
.run()
@ -971,21 +847,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
Recipient.live(groupRecipient).refresh()
}
fun updateMembers(groupId: GroupId, members: List<RecipientId>) {
writableDatabase.withinTransaction { database ->
database
.update(TABLE_NAME)
.values(ACTIVE to 1)
.where("$GROUP_ID = ?", groupId)
.run()
performMembershipUpdate(database, groupId, members)
}
val groupRecipient = recipients.getOrInsertFromGroupId(groupId)
Recipient.live(groupRecipient).refresh()
}
fun remove(groupId: GroupId, source: RecipientId) {
writableDatabase
.delete(MembershipTable.TABLE_NAME)
@ -1171,7 +1032,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
}
class V2GroupProperties(val groupMasterKey: GroupMasterKey, val groupRevision: Int, val decryptedGroupBytes: ByteArray) {
class V2GroupProperties(val groupMasterKey: GroupMasterKey, val groupRevision: Int, private val decryptedGroupBytes: ByteArray) {
val decryptedGroup: DecryptedGroup by lazy {
DecryptedGroup.ADAPTER.decode(decryptedGroupBytes)
}
@ -1410,7 +1271,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
}
enum class MemberSet(val includeSelf: Boolean, val includePending: Boolean) {
FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false), FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true), FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true)
FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false)
}
/**

View file

@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.PendingRetryReceiptCache;
import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
@ -101,7 +100,6 @@ public class ApplicationDependencies {
private static volatile FrameRateTracker frameRateTracker;
private static volatile MegaphoneRepository megaphoneRepository;
private static volatile GroupsV2Authorization groupsV2Authorization;
private static volatile GroupsV2StateProcessor groupsV2StateProcessor;
private static volatile GroupsV2Operations groupsV2Operations;
private static volatile EarlyMessageCache earlyMessageCache;
private static volatile TypingStatusRepository typingStatusRepository;
@ -198,18 +196,6 @@ public class ApplicationDependencies {
return groupsV2Operations;
}
public static @NonNull GroupsV2StateProcessor getGroupsV2StateProcessor() {
if (groupsV2StateProcessor == null) {
synchronized (LOCK) {
if (groupsV2StateProcessor == null) {
groupsV2StateProcessor = new GroupsV2StateProcessor(application);
}
}
}
return groupsV2StateProcessor;
}
public static @NonNull SignalServiceMessageSender getSignalServiceMessageSender() {
SignalServiceMessageSender local = messageSender;

View file

@ -12,7 +12,6 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
@ -20,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
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;
@ -28,8 +26,6 @@ import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -41,101 +37,60 @@ public final class GroupManager {
private static final String TAG = Log.tag(GroupManager.class);
@WorkerThread
public static @NonNull GroupActionResult createGroup(@NonNull ServiceId authServiceId,
@NonNull Context context,
@NonNull Set<Recipient> members,
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
int disappearingMessagesTimer)
throws GroupChangeBusyException, GroupChangeFailedException, IOException
{
boolean shouldAttemptToCreateV2 = !mms;
Set<RecipientId> memberIds = getMemberIds(members);
if (shouldAttemptToCreateV2) {
try {
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
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);
return GroupManagerV1.createGroup(context, memberIds, avatar, name, false);
}
} else {
return GroupManagerV1.createGroup(context, memberIds, avatar, name, mms);
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
return groupCreator.createGroup(members, name, avatar, disappearingMessagesTimer);
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable", e);
throw new GroupChangeFailedException(e);
}
}
@WorkerThread
public static GroupActionResult updateGroupDetails(@NonNull Context context,
@NonNull GroupId groupId,
@Nullable byte[] avatar,
boolean avatarChanged,
@NonNull String name,
boolean nameChanged,
@NonNull String description,
boolean descriptionChanged)
public static void updateGroupDetails(@NonNull Context context,
@NonNull GroupId groupId,
@Nullable byte[] avatar,
boolean avatarChanged,
@NonNull String name,
boolean nameChanged,
@NonNull String description,
boolean descriptionChanged)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
if (groupId.isV2()) {
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
return edit.updateGroupTitleDescriptionAndAvatar(nameChanged ? name : null,
descriptionChanged ? description : null,
avatar,
avatarChanged);
}
} else if (groupId.isV1()) {
List<Recipient> members = SignalDatabase.groups()
.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
Set<RecipientId> recipientIds = getMemberIds(new HashSet<>(members));
return GroupManagerV1.updateGroup(context, groupId.requireV1(), recipientIds, avatar, name, 0);
} else {
return GroupManagerV1.updateGroup(context, groupId.requireMms(), avatar, name);
}
}
@WorkerThread
public static void migrateGroupToServer(@NonNull Context context,
@NonNull GroupId.V1 groupIdV1,
@NonNull Collection<Recipient> members)
throws IOException, GroupChangeFailedException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
new GroupManagerV2(context).migrateGroupOnToServer(groupIdV1, members);
}
private static Set<RecipientId> getMemberIds(Collection<Recipient> recipients) {
Set<RecipientId> results = new HashSet<>(recipients.size());
for (Recipient recipient : recipients) {
results.add(recipient.getId());
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
return results;
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
edit.updateGroupTitleDescriptionAndAvatar(nameChanged ? name : null,
descriptionChanged ? description : null,
avatar,
avatarChanged);
}
}
@WorkerThread
public static void leaveGroup(@NonNull Context context, @NonNull GroupId.Push groupId, boolean sendToMembers)
throws GroupChangeBusyException, GroupChangeFailedException, IOException
{
if (groupId.isV2()) {
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
edit.leaveGroup(sendToMembers);
Log.i(TAG, "Left group " + groupId);
} catch (GroupInsufficientRightsException e) {
Log.w(TAG, "Unexpected prevention from leaving " + groupId + " due to rights", e);
throw new GroupChangeFailedException(e);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Already left group " + groupId, e);
}
} else {
if (!GroupManagerV1.leaveGroup(context, groupId.requireV1())) {
Log.w(TAG, "GV1 group leave failed" + groupId);
throw new GroupChangeFailedException();
}
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
edit.leaveGroup(sendToMembers);
Log.i(TAG, "Left group " + groupId);
} catch (GroupInsufficientRightsException e) {
Log.w(TAG, "Unexpected prevention from leaving " + groupId + " due to rights", e);
throw new GroupChangeFailedException(e);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Already left group " + groupId, e);
}
SignalDatabase.recipients().getByGroupId(groupId).ifPresent(id -> SignalDatabase.messages().deleteScheduledMessages(id));
@ -145,13 +100,11 @@ public final class GroupManager {
public static void leaveGroupFromBlockOrMessageRequest(@NonNull Context context, @NonNull GroupId.Push groupId)
throws IOException, GroupChangeBusyException, GroupChangeFailedException
{
if (groupId.isV2()) {
leaveGroup(context, groupId.requireV2(), true);
} else {
if (!GroupManagerV1.silentLeaveGroup(context, groupId.requireV1())) {
throw new GroupChangeFailedException();
}
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
leaveGroup(context, groupId.requireV2(), true);
}
@WorkerThread
@ -181,15 +134,14 @@ public final class GroupManager {
* processing deny messages.
*/
@WorkerThread
public static GroupsV2StateProcessor.GroupUpdateResult updateGroupFromServer(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey,
int revision,
long timestamp,
@Nullable byte[] signedGroupChange)
public static void updateGroupFromServer(@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey,
int revision,
long timestamp)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
return updater.updateLocalToServerRevision(revision, timestamp, null, signedGroupChange);
updater.updateLocalToServerRevision(revision, timestamp);
}
}
@ -220,36 +172,6 @@ public final class GroupManager {
}
}
@WorkerThread
public static V2GroupServerStatus v2GroupStatus(@NonNull Context context,
@NonNull ServiceId authServiceId,
@NonNull GroupMasterKey groupMasterKey)
throws IOException
{
try {
new GroupManagerV2(context).groupServerQuery(authServiceId, groupMasterKey);
return V2GroupServerStatus.FULL_OR_PENDING_MEMBER;
} catch (GroupNotAMemberException e) {
return V2GroupServerStatus.NOT_A_MEMBER;
} catch (GroupDoesNotExistException e) {
return V2GroupServerStatus.DOES_NOT_EXIST;
}
}
/**
* Tries to gets the exact version of the group at the time you joined.
* <p>
* If it fails to get the exact version, it will give the latest.
*/
@WorkerThread
public static DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId,
@NonNull Context context,
@NonNull GroupMasterKey groupMasterKey)
throws IOException, GroupDoesNotExistException, GroupNotAMemberException
{
return new GroupManagerV2(context).addedGroupVersion(authServiceId, groupMasterKey);
}
@WorkerThread
public static void setMemberAdmin(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@ -290,12 +212,12 @@ public final class GroupManager {
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
if (groupId.isV2()) {
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.updateGroupTimer(expirationTime);
}
} else {
GroupManagerV1.updateGroupTimer(context, groupId.requireV1(), expirationTime);
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.updateGroupTimer(expirationTime);
}
}
@ -330,17 +252,6 @@ public final class GroupManager {
}
}
@WorkerThread
public static void unban(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull RecipientId recipientId)
throws GroupChangeBusyException, IOException, GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.unban(Collections.singleton(Recipient.resolved(recipientId).requireServiceId()));
}
}
@WorkerThread
public static void applyMembershipAdditionRightsChange(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@ -366,7 +277,7 @@ public final class GroupManager {
@WorkerThread
public static void applyAnnouncementGroupChange(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull boolean isAnnouncementGroup)
boolean isAnnouncementGroup)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
@ -423,21 +334,14 @@ public final class GroupManager {
@NonNull Collection<RecipientId> newMembers)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException, MembershipNotSuitableForV2Exception
{
if (groupId.isV2()) {
GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId);
if (!groupId.isV2()) {
throw new GroupChangeFailedException("Not gv2");
}
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
return editor.addMembers(newMembers, groupRecord.requireV2GroupProperties().getBannedMembers());
}
} else {
GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId);
List<RecipientId> members = groupRecord.getMembers();
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
Set<RecipientId> recipientIds = new HashSet<>(members);
int originalSize = recipientIds.size();
GroupRecord groupRecord = SignalDatabase.groups().requireGroup(groupId);
recipientIds.addAll(newMembers);
return GroupManagerV1.updateGroup(context, groupId, recipientIds, avatar, groupRecord.getTitle(), recipientIds.size() - originalSize);
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
return editor.addMembers(newMembers, groupRecord.requireV2GroupProperties().getBannedMembers());
}
}
@ -478,10 +382,6 @@ public final class GroupManager {
}
}
public static void sendNoopUpdate(@NonNull Context context, @NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup currentState) {
new GroupManagerV2(context).sendNoopGroupUpdate(groupMasterKey, currentState);
}
@WorkerThread
public static @NonNull GroupExternalCredential getGroupExternalCredential(@NonNull Context context,
@NonNull GroupId.V2 groupId)
@ -534,13 +434,4 @@ public final class GroupManager {
ENABLED,
ENABLED_WITH_APPROVAL
}
public enum V2GroupServerStatus {
/** The group does not exist. The expected pre-migration state for V1 groups. */
DOES_NOT_EXIST,
/** Group exists but self is not in the group. */
NOT_A_MEMBER,
/** Self is a full or pending member of the group. */
FULL_OR_PENDING_MEMBER
}
}

View file

@ -1,229 +0,0 @@
package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.internal.push.GroupContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import okio.ByteString;
final class GroupManagerV1 {
private static final String TAG = Log.tag(GroupManagerV1.class);
static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<RecipientId> memberIds,
@Nullable byte[] avatarBytes,
@Nullable String name,
boolean mms)
{
final GroupTable groupDatabase = SignalDatabase.groups();
final SecureRandom secureRandom = new SecureRandom();
final GroupId groupId = mms ? GroupId.createMms(secureRandom) : GroupId.createV1(secureRandom);
final RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
final Recipient groupRecipient = Recipient.resolved(groupRecipientId);
memberIds.add(Recipient.self().getId());
if (groupId.isV1()) {
GroupId.V1 groupIdV1 = groupId.requireV1();
groupDatabase.create(groupIdV1, name, memberIds, null);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
}
groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null);
SignalDatabase.recipients().setProfileSharing(groupRecipient.getId(), true);
return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes, memberIds.size() - 1);
} else {
groupDatabase.create(groupId.requireMms(), name, memberIds);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
}
groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient, ThreadTable.DistributionTypes.CONVERSATION);
return new GroupActionResult(groupRecipient, threadId, memberIds.size() - 1, Collections.emptyList());
}
}
static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId groupId,
@NonNull Set<RecipientId> memberAddresses,
@Nullable byte[] avatarBytes,
@Nullable String name,
int newMemberCount)
{
final GroupTable groupDatabase = SignalDatabase.groups();
final RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
memberAddresses.add(Recipient.self().getId());
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
if (groupId.isPush()) {
GroupId.V1 groupIdV1 = groupId.requireV1();
groupDatabase.updateTitle(groupIdV1, name);
groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
}
return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes, newMemberCount);
} else {
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList());
}
}
static GroupActionResult updateGroup(@NonNull Context context,
@NonNull GroupId.Mms groupId,
@Nullable byte[] avatarBytes,
@Nullable String name)
{
GroupTable groupDatabase = SignalDatabase.groups();
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient);
groupDatabase.updateTitle(groupId, name);
groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
try {
AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null);
} catch (IOException e) {
Log.w(TAG, "Failed to save avatar!", e);
}
return new GroupActionResult(groupRecipient, threadId, 0, Collections.emptyList());
}
private static GroupActionResult sendGroupUpdate(@NonNull Context context,
@NonNull GroupId.V1 groupId,
@NonNull Set<RecipientId> members,
@Nullable String groupName,
@Nullable byte[] avatar,
int newMemberCount)
{
Attachment avatarAttachment = null;
RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
List<GroupContext.Member> uuidMembers = new ArrayList<>(members.size());
List<String> e164Members = new ArrayList<>(members.size());
for (RecipientId member : members) {
Recipient recipient = Recipient.resolved(member);
if (recipient.getHasE164()) {
e164Members.add(recipient.requireE164());
}
}
GroupContext.Builder groupContextBuilder = new GroupContext.Builder()
.id(ByteString.of(groupId.getDecodedId()))
.type(GroupContext.Type.UPDATE)
.membersE164(e164Members)
.members(uuidMembers);
if (groupName != null) groupContextBuilder.name(groupName);
GroupContext groupContext = groupContextBuilder.build();
if (avatar != null) {
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentTable.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, false, false, null, null, null, null, null);
}
OutgoingMessage outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient,
new MessageGroupContext(groupContext),
avatarAttachment != null ? Collections.singletonList(avatarAttachment) : Collections.emptyList(),
System.currentTimeMillis(),
0,
false,
null,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList());
long threadId = MessageSender.send(context, outgoingMessage, -1, MessageSender.SendType.SIGNAL, null, null);
return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList());
}
@WorkerThread
static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
return false;
}
@WorkerThread
static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) {
return false;
}
@WorkerThread
static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) {
RecipientTable recipientTable = SignalDatabase.recipients();
ThreadTable threadTable = SignalDatabase.threads();
Recipient recipient = Recipient.externalGroupExact(groupId);
long threadId = threadTable.getOrCreateThreadIdFor(recipient);
recipientTable.setExpireMessages(recipient.getId(), expirationTime);
OutgoingMessage outgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(context, outgoingMessage, threadId, MessageSender.SendType.SIGNAL, null, null);
}
@WorkerThread
private static Optional<OutgoingMessage> createGroupLeaveMessage(@NonNull Context context,
@NonNull GroupId.V1 groupId,
@NonNull Recipient groupRecipient)
{
GroupTable groupDatabase = SignalDatabase.groups();
if (!groupDatabase.isActive(groupId)) {
Log.w(TAG, "Group has already been left.");
return Optional.empty();
}
return Optional.empty();
}
}

View file

@ -7,9 +7,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
@ -34,7 +31,6 @@ import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper;
@ -52,7 +48,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
@ -88,6 +83,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import okio.ByteString;
@ -95,12 +91,11 @@ final class GroupManagerV2 {
private static final String TAG = Log.tag(GroupManagerV2.class);
private final Context context;
private final GroupTable groupDatabase;
private final GroupsV2Api groupsV2Api;
private final Context context;
private final GroupTable groupDatabase;
private final GroupsV2Api groupsV2Api;
private final GroupsV2Operations groupsV2Operations;
private final GroupsV2Authorization authorization;
private final GroupsV2StateProcessor groupsV2StateProcessor;
private final ServiceIds serviceIds;
private final ACI selfAci;
private final PNI selfPni;
@ -113,7 +108,6 @@ final class GroupManagerV2 {
ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api(),
ApplicationDependencies.getGroupsV2Operations(),
ApplicationDependencies.getGroupsV2Authorization(),
ApplicationDependencies.getGroupsV2StateProcessor(),
SignalStore.account().getServiceIds(),
new GroupCandidateHelper(),
new SendGroupUpdateHelper(context));
@ -124,7 +118,6 @@ final class GroupManagerV2 {
GroupsV2Api groupsV2Api,
GroupsV2Operations groupsV2Operations,
GroupsV2Authorization authorization,
GroupsV2StateProcessor groupsV2StateProcessor,
ServiceIds serviceIds,
GroupCandidateHelper groupCandidateHelper,
SendGroupUpdateHelper sendGroupUpdateHelper)
@ -134,7 +127,6 @@ final class GroupManagerV2 {
this.groupsV2Api = groupsV2Api;
this.groupsV2Operations = groupsV2Operations;
this.authorization = authorization;
this.groupsV2StateProcessor = groupsV2StateProcessor;
this.serviceIds = serviceIds;
this.selfAci = serviceIds.getAci();
this.selfPni = serviceIds.requirePni();
@ -212,65 +204,6 @@ final class GroupManagerV2 {
return new GroupUpdater(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
}
@WorkerThread
void groupServerQuery(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
{
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
.getCurrentGroupStateFromServer();
}
@WorkerThread
@NonNull DecryptedGroup addedGroupVersion(@NonNull ServiceId authServiceId, @NonNull GroupMasterKey groupMasterKey)
throws GroupNotAMemberException, IOException, GroupDoesNotExistException
{
GroupsV2StateProcessor.StateProcessorForGroup stateProcessorForGroup = new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey);
DecryptedGroup latest = stateProcessorForGroup.getCurrentGroupStateFromServer();
if (latest.revision == 0) {
return latest;
}
Optional<DecryptedMember> selfInFullMemberList = DecryptedGroupUtil.findMemberByAci(latest.members, selfAci);
if (!selfInFullMemberList.isPresent()) {
return latest;
}
DecryptedGroup joinedVersion = stateProcessorForGroup.getSpecificVersionFromServer(selfInFullMemberList.get().joinedAtRevision);
if (joinedVersion != null) {
return joinedVersion;
} else {
Log.w(TAG, "Unable to retrieve exact version joined at, using latest");
return latest;
}
}
@WorkerThread
void migrateGroupOnToServer(@NonNull GroupId.V1 groupIdV1, @NonNull Collection<Recipient> members)
throws IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException, GroupChangeFailedException
{
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
GroupRecord groupRecord = groupDatabase.requireGroup(groupIdV1);
String name = Util.emptyIfNull(groupRecord.getTitle());
byte[] avatar = groupRecord.hasAvatar() ? AvatarHelper.getAvatarBytes(context, groupRecord.getRecipientId()) : null;
int messageTimer = Recipient.resolved(groupRecord.getRecipientId()).getExpiresInSeconds();
Set<RecipientId> memberIds = Stream.of(members)
.map(Recipient::getId)
.filterNot(m -> m.equals(Recipient.self().getId()))
.collect(Collectors.toSet());
createGroupOnServer(groupSecretParams, name, avatar, memberIds, Member.Role.ADMINISTRATOR, messageTimer);
}
@WorkerThread
void sendNoopGroupUpdate(@NonNull GroupMasterKey masterKey, @NonNull DecryptedGroup currentState) {
sendGroupUpdateHelper.sendGroupUpdate(masterKey, new GroupMutation(currentState, new DecryptedGroupChange(), currentState), null);
}
final class GroupCreator extends LockOwner {
GroupCreator(@NonNull Closeable lock) {
@ -278,8 +211,7 @@ final class GroupManagerV2 {
}
@WorkerThread
@NonNull GroupManager.GroupActionResult createGroup(@NonNull ServiceId authServiceId,
@NonNull Collection<RecipientId> members,
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
@Nullable String name,
@Nullable byte[] avatar,
int disappearingMessagesTimer)
@ -289,7 +221,7 @@ final class GroupManagerV2 {
DecryptedGroup decryptedGroup;
try {
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, disappearingMessagesTimer);
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, disappearingMessagesTimer);
} catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e);
}
@ -430,9 +362,9 @@ final class GroupManagerV2 {
@NonNull GroupManager.GroupActionResult approveRequests(@NonNull Collection<RecipientId> recipientIds)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
Set<UUID> uuids = Stream.of(recipientIds)
.map(r -> Recipient.resolved(r).requireServiceId().getRawUuid())
.collect(Collectors.toSet());
Set<UUID> uuids = recipientIds.stream()
.map(r -> Recipient.resolved(r).requireServiceId().getRawUuid())
.collect(Collectors.toSet());
return commitChangeWithConflictResolution(selfAci, groupOperations.createApproveGroupJoinRequest(uuids));
}
@ -441,9 +373,9 @@ final class GroupManagerV2 {
@NonNull GroupManager.GroupActionResult denyRequests(@NonNull Collection<RecipientId> recipientIds)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
Set<ACI> uuids = Stream.of(recipientIds)
.map(r -> Recipient.resolved(r).requireAci())
.collect(Collectors.toSet());
Set<ACI> uuids = recipientIds.stream()
.map(r -> Recipient.resolved(r).requireAci())
.collect(Collectors.toSet());
return commitChangeWithConflictResolution(selfAci, groupOperations.createRefuseGroupJoinRequest(uuids, true, v2GroupProperties.getDecryptedGroup().bannedMembers));
}
@ -471,7 +403,7 @@ final class GroupManagerV2 {
if (aciPendingMember.isPresent()) {
selfPendingMember = aciPendingMember;
} else if (pniPendingMember.isPresent() && !selfMember.isPresent()) {
} else if (pniPendingMember.isPresent() && selfMember.isEmpty()) {
selfPendingMember = pniPendingMember;
serviceId = selfPni;
}
@ -506,7 +438,7 @@ final class GroupManagerV2 {
@NonNull GroupManager.GroupActionResult addMemberAdminsAndLeaveGroup(Collection<RecipientId> newAdmins)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
List<UUID> newAdminRecipients = Stream.of(newAdmins).map(id -> Recipient.resolved(id).requireServiceId().getRawUuid()).toList();
List<UUID> newAdminRecipients = newAdmins.stream().map(id -> Recipient.resolved(id).requireServiceId().getRawUuid()).collect(Collectors.toList());
return commitChangeWithConflictResolution(selfAci, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci,
newAdminRecipients));
@ -520,7 +452,7 @@ final class GroupManagerV2 {
DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
Optional<DecryptedMember> selfInGroup = DecryptedGroupUtil.findMemberByAci(group.members, selfAci);
if (!selfInGroup.isPresent()) {
if (selfInGroup.isEmpty()) {
Log.w(TAG, "Self not in group " + groupId);
return null;
}
@ -584,12 +516,6 @@ final class GroupManagerV2 {
return commitChangeWithConflictResolution(selfAci, groupOperations.createBanServiceIdsChange(Collections.singleton(serviceId), rejectJoinRequest, v2GroupProperties.getDecryptedGroup().bannedMembers));
}
public GroupManager.GroupActionResult unban(Set<ServiceId> serviceIds)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(selfAci, groupOperations.createUnbanServiceIdsChange(serviceIds));
}
@WorkerThread
public GroupManager.GroupActionResult cycleGroupLinkPassword()
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
@ -637,13 +563,7 @@ final class GroupManagerV2 {
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(authServiceId, change, false);
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
return commitChangeWithConflictResolution(authServiceId, change, allowWhenBlocked, true);
return commitChangeWithConflictResolution(authServiceId, change, false, true);
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
@ -683,7 +603,7 @@ final class GroupManagerV2 {
private GroupChange.Actions.Builder resolveConflict(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change)
throws IOException, GroupNotAMemberException, GroupChangeFailedException
{
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = groupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
GroupsV2StateProcessor.GroupUpdateResult groupUpdateResult = GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.updateLocalGroupToRevision(GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null);
if (groupUpdateResult.getLatestServer() == null) {
@ -716,7 +636,7 @@ final class GroupManagerV2 {
List<RecipientId> ids = groupOperations.decryptAddMembers(change.addMembers)
.stream()
.map(RecipientId::from)
.collect(java.util.stream.Collectors.toList());
.collect(Collectors.toList());
for (RecipientId id : ids) {
ProfileUtil.updateExpiringProfileKeyCredential(Recipient.resolved(id));
@ -798,11 +718,11 @@ final class GroupManagerV2 {
}
@WorkerThread
GroupsV2StateProcessor.GroupUpdateResult updateLocalToServerRevision(int revision, long timestamp, @Nullable GroupSecretParams groupSecretParams, @Nullable byte[] signedGroupChange)
void updateLocalToServerRevision(int revision, long timestamp)
throws IOException, GroupNotAMemberException
{
return new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey, groupSecretParams)
.updateLocalGroupToRevision(revision, timestamp, getDecryptedGroupChange(signedGroupChange));
GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.updateLocalGroupToRevision(revision, timestamp, null);
}
@WorkerThread
@ -814,16 +734,16 @@ final class GroupManagerV2 {
@Nullable String serverGuid)
throws IOException, GroupNotAMemberException
{
return new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey, groupSecretParams)
.updateLocalGroupToRevision(revision, timestamp, localRecord, getDecryptedGroupChange(signedGroupChange), serverGuid);
return GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey, groupSecretParams)
.updateLocalGroupToRevision(revision, timestamp, localRecord, getDecryptedGroupChange(signedGroupChange), serverGuid);
}
@WorkerThread
void forceSanityUpdateFromServer(long timestamp)
throws IOException, GroupNotAMemberException
{
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
.forceSanityUpdateFromServer(timestamp);
GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.forceSanityUpdateFromServer(timestamp);
}
private DecryptedGroupChange getDecryptedGroupChange(@Nullable byte[] signedGroupChange) {
@ -847,7 +767,6 @@ final class GroupManagerV2 {
@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members,
@NonNull Member.Role memberRole,
int disappearingMessageTimerSeconds)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception, GroupAlreadyExistsException
{
@ -873,7 +792,7 @@ final class GroupManagerV2 {
Optional.ofNullable(avatar),
self,
candidates,
memberRole,
Member.Role.DEFAULT,
disappearingMessageTimerSeconds);
try {
@ -951,7 +870,7 @@ final class GroupManagerV2 {
Log.i(TAG, "Group already present locally");
if (decryptedChange != null) {
try {
groupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to apply join change to existing group", e);
@ -965,7 +884,7 @@ final class GroupManagerV2 {
Log.i(TAG, "Create placeholder failed, group suddenly present locally, attempting to apply change");
if (decryptedChange != null) {
try {
groupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
GroupsV2StateProcessor.forGroup(SignalStore.account().getServiceIds(), groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision, System.currentTimeMillis(), decryptedChange);
} catch (GroupNotAMemberException e) {
Log.w(TAG, "Unable to apply join change to existing group", e);
@ -1014,10 +933,10 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, IOException
{
try {
new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision,
System.currentTimeMillis(),
decryptedChange);
GroupsV2StateProcessor.forGroup(serviceIds, groupMasterKey)
.updateLocalGroupToRevision(decryptedChange.revision,
System.currentTimeMillis(),
decryptedChange);
RecipientAndThread recipientAndThread = sendGroupUpdateHelper.sendGroupUpdate(groupMasterKey, new GroupMutation(null, decryptedChange, decryptedGroup), signedGroupChange);
@ -1331,9 +1250,10 @@ final class GroupManagerV2 {
}
private static @NonNull List<RecipientId> getPendingMemberRecipientIds(@NonNull List<DecryptedPendingMember> newPendingMembersList) {
return Stream.of(DecryptedGroupUtil.pendingToServiceIdList(newPendingMembersList))
.map(serviceId -> RecipientId.from(serviceId))
.toList();
return DecryptedGroupUtil.pendingToServiceIdList(newPendingMembersList)
.stream()
.map(RecipientId::from)
.collect(Collectors.toList());
}
private static @NonNull AccessControl.AccessRequired rightsToAccessControl(@NonNull GroupAccessControl rights) {

View file

@ -6,10 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupManager;
@ -21,14 +18,11 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
final class AddGroupDetailsRepository {
private static final String TAG = Log.tag(AddGroupDetailsRepository.class);
private final Context context;
AddGroupDetailsRepository(@NonNull Context context) {
@ -50,20 +44,15 @@ final class AddGroupDetailsRepository {
void createGroup(@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
@Nullable Integer disappearingMessagesTimer,
Consumer<GroupCreateResult> resultConsumer)
{
SignalExecutors.BOUNDED.execute(() -> {
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
try {
GroupManager.GroupActionResult result = GroupManager.createGroup(SignalStore.account().requireAci(),
context,
recipients,
GroupManager.GroupActionResult result = GroupManager.createGroup(context,
members,
avatar,
name,
mms,
disappearingMessagesTimer != null ? disappearingMessagesTimer
: SignalStore.settings().getUniversalExpireTimer());

View file

@ -104,11 +104,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
byte[] avatarBytes = avatar.getValue();
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
String groupName = name.getValue();
Integer disappearingTimer = disappearingMessagesTimer.getValue();
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
if (TextUtils.isEmpty(groupName)) {
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
return;
}
@ -116,7 +115,6 @@ public final class AddGroupDetailsViewModel extends ViewModel {
repository.createGroup(memberIds,
avatarBytes,
groupName,
isGroupMms,
disappearingTimer,
groupCreateResult::postValue);
}

View file

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.groups.v2.processing;
import android.content.Context;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -8,8 +7,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
@ -18,7 +15,6 @@ 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.backup.v2.proto.GroupChangeChatUpdate;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.RecipientTable;
@ -29,7 +25,6 @@ import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupDoesNotExistException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMutation;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
@ -93,30 +88,25 @@ public class GroupsV2StateProcessor {
*/
public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper.RESTORE_PLACEHOLDER_REVISION;
private final Context context;
private final RecipientTable recipientTable;
private final GroupTable groupDatabase;
private final GroupsV2Authorization groupsV2Authorization;
private final GroupsV2Api groupsV2Api;
public GroupsV2StateProcessor(@NonNull Context context) {
this.context = context.getApplicationContext();
this.groupsV2Authorization = ApplicationDependencies.getGroupsV2Authorization();
this.groupsV2Api = ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api();
this.recipientTable = SignalDatabase.recipients();
this.groupDatabase = SignalDatabase.groups();
private GroupsV2StateProcessor() {
}
public StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey) {
public static StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey) {
return forGroup(serviceIds, groupMasterKey, null);
}
public StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey, @Nullable GroupSecretParams groupSecretParams) {
if (groupSecretParams == null) {
return new StateProcessorForGroup(serviceIds, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, recipientTable);
} else {
return new StateProcessorForGroup(serviceIds, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, groupSecretParams, recipientTable);
}
public static StateProcessorForGroup forGroup(@NonNull ServiceIds serviceIds, @NonNull GroupMasterKey groupMasterKey, @Nullable GroupSecretParams groupSecretParams) {
groupSecretParams = groupSecretParams != null ? groupSecretParams : GroupSecretParams.deriveFromMasterKey(groupMasterKey);
return new StateProcessorForGroup(
serviceIds,
SignalDatabase.groups(),
ApplicationDependencies.getSignalServiceAccountManager().getGroupsV2Api(),
ApplicationDependencies.getGroupsV2Authorization(),
groupMasterKey,
groupSecretParams,
SignalDatabase.recipients()
);
}
public enum GroupState {
@ -160,18 +150,6 @@ public class GroupsV2StateProcessor {
private final ProfileAndMessageHelper profileAndMessageHelper;
private StateProcessorForGroup(@NonNull ServiceIds serviceIds,
@NonNull Context context,
@NonNull GroupTable groupDatabase,
@NonNull GroupsV2Api groupsV2Api,
@NonNull GroupsV2Authorization groupsV2Authorization,
@NonNull GroupMasterKey groupMasterKey,
@NonNull RecipientTable recipientTable)
{
this(serviceIds, context, groupDatabase, groupsV2Api, groupsV2Authorization, groupMasterKey, GroupSecretParams.deriveFromMasterKey(groupMasterKey), recipientTable);
}
private StateProcessorForGroup(@NonNull ServiceIds serviceIds,
@NonNull Context context,
@NonNull GroupTable groupDatabase,
@NonNull GroupsV2Api groupsV2Api,
@NonNull GroupsV2Authorization groupsV2Authorization,
@ -186,7 +164,7 @@ public class GroupsV2StateProcessor {
this.masterKey = groupMasterKey;
this.groupSecretParams = groupSecretParams;
this.groupId = GroupId.v2(groupSecretParams.getPublicParams().getGroupIdentifier());
this.profileAndMessageHelper = new ProfileAndMessageHelper(context, serviceIds.getAci(), groupMasterKey, groupId, recipientTable);
this.profileAndMessageHelper = new ProfileAndMessageHelper(serviceIds.getAci(), groupMasterKey, groupId, recipientTable);
}
@VisibleForTesting StateProcessorForGroup(@NonNull ServiceIds serviceIds,
@ -522,40 +500,6 @@ public class GroupsV2StateProcessor {
return new GroupUpdateResult(GroupState.GROUP_UPDATED, finalState);
}
@WorkerThread
public @NonNull DecryptedGroup getCurrentGroupStateFromServer()
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
{
try {
return groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams));
} catch (GroupNotFoundException e) {
throw new GroupDoesNotExistException(e);
} catch (NotInGroupException e) {
throw new GroupNotAMemberException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new IOException(e);
}
}
@WorkerThread
public @Nullable DecryptedGroup getSpecificVersionFromServer(int revision)
throws IOException, GroupNotAMemberException, GroupDoesNotExistException
{
try {
return groupsV2Api.getGroupHistoryPage(groupSecretParams, revision, groupsV2Authorization.getAuthorizationForToday(serviceIds, groupSecretParams), true)
.getResults()
.get(0)
.getGroup()
.orElse(null);
} catch (GroupNotFoundException e) {
throw new GroupDoesNotExistException(e);
} catch (NotInGroupException e) {
throw new GroupNotAMemberException(e);
} catch (VerificationFailedException | InvalidGroupStateException e) {
throw new IOException(e);
}
}
private void insertGroupLeave() {
if (!groupDatabase.isActive(groupId)) {
warn("Group has already been left.");
@ -655,11 +599,7 @@ public class GroupsV2StateProcessor {
}
private void info(String message) {
info(message, null);
}
private void info(String message, Throwable t) {
Log.i(TAG, "[" + groupId.toString() + "] " + message, t);
Log.i(TAG, "[" + groupId.toString() + "] " + message);
}
private void warn(String message) {
@ -674,7 +614,6 @@ public class GroupsV2StateProcessor {
@VisibleForTesting
static class ProfileAndMessageHelper {
private final Context context;
private final ACI aci;
private final GroupId.V2 groupId;
private final RecipientTable recipientTable;
@ -682,8 +621,7 @@ public class GroupsV2StateProcessor {
@VisibleForTesting
GroupMasterKey masterKey;
ProfileAndMessageHelper(@NonNull Context context, @NonNull ACI aci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientTable recipientTable) {
this.context = context;
ProfileAndMessageHelper(@NonNull ACI aci, @NonNull GroupMasterKey masterKey, @NonNull GroupId.V2 groupId, @NonNull RecipientTable recipientTable) {
this.aci = aci;
this.masterKey = masterKey;
this.groupId = groupId;
@ -706,13 +644,13 @@ public class GroupsV2StateProcessor {
DecryptedMember selfAsMember = selfAsMemberOptional.get();
int revisionJoinedAt = selfAsMember.joinedAtRevision;
Optional<Recipient> addedByOptional = Stream.of(inputGroupState.getServerHistory())
.map(ServerGroupLogEntry::getChange)
.filter(c -> c != null && c.revision == revisionJoinedAt)
.findFirst()
.map(c -> Optional.ofNullable(ServiceId.parseOrNull(c.editorServiceIdBytes))
.map(Recipient::externalPush))
.orElse(Optional.empty());
Optional<Recipient> addedByOptional = inputGroupState.getServerHistory()
.stream()
.map(ServerGroupLogEntry::getChange)
.filter(c -> c != null && c.revision == revisionJoinedAt)
.findFirst()
.flatMap(c -> Optional.ofNullable(ServiceId.parseOrNull(c.editorServiceIdBytes))
.map(Recipient::externalPush));
if (addedByOptional.isPresent()) {
Recipient addedBy = addedByOptional.get();
@ -804,7 +742,7 @@ public class GroupsV2StateProcessor {
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp, @Nullable String serverGuid) {
Optional<ServiceId> editor = getEditor(decryptedGroupV2Context);
boolean outgoing = !editor.isPresent() || aci.equals(editor.get());
boolean outgoing = editor.isEmpty() || aci.equals(editor.get());
GV2UpdateDescription updateDescription = new GV2UpdateDescription.Builder()
.gv2ChangeDescription(decryptedGroupV2Context)

View file

@ -89,7 +89,7 @@ final class RequestGroupV2InfoWorkerJob extends BaseJob {
return;
}
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis(), null);
GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis());
}
@Override

View file

@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.database.GroupStateTestData
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.model.databaseprotos.member
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testutil.SystemOutLogger
@ -66,7 +65,6 @@ class GroupManagerV2Test_edit {
private lateinit var groupsV2API: GroupsV2Api
private lateinit var groupsV2Operations: GroupsV2Operations
private lateinit var groupsV2Authorization: GroupsV2Authorization
private lateinit var groupsV2StateProcessor: GroupsV2StateProcessor
private lateinit var groupCandidateHelper: GroupCandidateHelper
private lateinit var sendGroupUpdateHelper: GroupManagerV2.SendGroupUpdateHelper
private lateinit var groupOperations: GroupsV2Operations.GroupOperations
@ -89,7 +87,6 @@ class GroupManagerV2Test_edit {
groupsV2API = mockk()
groupsV2Operations = GroupsV2Operations(clientZkOperations, 1000)
groupsV2Authorization = mockk(relaxed = true)
groupsV2StateProcessor = mockk()
groupCandidateHelper = mockk()
sendGroupUpdateHelper = mockk()
groupOperations = groupsV2Operations.forGroup(groupSecretParams)
@ -100,7 +97,6 @@ class GroupManagerV2Test_edit {
groupsV2API,
groupsV2Operations,
groupsV2Authorization,
groupsV2StateProcessor,
serviceIds,
groupCandidateHelper,
sendGroupUpdateHelper