Cleanup group management code.
This commit is contained in:
parent
34faa9003f
commit
a71faf674d
11 changed files with 131 additions and 859 deletions
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue