GV2 database.
This commit is contained in:
parent
640c82d517
commit
9e6cca1cd0
23 changed files with 513 additions and 78 deletions
|
@ -364,7 +364,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
|
||||
GroupId groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
@ -550,7 +550,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
|||
@Override
|
||||
protected Optional<GroupData> doInBackground(GroupId... groupIds) {
|
||||
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
|
||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], false);
|
||||
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
|
||||
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
|
||||
existingContacts.addAll(recipients);
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -34,7 +35,7 @@ public final class GroupMembersDialog {
|
|||
public void display() {
|
||||
SimpleTask.run(
|
||||
lifecycle,
|
||||
() -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), true),
|
||||
() -> DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupRecipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF),
|
||||
members -> {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.ConversationActivity_group_members)
|
||||
|
|
|
@ -31,6 +31,7 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
|||
import androidx.loader.content.Loader;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -368,7 +369,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
|
||||
|
||||
if (receiptInfoList.isEmpty()) {
|
||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false);
|
||||
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient recipient : group) {
|
||||
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
|
||||
|
|
|
@ -132,6 +132,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
|
@ -1574,7 +1575,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
|
||||
if (params[0].isGroup()) {
|
||||
recipients.addAll(DatabaseFactory.getGroupDatabase(ConversationActivity.this)
|
||||
.getGroupMembers(params[0].requireGroupId(), false));
|
||||
.getGroupMembers(params[0].requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF));
|
||||
} else {
|
||||
recipients.add(params[0]);
|
||||
}
|
||||
|
|
|
@ -1,39 +1,50 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.storageservice.protos.groups.Member;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupDatabase extends Database {
|
||||
public final class GroupDatabase extends Database {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = GroupDatabase.class.getSimpleName();
|
||||
private static final String TAG = Log.tag(GroupDatabase.class);
|
||||
|
||||
static final String TABLE_NAME = "groups";
|
||||
private static final String ID = "_id";
|
||||
|
@ -50,6 +61,14 @@ public class GroupDatabase extends Database {
|
|||
static final String ACTIVE = "active";
|
||||
static final String MMS = "mms";
|
||||
|
||||
/* V2 Group columns */
|
||||
/** 32 bytes serialized {@link GroupMasterKey} */
|
||||
private static final String V2_MASTER_KEY = "master_key";
|
||||
/** Increments with every change to the group */
|
||||
private static final String V2_REVISION = "revision";
|
||||
/** Serialized {@link DecryptedGroup} protobuf */
|
||||
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME +
|
||||
" (" + ID + " INTEGER PRIMARY KEY, " +
|
||||
|
@ -64,7 +83,10 @@ public class GroupDatabase extends Database {
|
|||
TIMESTAMP + " INTEGER, " +
|
||||
ACTIVE + " INTEGER DEFAULT 1, " +
|
||||
AVATAR_DIGEST + " BLOB, " +
|
||||
MMS + " INTEGER DEFAULT 0);";
|
||||
MMS + " INTEGER DEFAULT 0, " +
|
||||
V2_MASTER_KEY + " BLOB, " +
|
||||
V2_REVISION + " BLOB, " +
|
||||
V2_DECRYPTED_GROUP + " BLOB);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
|
||||
|
@ -140,19 +162,20 @@ public class GroupDatabase extends Database {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public GroupId getOrCreateGroupForMembers(List<RecipientId> members, boolean mms) {
|
||||
public GroupId.Mms getOrCreateMmsGroupForMembers(List<RecipientId> members) {
|
||||
Collections.sort(members);
|
||||
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
|
||||
MEMBERS + " = ? AND " + MMS + " = ?",
|
||||
new String[] {RecipientId.toSerializedList(members), mms ? "1" : "0"},
|
||||
new String[] {RecipientId.toSerializedList(members), "1"},
|
||||
null, null, null);
|
||||
try {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)));
|
||||
return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)))
|
||||
.requireMms();
|
||||
} else {
|
||||
GroupId groupId = allocateGroupId(mms);
|
||||
create(groupId, null, members, null, null);
|
||||
GroupId.Mms groupId = GroupId.createMms(new SecureRandom());
|
||||
create(groupId, members);
|
||||
return groupId;
|
||||
}
|
||||
} finally {
|
||||
|
@ -194,24 +217,62 @@ public class GroupDatabase extends Database {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> getGroupMembers(@NonNull GroupId groupId, boolean includeSelf) {
|
||||
List<RecipientId> members = getCurrentMembers(groupId);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
@WorkerThread
|
||||
public @NonNull List<Recipient> getGroupMembers(@NonNull GroupId groupId, @NonNull MemberSet memberSet) {
|
||||
if (groupId.isV2()) {
|
||||
return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMembers(context, memberSet))
|
||||
.or(Collections.emptyList());
|
||||
} else {
|
||||
List<RecipientId> currentMembers = getCurrentMembers(groupId);
|
||||
List<Recipient> recipients = new ArrayList<>(currentMembers.size());
|
||||
|
||||
for (RecipientId member : members) {
|
||||
if (!includeSelf && Recipient.resolved(member).isLocalNumber()) {
|
||||
continue;
|
||||
for (RecipientId member : currentMembers) {
|
||||
if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) {
|
||||
recipients.add(Recipient.resolved(member));
|
||||
}
|
||||
}
|
||||
|
||||
recipients.add(Recipient.resolved(member));
|
||||
return recipients;
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public void create(@NonNull GroupId groupId, @Nullable String title, @NonNull List<RecipientId> members,
|
||||
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
|
||||
public void create(@NonNull GroupId.V1 groupId,
|
||||
@Nullable String title,
|
||||
@NonNull Collection<RecipientId> members,
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@Nullable String relay)
|
||||
{
|
||||
create(groupId, title, members, avatar, relay, null, null);
|
||||
}
|
||||
|
||||
public void create(@NonNull GroupId.Mms groupId,
|
||||
@NonNull Collection<RecipientId> members)
|
||||
{
|
||||
create(groupId, null, members, null, null, null, null);
|
||||
}
|
||||
|
||||
public void create(@NonNull GroupId.V2 groupId,
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@Nullable String relay,
|
||||
@NonNull GroupMasterKey groupMasterKey,
|
||||
@NonNull DecryptedGroup groupState)
|
||||
{
|
||||
create(groupId, groupState.getTitle(), Collections.emptyList(), avatar, relay, groupMasterKey, groupState);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version).
|
||||
*/
|
||||
private void create(@NonNull GroupId groupId,
|
||||
@Nullable String title,
|
||||
@NonNull Collection<RecipientId> memberCollection,
|
||||
@Nullable SignalServiceAttachmentPointer avatar,
|
||||
@Nullable String relay,
|
||||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@Nullable DecryptedGroup groupState)
|
||||
{
|
||||
List<RecipientId> members = new ArrayList<>(new HashSet<>(memberCollection));
|
||||
|
||||
Collections.sort(members);
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
@ -234,6 +295,21 @@ public class GroupDatabase extends Database {
|
|||
contentValues.put(ACTIVE, 1);
|
||||
contentValues.put(MMS, groupId.isMms());
|
||||
|
||||
if (groupMasterKey != null) {
|
||||
if (groupState == null) {
|
||||
throw new AssertionError("V2 master key but no group state");
|
||||
}
|
||||
groupId.requireV2();
|
||||
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||
contentValues.put(V2_REVISION, groupState.getVersion());
|
||||
contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray());
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(context, groupState));
|
||||
} else {
|
||||
if (groupId.isV2()) {
|
||||
throw new AssertionError("V2 group id but no master key");
|
||||
}
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
|
@ -242,7 +318,10 @@ public class GroupDatabase extends Database {
|
|||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void update(@NonNull GroupId groupId, String title, SignalServiceAttachmentPointer avatar) {
|
||||
public void update(@NonNull GroupId.V1 groupId,
|
||||
@Nullable String title,
|
||||
@Nullable SignalServiceAttachmentPointer avatar)
|
||||
{
|
||||
ContentValues contentValues = new ContentValues();
|
||||
if (title != null) contentValues.put(TITLE, title);
|
||||
|
||||
|
@ -265,7 +344,30 @@ public class GroupDatabase extends Database {
|
|||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void updateTitle(@NonNull GroupId groupId, String title) {
|
||||
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
|
||||
update(GroupId.v2(groupMasterKey), decryptedGroup);
|
||||
}
|
||||
|
||||
public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) {
|
||||
String title = decryptedGroup.getTitle();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
contentValues.put(TITLE, title);
|
||||
contentValues.put(V2_REVISION, decryptedGroup.getVersion());
|
||||
contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray());
|
||||
contentValues.put(MEMBERS, serializeV2GroupMembers(context, decryptedGroup));
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
|
||||
GROUP_ID + " = ?",
|
||||
new String[]{ groupId.toString() });
|
||||
|
||||
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
Recipient.live(groupRecipient).refresh();
|
||||
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void updateTitle(@NonNull GroupId.V1 groupId, String title) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(TITLE, title);
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
|
||||
|
@ -278,7 +380,7 @@ public class GroupDatabase extends Database {
|
|||
/**
|
||||
* Used to bust the Glide cache when an avatar changes.
|
||||
*/
|
||||
public void onAvatarUpdated(@NonNull GroupId groupId, boolean hasAvatar) {
|
||||
public void onAvatarUpdated(@NonNull GroupId.V1 groupId, boolean hasAvatar) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0);
|
||||
|
||||
|
@ -350,10 +452,18 @@ public class GroupDatabase extends Database {
|
|||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
|
||||
}
|
||||
|
||||
public static GroupId allocateGroupId(boolean mms) {
|
||||
byte[] groupId = new byte[16];
|
||||
new SecureRandom().nextBytes(groupId);
|
||||
return mms ? GroupId.mms(groupId) : GroupId.v1(groupId);
|
||||
private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) {
|
||||
List<RecipientId> groupMembers = new ArrayList<>(decryptedGroup.getMembersCount());
|
||||
|
||||
for (DecryptedMember member : decryptedGroup.getMembersList()) {
|
||||
Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.fromByteString(member.getUuid()), null));
|
||||
|
||||
groupMembers.add(recipient.getId());
|
||||
}
|
||||
|
||||
Collections.sort(groupMembers);
|
||||
|
||||
return RecipientId.toSerializedList(groupMembers);
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
@ -387,7 +497,10 @@ public class GroupDatabase extends Database {
|
|||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1);
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -399,21 +512,23 @@ public class GroupDatabase extends Database {
|
|||
|
||||
public static class GroupRecord {
|
||||
|
||||
private final GroupId id;
|
||||
private final RecipientId recipientId;
|
||||
private final String title;
|
||||
private final List<RecipientId> members;
|
||||
private final long avatarId;
|
||||
private final byte[] avatarKey;
|
||||
private final byte[] avatarDigest;
|
||||
private final String avatarContentType;
|
||||
private final String relay;
|
||||
private final boolean active;
|
||||
private final boolean mms;
|
||||
private final GroupId id;
|
||||
private final RecipientId recipientId;
|
||||
private final String title;
|
||||
private final List<RecipientId> members;
|
||||
private final long avatarId;
|
||||
private final byte[] avatarKey;
|
||||
private final byte[] avatarDigest;
|
||||
private final String avatarContentType;
|
||||
private final String relay;
|
||||
private final boolean active;
|
||||
private final boolean mms;
|
||||
@Nullable private final V2GroupProperties v2GroupProperties;
|
||||
|
||||
public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members,
|
||||
long avatarId, byte[] avatarKey, String avatarContentType,
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms)
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms,
|
||||
@Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes)
|
||||
{
|
||||
this.id = id;
|
||||
this.recipientId = recipientId;
|
||||
|
@ -426,6 +541,18 @@ public class GroupDatabase extends Database {
|
|||
this.active = active;
|
||||
this.mms = mms;
|
||||
|
||||
V2GroupProperties v2GroupProperties = null;
|
||||
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
|
||||
GroupMasterKey groupMasterKey;
|
||||
try {
|
||||
groupMasterKey = new GroupMasterKey(groupMasterKeyBytes);
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
v2GroupProperties = new V2GroupProperties(groupMasterKey, groupRevision, decryptedGroupBytes);
|
||||
}
|
||||
this.v2GroupProperties = v2GroupProperties;
|
||||
|
||||
if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members);
|
||||
else this.members = new LinkedList<>();
|
||||
}
|
||||
|
@ -477,5 +604,97 @@ public class GroupDatabase extends Database {
|
|||
public boolean isMms() {
|
||||
return mms;
|
||||
}
|
||||
|
||||
public boolean isV2Group() {
|
||||
return v2GroupProperties != null;
|
||||
}
|
||||
|
||||
public @NonNull V2GroupProperties requireV2GroupProperties() {
|
||||
if (v2GroupProperties == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
return v2GroupProperties;
|
||||
}
|
||||
|
||||
public boolean isAdmin(@NonNull Recipient recipient) {
|
||||
return isV2Group() && requireV2GroupProperties().isAdmin(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
public static class V2GroupProperties {
|
||||
|
||||
@NonNull private final GroupMasterKey groupMasterKey;
|
||||
private final int groupRevision;
|
||||
@NonNull private final byte[] decryptedGroupBytes;
|
||||
private DecryptedGroup decryptedGroup;
|
||||
|
||||
private V2GroupProperties(@NonNull GroupMasterKey groupMasterKey, int groupRevision, @NonNull byte[] decryptedGroup) {
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.groupRevision = groupRevision;
|
||||
this.decryptedGroupBytes = decryptedGroup;
|
||||
}
|
||||
|
||||
public @NonNull GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public int getGroupRevision() {
|
||||
return groupRevision;
|
||||
}
|
||||
|
||||
public @NonNull DecryptedGroup getDecryptedGroup() {
|
||||
try {
|
||||
if (decryptedGroup == null) {
|
||||
decryptedGroup = DecryptedGroup.parseFrom(decryptedGroupBytes);
|
||||
}
|
||||
return decryptedGroup;
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAdmin(@NonNull Recipient recipient) {
|
||||
return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), recipient.getUuid().get())
|
||||
.transform(t -> t.getRole() == Member.Role.ADMINISTRATOR)
|
||||
.or(false);
|
||||
}
|
||||
|
||||
public List<Recipient> getMembers(@NonNull Context context, @NonNull MemberSet memberSet) {
|
||||
boolean includeSelf = memberSet.includeSelf;
|
||||
DecryptedGroup groupV2 = getDecryptedGroup();
|
||||
UUID selfUuid = Recipient.self().getUuid().get();
|
||||
List<Recipient> recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount());
|
||||
|
||||
for (UUID uuid : DecryptedGroupUtil.toUuidList(groupV2.getMembersList())) {
|
||||
if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(Recipient.externalPush(context, uuid, null));
|
||||
}
|
||||
}
|
||||
if (memberSet.includePending) {
|
||||
for (UUID uuid : DecryptedGroupUtil.pendingToUuidList(groupV2.getPendingMembersList())) {
|
||||
if (includeSelf || !selfUuid.equals(uuid)) {
|
||||
recipients.add(Recipient.externalPush(context, uuid, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
}
|
||||
|
||||
public enum MemberSet {
|
||||
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);
|
||||
|
||||
private boolean includeSelf;
|
||||
private boolean includePending;
|
||||
|
||||
MemberSet(boolean includeSelf, boolean includePending) {
|
||||
this.includeSelf = includeSelf;
|
||||
this.includePending = includePending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database;
|
|||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
@ -11,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -41,7 +43,7 @@ public class GroupReceiptDatabase extends Database {
|
|||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void insert(List<RecipientId> recipientIds, long mmsId, int status, long timestamp) {
|
||||
public void insert(Collection<RecipientId> recipientIds, long mmsId, int status, long timestamp) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
for (RecipientId recipientId : recipientIds) {
|
||||
|
|
|
@ -1141,8 +1141,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
||||
|
||||
if (message.getRecipient().isGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false);
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(),
|
||||
messageId, defaultReceiptStatus, message.getSentTimeMillis());
|
||||
|
|
|
@ -233,7 +233,7 @@ public class SmsMigrator {
|
|||
|
||||
List<RecipientId> recipientIds = Stream.of(ourRecipients).map(Recipient::getId).toList();
|
||||
|
||||
GroupId ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipientIds, true);
|
||||
GroupId.Mms ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipientIds);
|
||||
RecipientId ourGroupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(ourGroupId);
|
||||
Recipient ourGroupRecipient = Recipient.resolved(ourGroupRecipientId);
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
|
|
@ -124,8 +124,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int TRANSFER_FILE_CLEANUP = 52;
|
||||
private static final int PROFILE_DATA_MIGRATION = 53;
|
||||
private static final int AVATAR_LOCATION_MIGRATION = 54;
|
||||
private static final int GROUPS_V2 = 55;
|
||||
|
||||
private static final int DATABASE_VERSION = 54;
|
||||
private static final int DATABASE_VERSION = 55;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -851,6 +852,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL("UPDATE groups SET avatar = NULL");
|
||||
}
|
||||
|
||||
if (oldVersion < GROUPS_V2) {
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN master_key");
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN revision");
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN decrypted_group");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -7,13 +7,17 @@ import org.signal.zkgroup.groups.GroupIdentifier;
|
|||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public abstract class GroupId {
|
||||
|
||||
private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!";
|
||||
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!";
|
||||
private static final int MMS_BYTE_LENGTH = 16;
|
||||
private static final int V1_MMS_BYTE_LENGTH = 16;
|
||||
private static final int V2_BYTE_LENGTH = GroupIdentifier.SIZE;
|
||||
private static final int V2_ENCODED_LENGTH = ENCODED_SIGNAL_GROUP_PREFIX.length() + V2_BYTE_LENGTH * 2;
|
||||
|
||||
|
@ -34,6 +38,14 @@ public abstract class GroupId {
|
|||
return new GroupId.V1(gv1GroupIdBytes);
|
||||
}
|
||||
|
||||
public static GroupId.V1 createV1(@NonNull SecureRandom secureRandom) {
|
||||
return v1(Util.getSecretBytes(secureRandom, V1_MMS_BYTE_LENGTH));
|
||||
}
|
||||
|
||||
public static GroupId.Mms createMms(@NonNull SecureRandom secureRandom) {
|
||||
return mms(Util.getSecretBytes(secureRandom, MMS_BYTE_LENGTH));
|
||||
}
|
||||
|
||||
public static GroupId.V2 v2(@NonNull byte[] bytes) {
|
||||
if (bytes.length != V2_BYTE_LENGTH) {
|
||||
throw new AssertionError();
|
||||
|
|
|
@ -93,7 +93,7 @@ public final class GroupV1MessageProcessor {
|
|||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
GroupId.V1 id = GroupId.v1(group.getGroupId());
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.UPDATE);
|
||||
|
||||
|
@ -127,7 +127,7 @@ public final class GroupV1MessageProcessor {
|
|||
{
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
GroupId id = GroupId.v1(group.getGroupId());
|
||||
GroupId.V1 id = GroupId.v1(group.getGroupId());
|
||||
|
||||
Set<RecipientId> recordMembers = new HashSet<>(groupRecord.getMembers());
|
||||
Set<RecipientId> messageMembers = new HashSet<>();
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupC
|
|||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -53,23 +54,28 @@ final class V1GroupManager {
|
|||
{
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final GroupId groupId = GroupDatabase.allocateGroupId(mms);
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
final GroupId groupId = mms ? GroupId.createMms(secureRandom) : GroupId.createV1(secureRandom);
|
||||
final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
|
||||
final Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
|
||||
memberIds.add(Recipient.self().getId());
|
||||
groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null);
|
||||
|
||||
if (!mms) {
|
||||
if (groupId.isV1()) {
|
||||
GroupId.V1 groupIdV1 = groupId.requireV1();
|
||||
|
||||
groupDatabase.create(groupIdV1, name, memberIds, null, 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(groupId, avatarBytes != null);
|
||||
groupDatabase.onAvatarUpdated(groupIdV1, avatarBytes != null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||
return sendGroupUpdate(context, groupId.requireV1(), memberIds, name, avatarBytes);
|
||||
return sendGroupUpdate(context, groupIdV1, memberIds, name, avatarBytes);
|
||||
} else {
|
||||
groupDatabase.create(groupId.requireMms(), memberIds);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
@ -88,16 +94,19 @@ final class V1GroupManager {
|
|||
|
||||
memberAddresses.add(Recipient.self().getId());
|
||||
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
|
||||
groupDatabase.updateTitle(groupId, name);
|
||||
groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
|
||||
|
||||
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, groupId.requireV1(), memberAddresses, name, avatarBytes);
|
||||
return sendGroupUpdate(context, groupIdV1, memberAddresses, name, avatarBytes);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
|
|
|
@ -38,9 +38,9 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private @NonNull GroupId groupId;
|
||||
private @NonNull GroupId.V1 groupId;
|
||||
|
||||
public AvatarDownloadJob(@NonNull GroupId groupId) {
|
||||
public AvatarDownloadJob(@NonNull GroupId.V1 groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
|
@ -48,7 +48,7 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
groupId);
|
||||
}
|
||||
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId groupId) {
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull GroupId.V1 groupId) {
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ public class AvatarDownloadJob extends BaseJob {
|
|||
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
|
||||
@Override
|
||||
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)));
|
||||
return new AvatarDownloadJob(parameters, GroupId.parse(data.getString(KEY_GROUP_ID)).requireV1());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -232,7 +232,7 @@ public class MmsDownloadJob extends BaseJob {
|
|||
|
||||
if (members.size() > 2) {
|
||||
List<RecipientId> recipients = new ArrayList<>(members);
|
||||
group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(recipients, true));
|
||||
group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients));
|
||||
}
|
||||
|
||||
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false, false);
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.klinker.android.send_message.Utils;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
|
@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -231,7 +233,7 @@ public final class MmsSendJob extends SendJob {
|
|||
}
|
||||
|
||||
if (message.getRecipient().isMmsGroup()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), false);
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient member : members) {
|
||||
if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) {
|
||||
|
@ -271,7 +273,7 @@ public final class MmsSendJob extends SendJob {
|
|||
PduPart part = new PduPart();
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = String.valueOf(Math.abs(Util.getSecureRandom().nextLong()));
|
||||
fileName = String.valueOf(Math.abs(new SecureRandom().nextLong()));
|
||||
String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(attachment.getContentType());
|
||||
|
||||
if (fileExtension != null) fileName = fileName + "." + fileExtension;
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
|
@ -299,7 +300,7 @@ public class PushGroupSendJob extends PushSendJob {
|
|||
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
|
||||
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
|
||||
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
return Stream.of(members).map(Recipient::getId).toList();
|
||||
}
|
||||
|
||||
|
|
|
@ -1030,7 +1030,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) {
|
||||
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
List<Recipient> messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList();
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, false);
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
Map<RecipientId, Integer> localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId))
|
||||
.collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus));
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
|
|||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -137,7 +138,7 @@ public class RetrieveProfileJob extends BaseJob {
|
|||
}
|
||||
|
||||
private void handleGroupRecipient(Recipient group) throws IOException {
|
||||
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), false);
|
||||
List<Recipient> recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(group.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
handleIndividualRecipient(recipient);
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -85,7 +86,7 @@ public class TypingSendJob extends BaseJob {
|
|||
Optional<byte[]> groupId = Optional.absent();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), false);
|
||||
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
||||
groupId = Optional.of(recipient.requireGroupId().getDecodedId());
|
||||
}
|
||||
|
||||
|
|
|
@ -428,13 +428,13 @@ public class Util {
|
|||
}
|
||||
|
||||
public static byte[] getSecretBytes(int size) {
|
||||
byte[] secret = new byte[size];
|
||||
getSecureRandom().nextBytes(secret);
|
||||
return secret;
|
||||
return getSecretBytes(new SecureRandom(), size);
|
||||
}
|
||||
|
||||
public static SecureRandom getSecureRandom() {
|
||||
return new SecureRandom();
|
||||
public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size) {
|
||||
byte[] secret = new byte[size];
|
||||
secureRandom.nextBytes(secret);
|
||||
return secret;
|
||||
}
|
||||
|
||||
public static int getDaysTillBuildExpiry() {
|
||||
|
|
|
@ -17,6 +17,7 @@ import static org.junit.Assert.assertNull;
|
|||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.groups.ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS;
|
||||
import static org.thoughtcrime.securesms.testutil.SecureRandomTestUtil.mockRandom;
|
||||
|
||||
public final class GroupIdTest {
|
||||
|
||||
|
@ -264,4 +265,20 @@ public final class GroupIdTest {
|
|||
public void cannot_create_v2_with_a_v1_length() throws IOException {
|
||||
GroupId.v2(Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_mms() {
|
||||
GroupId.Mms mms = GroupId.createMms(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 }));
|
||||
|
||||
assertEquals("__signal_mms_group__!090a0b0c0d0e0f000102030405060708", mms.toString());
|
||||
assertTrue(mms.isMms());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void create_v1() {
|
||||
GroupId.V1 v1 = GroupId.createV1(mockRandom(new byte[]{ 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8 }));
|
||||
|
||||
assertEquals("__textsecure_group__!090a0b0c0d0e0f000102030405060708", v1.toString());
|
||||
assertTrue(v1.isV1());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DecryptedGroupUtil {
|
||||
|
||||
public static Set<UUID> toUuidSet(Collection<DecryptedMember> membersList) {
|
||||
HashSet<UUID> uuids = new HashSet<>(membersList.size());
|
||||
|
||||
for (DecryptedMember member : membersList) {
|
||||
uuids.add(toUuid(member));
|
||||
}
|
||||
|
||||
return uuids;
|
||||
}
|
||||
|
||||
public static ArrayList<UUID> toUuidList(Collection<DecryptedMember> membersList) {
|
||||
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
||||
|
||||
for (DecryptedMember member : membersList) {
|
||||
uuidList.add(toUuid(member));
|
||||
}
|
||||
|
||||
return uuidList;
|
||||
}
|
||||
|
||||
public static ArrayList<UUID> pendingToUuidList(Collection<DecryptedPendingMember> membersList) {
|
||||
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
|
||||
|
||||
for (DecryptedPendingMember member : membersList) {
|
||||
uuidList.add(toUuid(member));
|
||||
}
|
||||
|
||||
return uuidList;
|
||||
}
|
||||
|
||||
public static UUID toUuid(DecryptedMember member) {
|
||||
return UUIDUtil.deserialize(member.getUuid().toByteArray());
|
||||
}
|
||||
|
||||
public static UUID toUuid(DecryptedPendingMember member) {
|
||||
return UUIDUtil.deserialize(member.getUuid().toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* The UUID of the member that made the change.
|
||||
*/
|
||||
public static UUID editorUuid(DecryptedGroupChange change) {
|
||||
return UuidUtil.fromByteString(change.getEditor());
|
||||
}
|
||||
|
||||
public static Optional<DecryptedMember> findMemberByUuid(Collection<DecryptedMember> members, UUID uuid) {
|
||||
ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
|
||||
for (DecryptedMember member : members) {
|
||||
if (uuidBytes.equals(member.getUuid())) {
|
||||
return Optional.of(member);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
public static Optional<DecryptedPendingMember> findPendingByUuid(Collection<DecryptedPendingMember> members, UUID uuid) {
|
||||
ByteString uuidBytes = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
|
||||
for (DecryptedPendingMember member : members) {
|
||||
if (uuidBytes.equals(member.getUuid())) {
|
||||
return Optional.of(member);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the uuid from the full members of a group.
|
||||
* <p>
|
||||
* Generally not expected to have to do this, just in the case of leaving a group where you cannot
|
||||
* get the new group state as you are not in the group any longer.
|
||||
*/
|
||||
public static DecryptedGroup removeMember(DecryptedGroup group, UUID uuid, int revision) {
|
||||
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
|
||||
ByteString uuidString = UuidUtil.toByteString(uuid);
|
||||
boolean removed = false;
|
||||
ArrayList<DecryptedMember> decryptedMembers = new ArrayList<>(builder.getMembersList());
|
||||
Iterator<DecryptedMember> membersList = decryptedMembers.iterator();
|
||||
|
||||
while (membersList.hasNext()) {
|
||||
if (uuidString.equals(membersList.next().getUuid())) {
|
||||
membersList.remove();
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
return builder.clearMembers()
|
||||
.addAllMembers(decryptedMembers)
|
||||
.setVersion(revision)
|
||||
.build();
|
||||
} else {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public final class DecryptedGroupUtilTest {
|
||||
|
||||
@Test
|
||||
public void can_extract_uuid_from_decrypted_member() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
DecryptedMember decryptedMember = DecryptedMember.newBuilder()
|
||||
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
|
||||
.build();
|
||||
|
||||
UUID parsed = DecryptedGroupUtil.toUuid(decryptedMember);
|
||||
|
||||
assertEquals(uuid, parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void can_extract_editor_uuid_from_decrypted_group_change() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
ByteString editor = ByteString.copyFrom(UUIDUtil.serialize(uuid));
|
||||
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder()
|
||||
.setEditor(editor)
|
||||
.build();
|
||||
|
||||
UUID parsed = DecryptedGroupUtil.editorUuid(groupChange);
|
||||
|
||||
assertEquals(uuid, parsed);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue