mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Refactor group store
This commit is contained in:
parent
a1014ba39c
commit
5b8c0c4e2d
14 changed files with 644 additions and 473 deletions
|
@ -265,6 +265,10 @@ public class Manager implements Closeable {
|
|||
return account.getSelfAddress();
|
||||
}
|
||||
|
||||
public RecipientId getSelfRecipientId() {
|
||||
return account.getSelfRecipientId();
|
||||
}
|
||||
|
||||
private IdentityKeyPair getIdentityKeyPair() {
|
||||
return account.getIdentityKeyPair();
|
||||
}
|
||||
|
@ -673,7 +677,7 @@ public class Manager implements Closeable {
|
|||
if (g == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
if (!g.isMember(account.getSelfAddress())) {
|
||||
if (!g.isMember(account.getSelfRecipientId())) {
|
||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||
}
|
||||
return g;
|
||||
|
@ -684,7 +688,7 @@ public class Manager implements Closeable {
|
|||
if (g == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) {
|
||||
if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
|
||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||
}
|
||||
return g;
|
||||
|
@ -725,7 +729,7 @@ public class Manager implements Closeable {
|
|||
GroupUtils.setGroupContext(messageBuilder, g);
|
||||
messageBuilder.withExpiration(g.getMessageExpirationTime());
|
||||
|
||||
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
|
||||
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
public Pair<Long, List<SendMessageResult>> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException {
|
||||
|
@ -736,17 +740,17 @@ public class Manager implements Closeable {
|
|||
var groupInfoV1 = (GroupInfoV1) g;
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build();
|
||||
messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
|
||||
groupInfoV1.removeMember(account.getSelfAddress());
|
||||
groupInfoV1.removeMember(account.getSelfRecipientId());
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
} else {
|
||||
final var groupInfoV2 = (GroupInfoV2) g;
|
||||
final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
|
||||
groupInfoV2.setGroup(groupGroupChangePair.first());
|
||||
groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
|
||||
messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
}
|
||||
|
||||
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
|
||||
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
public Pair<GroupId, List<SendMessageResult>> updateGroup(
|
||||
|
@ -754,11 +758,7 @@ public class Manager implements Closeable {
|
|||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
|
||||
return sendUpdateGroupMessage(groupId,
|
||||
name,
|
||||
members == null
|
||||
? null
|
||||
: getSignalServiceAddresses(members).stream()
|
||||
.map(this::resolveRecipient)
|
||||
.collect(Collectors.toSet()),
|
||||
members == null ? null : getSignalServiceAddresses(members),
|
||||
avatarFile);
|
||||
}
|
||||
|
||||
|
@ -769,16 +769,20 @@ public class Manager implements Closeable {
|
|||
SignalServiceDataMessage.Builder messageBuilder;
|
||||
if (groupId == null) {
|
||||
// Create new group
|
||||
var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
|
||||
var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
|
||||
members == null ? Set.of() : members,
|
||||
avatarFile);
|
||||
if (gv2 == null) {
|
||||
if (gv2Pair == null) {
|
||||
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
|
||||
gv1.addMembers(List.of(account.getSelfAddress()));
|
||||
gv1.addMembers(List.of(account.getSelfRecipientId()));
|
||||
updateGroupV1(gv1, name, members, avatarFile);
|
||||
messageBuilder = getGroupUpdateMessageBuilder(gv1);
|
||||
g = gv1;
|
||||
} else {
|
||||
final var gv2 = gv2Pair.first();
|
||||
final var decryptedGroup = gv2Pair.second();
|
||||
|
||||
gv2.setGroup(decryptedGroup, this::resolveRecipient);
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(gv2.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
|
@ -792,7 +796,7 @@ public class Manager implements Closeable {
|
|||
final var groupInfoV2 = (GroupInfoV2) group;
|
||||
|
||||
Pair<Long, List<SendMessageResult>> result = null;
|
||||
if (groupInfoV2.isPendingMember(getSelfAddress())) {
|
||||
if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
|
||||
var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
|
||||
result = sendUpdateGroupMessage(groupInfoV2,
|
||||
groupGroupChangePair.first(),
|
||||
|
@ -801,10 +805,7 @@ public class Manager implements Closeable {
|
|||
|
||||
if (members != null) {
|
||||
final var newMembers = new HashSet<>(members);
|
||||
newMembers.removeAll(group.getMembers()
|
||||
.stream()
|
||||
.map(this::resolveRecipient)
|
||||
.collect(Collectors.toSet()));
|
||||
newMembers.removeAll(group.getMembers());
|
||||
if (newMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
|
||||
result = sendUpdateGroupMessage(groupInfoV2,
|
||||
|
@ -834,7 +835,8 @@ public class Manager implements Closeable {
|
|||
|
||||
account.getGroupStore().updateGroup(g);
|
||||
|
||||
final var result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress()));
|
||||
final var result = sendMessage(messageBuilder,
|
||||
g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
return new Pair<>(g.getGroupId(), result.second());
|
||||
}
|
||||
|
||||
|
@ -846,12 +848,13 @@ public class Manager implements Closeable {
|
|||
}
|
||||
|
||||
if (members != null) {
|
||||
final var memberAddresses = members.stream()
|
||||
final var newMemberAddresses = members.stream()
|
||||
.filter(member -> !g.isMember(member))
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList());
|
||||
final var newE164Members = new HashSet<String>();
|
||||
for (var member : memberAddresses) {
|
||||
if (g.isMember(member) || !member.getNumber().isPresent()) {
|
||||
for (var member : newMemberAddresses) {
|
||||
if (!member.getNumber().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
newE164Members.add(member.getNumber().get());
|
||||
|
@ -866,7 +869,7 @@ public class Manager implements Closeable {
|
|||
+ " to group: Not registered on Signal");
|
||||
}
|
||||
|
||||
g.addMembers(memberAddresses);
|
||||
g.addMembers(members);
|
||||
}
|
||||
|
||||
if (avatarFile != null) {
|
||||
|
@ -928,10 +931,10 @@ public class Manager implements Closeable {
|
|||
private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
|
||||
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
|
||||
) throws IOException {
|
||||
group.setGroup(newDecryptedGroup);
|
||||
group.setGroup(newDecryptedGroup, this::resolveRecipient);
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
|
||||
account.getGroupStore().updateGroup(group);
|
||||
return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress()));
|
||||
return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
|
||||
|
@ -944,21 +947,24 @@ public class Manager implements Closeable {
|
|||
}
|
||||
g = (GroupInfoV1) group;
|
||||
|
||||
if (!g.isMember(recipient)) {
|
||||
if (!g.isMember(resolveRecipient(recipient))) {
|
||||
throw new NotAGroupMemberException(groupId, g.name);
|
||||
}
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(g);
|
||||
|
||||
// Send group message only to the recipient who requested it
|
||||
return sendMessage(messageBuilder, List.of(recipient));
|
||||
return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
||||
.withId(g.getGroupId().serialize())
|
||||
.withName(g.name)
|
||||
.withMembers(new ArrayList<>(g.getMembers()));
|
||||
.withMembers(g.getMembers()
|
||||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
try {
|
||||
final var attachment = createGroupAvatarAttachment(g.getGroupId());
|
||||
|
@ -991,7 +997,7 @@ public class Manager implements Closeable {
|
|||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
||||
|
||||
// Send group info request message to the recipient who sent us a message with this groupId
|
||||
return sendMessage(messageBuilder, List.of(recipient));
|
||||
return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
|
||||
}
|
||||
|
||||
void sendReceipt(
|
||||
|
@ -1126,9 +1132,9 @@ public class Manager implements Closeable {
|
|||
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
|
||||
}
|
||||
|
||||
private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
|
||||
private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException {
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
|
||||
sendMessage(messageBuilder, List.of(address));
|
||||
sendMessage(messageBuilder, Set.of(recipientId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1139,7 +1145,7 @@ public class Manager implements Closeable {
|
|||
) throws IOException, InvalidNumberException {
|
||||
var recipientId = canonicalizeAndResolveRecipient(number);
|
||||
setExpirationTimer(recipientId, messageExpirationTimer);
|
||||
sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
|
||||
sendExpirationTimerUpdate(recipientId);
|
||||
account.save();
|
||||
}
|
||||
|
||||
|
@ -1266,7 +1272,7 @@ public class Manager implements Closeable {
|
|||
messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
|
||||
}
|
||||
|
||||
private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
|
||||
private Set<RecipientId> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
|
||||
final var signalServiceAddresses = new HashSet<SignalServiceAddress>(numbers.size());
|
||||
final var addressesMissingUuid = new HashSet<SignalServiceAddress>();
|
||||
|
||||
|
@ -1302,7 +1308,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
return signalServiceAddresses;
|
||||
return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Map<String, UUID> getRegisteredUsers(final Set<String> numbersMissingUuid) throws IOException {
|
||||
|
@ -1316,10 +1322,8 @@ public class Manager implements Closeable {
|
|||
}
|
||||
|
||||
private Pair<Long, List<SendMessageResult>> sendMessage(
|
||||
SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
|
||||
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientId> recipientIds
|
||||
) throws IOException {
|
||||
recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet());
|
||||
final var recipientIds = recipients.stream().map(this::resolveRecipient).collect(Collectors.toSet());
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
messageBuilder.withTimestamp(timestamp);
|
||||
getOrCreateMessagePipe();
|
||||
|
@ -1331,8 +1335,12 @@ public class Manager implements Closeable {
|
|||
try {
|
||||
var messageSender = createMessageSender();
|
||||
final var isRecipientUpdate = false;
|
||||
var result = messageSender.sendMessage(new ArrayList<>(recipients),
|
||||
unidentifiedAccessHelper.getAccessFor(recipientIds),
|
||||
final var recipientIdList = new ArrayList<>(recipientIds);
|
||||
final var addresses = recipientIdList.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList());
|
||||
var result = messageSender.sendMessage(addresses,
|
||||
unidentifiedAccessHelper.getAccessFor(recipientIdList),
|
||||
isRecipientUpdate,
|
||||
message);
|
||||
|
||||
|
@ -1352,19 +1360,19 @@ public class Manager implements Closeable {
|
|||
} else {
|
||||
// Send to all individually, so sync messages are sent correctly
|
||||
messageBuilder.withProfileKey(account.getProfileKey().serialize());
|
||||
var results = new ArrayList<SendMessageResult>(recipients.size());
|
||||
for (var address : recipients) {
|
||||
final var contact = account.getContactStore().getContact(resolveRecipient(address));
|
||||
var results = new ArrayList<SendMessageResult>(recipientIds.size());
|
||||
for (var recipientId : recipientIds) {
|
||||
final var contact = account.getContactStore().getContact(recipientId);
|
||||
final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
|
||||
messageBuilder.withExpiration(expirationTime);
|
||||
message = messageBuilder.build();
|
||||
results.add(sendMessage(address, message));
|
||||
results.add(sendMessage(resolveSignalServiceAddress(recipientId), message));
|
||||
}
|
||||
return new Pair<>(timestamp, results);
|
||||
}
|
||||
} finally {
|
||||
if (message != null && message.isEndSession()) {
|
||||
for (var recipient : recipients) {
|
||||
for (var recipient : recipientIds) {
|
||||
handleEndSession(recipient);
|
||||
}
|
||||
}
|
||||
|
@ -1448,8 +1456,8 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleEndSession(SignalServiceAddress source) {
|
||||
account.getSessionStore().deleteAllSessions(source.getIdentifier());
|
||||
private void handleEndSession(RecipientId recipientId) {
|
||||
account.getSessionStore().deleteAllSessions(recipientId);
|
||||
}
|
||||
|
||||
private List<HandleAction> handleSignalServiceDataMessage(
|
||||
|
@ -1486,7 +1494,7 @@ public class Manager implements Closeable {
|
|||
groupV1.addMembers(groupInfo.getMembers()
|
||||
.get()
|
||||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.map(this::resolveRecipient)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
|
@ -1500,7 +1508,7 @@ public class Manager implements Closeable {
|
|||
break;
|
||||
case QUIT: {
|
||||
if (groupV1 != null) {
|
||||
groupV1.removeMember(source);
|
||||
groupV1.removeMember(resolveRecipient(source));
|
||||
account.getGroupStore().updateGroup(groupV1);
|
||||
}
|
||||
break;
|
||||
|
@ -1527,7 +1535,7 @@ public class Manager implements Closeable {
|
|||
|
||||
final var conversationPartnerAddress = isSync ? destination : source;
|
||||
if (conversationPartnerAddress != null && message.isEndSession()) {
|
||||
handleEndSession(conversationPartnerAddress);
|
||||
handleEndSession(resolveRecipient(conversationPartnerAddress));
|
||||
}
|
||||
if (message.isExpirationUpdate() || message.getBody().isPresent()) {
|
||||
if (message.getGroupContext().isPresent()) {
|
||||
|
@ -1613,7 +1621,7 @@ public class Manager implements Closeable {
|
|||
final GroupInfoV2 groupInfoV2;
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
// Received a v2 group message for a v1 group, we need to locally migrate the group
|
||||
account.getGroupStore().deleteGroup(groupInfo.getGroupId());
|
||||
account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
|
||||
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
|
||||
logger.info("Locally migrated group {} to group v2, id: {}",
|
||||
groupInfo.getGroupId().toBase64(),
|
||||
|
@ -1641,7 +1649,7 @@ public class Manager implements Closeable {
|
|||
downloadGroupAvatar(groupId, groupSecretParams, avatar);
|
||||
}
|
||||
}
|
||||
groupInfoV2.setGroup(group);
|
||||
groupInfoV2.setGroup(group, this::resolveRecipient);
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
}
|
||||
|
||||
|
@ -1885,7 +1893,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
var groupId = GroupUtils.getGroupId(message.getGroupContext().get());
|
||||
var group = getGroup(groupId);
|
||||
if (group != null && !group.isMember(source)) {
|
||||
if (group != null && !group.isMember(resolveRecipient(source))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1959,13 +1967,13 @@ public class Manager implements Closeable {
|
|||
}
|
||||
syncGroup.addMembers(g.getMembers()
|
||||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.map(this::resolveRecipient)
|
||||
.collect(Collectors.toSet()));
|
||||
if (!g.isActive()) {
|
||||
syncGroup.removeMember(account.getSelfAddress());
|
||||
syncGroup.removeMember(account.getSelfRecipientId());
|
||||
} else {
|
||||
// Add ourself to the member set as it's marked as active
|
||||
syncGroup.addMembers(List.of(account.getSelfAddress()));
|
||||
syncGroup.addMembers(List.of(account.getSelfRecipientId()));
|
||||
}
|
||||
syncGroup.blocked = g.isBlocked();
|
||||
if (g.getColor().isPresent()) {
|
||||
|
@ -1975,7 +1983,6 @@ public class Manager implements Closeable {
|
|||
if (g.getAvatar().isPresent()) {
|
||||
downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
|
||||
}
|
||||
syncGroup.inboxPosition = g.getInboxPosition().orNull();
|
||||
syncGroup.archived = g.isArchived();
|
||||
account.getGroupStore().updateGroup(syncGroup);
|
||||
}
|
||||
|
@ -2282,13 +2289,16 @@ public class Manager implements Closeable {
|
|||
var groupInfo = (GroupInfoV1) record;
|
||||
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
|
||||
Optional.fromNullable(groupInfo.name),
|
||||
new ArrayList<>(groupInfo.getMembers()),
|
||||
groupInfo.getMembers()
|
||||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList()),
|
||||
createGroupAvatarAttachment(groupInfo.getGroupId()),
|
||||
groupInfo.isMember(account.getSelfAddress()),
|
||||
groupInfo.isMember(account.getSelfRecipientId()),
|
||||
Optional.of(groupInfo.messageExpirationTime),
|
||||
Optional.fromNullable(groupInfo.color),
|
||||
groupInfo.blocked,
|
||||
Optional.fromNullable(groupInfo.inboxPosition),
|
||||
Optional.absent(),
|
||||
groupInfo.archived));
|
||||
}
|
||||
}
|
||||
|
@ -2434,7 +2444,7 @@ public class Manager implements Closeable {
|
|||
final var group = account.getGroupStore().getGroup(groupId);
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
|
||||
((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams));
|
||||
((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
|
||||
account.getGroupStore().updateGroup(group);
|
||||
}
|
||||
return group;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.asamk.signal.manager.groups;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes;
|
||||
|
||||
public class GroupIdV1 extends GroupId {
|
||||
|
@ -8,6 +10,10 @@ public class GroupIdV1 extends GroupId {
|
|||
return new GroupIdV1(getSecretBytes(16));
|
||||
}
|
||||
|
||||
public static GroupIdV1 fromBase64(String groupId) {
|
||||
return new GroupIdV1(Base64.getDecoder().decode(groupId));
|
||||
}
|
||||
|
||||
public GroupIdV1(final byte[] id) {
|
||||
super(id);
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ public class GroupHelper {
|
|||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
|
||||
}
|
||||
|
||||
public GroupInfoV2 createGroupV2(
|
||||
public Pair<GroupInfoV2, DecryptedGroup> createGroupV2(
|
||||
String name, Set<RecipientId> members, File avatarFile
|
||||
) throws IOException {
|
||||
final var avatarBytes = readAvatarBytes(avatarFile);
|
||||
|
@ -129,9 +129,8 @@ public class GroupHelper {
|
|||
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
|
||||
final var masterKey = groupSecretParams.getMasterKey();
|
||||
var g = new GroupInfoV2(groupId, masterKey);
|
||||
g.setGroup(decryptedGroup);
|
||||
|
||||
return g;
|
||||
return new Pair<>(g, decryptedGroup);
|
||||
}
|
||||
|
||||
private byte[] readAvatarBytes(final File avatarFile) throws IOException {
|
||||
|
|
|
@ -6,7 +6,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -76,7 +75,7 @@ public class UnidentifiedAccessHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
|
||||
public List<Optional<UnidentifiedAccessPair>> getAccessFor(List<RecipientId> recipients) {
|
||||
return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import org.asamk.signal.manager.groups.GroupId;
|
|||
import org.asamk.signal.manager.storage.contacts.ContactsStore;
|
||||
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.JsonGroupStore;
|
||||
import org.asamk.signal.manager.storage.groups.GroupStore;
|
||||
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
||||
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
||||
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
|
||||
|
@ -86,7 +86,8 @@ public class SignalAccount implements Closeable {
|
|||
private SignedPreKeyStore signedPreKeyStore;
|
||||
private SessionStore sessionStore;
|
||||
private IdentityKeyStore identityKeyStore;
|
||||
private JsonGroupStore groupStore;
|
||||
private GroupStore groupStore;
|
||||
private GroupStore.Storage groupStoreStorage;
|
||||
private RecipientStore recipientStore;
|
||||
private StickerStore stickerStore;
|
||||
private StickerStore.Storage stickerStoreStorage;
|
||||
|
@ -130,7 +131,9 @@ public class SignalAccount implements Closeable {
|
|||
account.profileKey = profileKey;
|
||||
|
||||
account.initStores(dataPath, identityKey, registrationId);
|
||||
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
|
||||
account.recipientStore::resolveRecipient,
|
||||
account::saveGroupStore);
|
||||
account.stickerStore = new StickerStore(account::saveStickerStore);
|
||||
|
||||
account.registered = false;
|
||||
|
@ -183,7 +186,9 @@ public class SignalAccount implements Closeable {
|
|||
account.deviceId = deviceId;
|
||||
|
||||
account.initStores(dataPath, identityKey, registrationId);
|
||||
account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
|
||||
account.recipientStore::resolveRecipient,
|
||||
account::saveGroupStore);
|
||||
account.stickerStore = new StickerStore(account::saveStickerStore);
|
||||
|
||||
account.registered = true;
|
||||
|
@ -209,6 +214,7 @@ public class SignalAccount implements Closeable {
|
|||
sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
}
|
||||
|
||||
public static File getFileName(File dataPath, String username) {
|
||||
|
@ -331,13 +337,16 @@ public class SignalAccount implements Closeable {
|
|||
|
||||
loadLegacyStores(rootNode, legacySignalProtocolStore);
|
||||
|
||||
var groupStoreNode = rootNode.get("groupStore");
|
||||
if (groupStoreNode != null) {
|
||||
groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
|
||||
groupStore.groupCachePath = getGroupCachePath(dataPath, username);
|
||||
}
|
||||
if (groupStore == null) {
|
||||
groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
|
||||
if (rootNode.hasNonNull("groupStore")) {
|
||||
groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
|
||||
groupStore = GroupStore.fromStorage(groupStoreStorage,
|
||||
getGroupCachePath(dataPath, username),
|
||||
recipientStore::resolveRecipient,
|
||||
this::saveGroupStore);
|
||||
} else {
|
||||
groupStore = new GroupStore(getGroupCachePath(dataPath, username),
|
||||
recipientStore::resolveRecipient,
|
||||
this::saveGroupStore);
|
||||
}
|
||||
|
||||
if (rootNode.hasNonNull("stickerStore")) {
|
||||
|
@ -510,6 +519,11 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
}
|
||||
|
||||
private void saveGroupStore(GroupStore.Storage storage) {
|
||||
this.groupStoreStorage = storage;
|
||||
save();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (fileChannel) {
|
||||
var rootNode = jsonProcessor.createObjectNode();
|
||||
|
@ -534,7 +548,7 @@ public class SignalAccount implements Closeable {
|
|||
.put("nextSignedPreKeyId", nextSignedPreKeyId)
|
||||
.put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
|
||||
.put("registered", registered)
|
||||
.putPOJO("groupStore", groupStore)
|
||||
.putPOJO("groupStore", groupStoreStorage)
|
||||
.putPOJO("stickerStore", stickerStoreStorage);
|
||||
try {
|
||||
try (var output = new ByteArrayOutputStream()) {
|
||||
|
@ -597,7 +611,7 @@ public class SignalAccount implements Closeable {
|
|||
return identityKeyStore;
|
||||
}
|
||||
|
||||
public JsonGroupStore getGroupStore() {
|
||||
public GroupStore getGroupStore() {
|
||||
return groupStore;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,15 +17,15 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static ObjectMapper createStorageObjectMapper() {
|
||||
final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
|
||||
jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
|
||||
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
|
||||
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
|
||||
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
|
||||
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
objectMapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
|
||||
objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
|
||||
|
||||
return jsonProcessor;
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -12,66 +10,43 @@ import java.util.stream.Stream;
|
|||
|
||||
public abstract class GroupInfo {
|
||||
|
||||
@JsonIgnore
|
||||
public abstract GroupId getGroupId();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract String getTitle();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract GroupInviteLinkUrl getGroupInviteLink();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract Set<SignalServiceAddress> getMembers();
|
||||
public abstract Set<RecipientId> getMembers();
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getPendingMembers() {
|
||||
public Set<RecipientId> getPendingMembers() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getRequestingMembers() {
|
||||
public Set<RecipientId> getRequestingMembers() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public abstract boolean isBlocked();
|
||||
|
||||
@JsonIgnore
|
||||
public abstract void setBlocked(boolean blocked);
|
||||
|
||||
@JsonIgnore
|
||||
public abstract int getMessageExpirationTime();
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembersWithout(SignalServiceAddress address) {
|
||||
return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet());
|
||||
public Set<RecipientId> getMembersWithout(RecipientId recipientId) {
|
||||
return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembersIncludingPendingWithout(SignalServiceAddress address) {
|
||||
public Set<RecipientId> getMembersIncludingPendingWithout(RecipientId recipientId) {
|
||||
return Stream.concat(getMembers().stream(), getPendingMembers().stream())
|
||||
.filter(member -> !member.matches(address))
|
||||
.filter(member -> !member.equals(recipientId))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isMember(SignalServiceAddress address) {
|
||||
for (var member : getMembers()) {
|
||||
if (member.matches(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public boolean isMember(RecipientId recipientId) {
|
||||
return getMembers().contains(recipientId);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isPendingMember(SignalServiceAddress address) {
|
||||
for (var member : getPendingMembers()) {
|
||||
if (member.matches(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public boolean isPendingMember(RecipientId recipientId) {
|
||||
return getPendingMembers().contains(recipientId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,27 @@
|
|||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GroupInfoV1 extends GroupInfo {
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
|
||||
private final GroupIdV1 groupId;
|
||||
|
||||
private GroupIdV2 expectedV2Id;
|
||||
|
||||
@JsonProperty
|
||||
public String name;
|
||||
|
||||
@JsonProperty
|
||||
@JsonDeserialize(using = MembersDeserializer.class)
|
||||
@JsonSerialize(using = MembersSerializer.class)
|
||||
public Set<SignalServiceAddress> members = new HashSet<>();
|
||||
@JsonProperty
|
||||
public Set<RecipientId> members = new HashSet<>();
|
||||
public String color;
|
||||
@JsonProperty(defaultValue = "0")
|
||||
public int messageExpirationTime;
|
||||
@JsonProperty(defaultValue = "false")
|
||||
public boolean blocked;
|
||||
@JsonProperty
|
||||
public Integer inboxPosition;
|
||||
@JsonProperty(defaultValue = "false")
|
||||
public boolean archived;
|
||||
|
||||
public GroupInfoV1(GroupIdV1 groupId) {
|
||||
|
@ -57,41 +29,30 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
}
|
||||
|
||||
public GroupInfoV1(
|
||||
@JsonProperty("groupId") byte[] groupId,
|
||||
@JsonProperty("expectedV2Id") byte[] expectedV2Id,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("members") Collection<SignalServiceAddress> members,
|
||||
@JsonProperty("avatarId") long _ignored_avatarId,
|
||||
@JsonProperty("color") String color,
|
||||
@JsonProperty("blocked") boolean blocked,
|
||||
@JsonProperty("inboxPosition") Integer inboxPosition,
|
||||
@JsonProperty("archived") boolean archived,
|
||||
@JsonProperty("messageExpirationTime") int messageExpirationTime,
|
||||
@JsonProperty("active") boolean _ignored_active
|
||||
final GroupIdV1 groupId,
|
||||
final GroupIdV2 expectedV2Id,
|
||||
final String name,
|
||||
final Set<RecipientId> members,
|
||||
final String color,
|
||||
final int messageExpirationTime,
|
||||
final boolean blocked,
|
||||
final boolean archived
|
||||
) {
|
||||
this.groupId = GroupId.v1(groupId);
|
||||
this.expectedV2Id = GroupId.v2(expectedV2Id);
|
||||
this.groupId = groupId;
|
||||
this.expectedV2Id = expectedV2Id;
|
||||
this.name = name;
|
||||
this.members.addAll(members);
|
||||
this.members = members;
|
||||
this.color = color;
|
||||
this.blocked = blocked;
|
||||
this.inboxPosition = inboxPosition;
|
||||
this.archived = archived;
|
||||
this.messageExpirationTime = messageExpirationTime;
|
||||
this.blocked = blocked;
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public GroupIdV1 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
@JsonProperty("groupId")
|
||||
private byte[] getGroupIdJackson() {
|
||||
return groupId.serialize();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public GroupIdV2 getExpectedV2Id() {
|
||||
if (expectedV2Id == null) {
|
||||
expectedV2Id = GroupUtils.getGroupIdV2(groupId);
|
||||
|
@ -99,11 +60,6 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
return expectedV2Id;
|
||||
}
|
||||
|
||||
@JsonProperty("expectedV2Id")
|
||||
private byte[] getExpectedV2IdJackson() {
|
||||
return getExpectedV2Id().serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return name;
|
||||
|
@ -114,8 +70,7 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
return null;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Set<SignalServiceAddress> getMembers() {
|
||||
public Set<RecipientId> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
|
@ -134,79 +89,11 @@ public class GroupInfoV1 extends GroupInfo {
|
|||
return messageExpirationTime;
|
||||
}
|
||||
|
||||
public void addMembers(Collection<SignalServiceAddress> addresses) {
|
||||
for (var address : addresses) {
|
||||
if (this.members.contains(address)) {
|
||||
continue;
|
||||
}
|
||||
removeMember(address);
|
||||
this.members.add(address);
|
||||
}
|
||||
public void addMembers(Collection<RecipientId> members) {
|
||||
this.members.addAll(members);
|
||||
}
|
||||
|
||||
public void removeMember(SignalServiceAddress address) {
|
||||
this.members.removeIf(member -> member.matches(address));
|
||||
}
|
||||
|
||||
private static final class JsonSignalServiceAddress {
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
|
||||
@JsonProperty
|
||||
private String number;
|
||||
|
||||
JsonSignalServiceAddress(@JsonProperty("uuid") final UUID uuid, @JsonProperty("number") final String number) {
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
JsonSignalServiceAddress(SignalServiceAddress address) {
|
||||
this.uuid = address.getUuid().orNull();
|
||||
this.number = address.getNumber().orNull();
|
||||
}
|
||||
|
||||
SignalServiceAddress toSignalServiceAddress() {
|
||||
return new SignalServiceAddress(uuid, number);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersSerializer extends JsonSerializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
final Set<SignalServiceAddress> value, final JsonGenerator jgen, final SerializerProvider provider
|
||||
) throws IOException {
|
||||
jgen.writeStartArray(value.size());
|
||||
for (var address : value) {
|
||||
if (address.getUuid().isPresent()) {
|
||||
jgen.writeObject(new JsonSignalServiceAddress(address));
|
||||
} else {
|
||||
jgen.writeString(address.getNumber().get());
|
||||
}
|
||||
}
|
||||
jgen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersDeserializer extends JsonDeserializer<Set<SignalServiceAddress>> {
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var addresses = new HashSet<SignalServiceAddress>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (var n : node) {
|
||||
if (n.isTextual()) {
|
||||
addresses.add(new SignalServiceAddress(null, n.textValue()));
|
||||
} else {
|
||||
var address = jsonProcessor.treeToValue(n, JsonSignalServiceAddress.class);
|
||||
addresses.add(address.toSignalServiceAddress());
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
public void removeMember(RecipientId recipientId) {
|
||||
this.members.removeIf(member -> member.equals(recipientId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.asamk.signal.manager.storage.groups;
|
|||
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
|
@ -18,12 +20,19 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
|
||||
private boolean blocked;
|
||||
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
|
||||
private RecipientResolver recipientResolver;
|
||||
|
||||
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
|
||||
this.groupId = groupId;
|
||||
this.masterKey = masterKey;
|
||||
}
|
||||
|
||||
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
|
||||
this.groupId = groupId;
|
||||
this.masterKey = masterKey;
|
||||
this.blocked = blocked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupIdV2 getGroupId() {
|
||||
return groupId;
|
||||
|
@ -33,8 +42,9 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
return masterKey;
|
||||
}
|
||||
|
||||
public void setGroup(final DecryptedGroup group) {
|
||||
public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
|
||||
this.group = group;
|
||||
this.recipientResolver = recipientResolver;
|
||||
}
|
||||
|
||||
public DecryptedGroup getGroup() {
|
||||
|
@ -63,35 +73,38 @@ public class GroupInfoV2 extends GroupInfo {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> getMembers() {
|
||||
public Set<RecipientId> getMembers() {
|
||||
if (this.group == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return group.getMembersList()
|
||||
.stream()
|
||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
|
||||
.map(recipientResolver::resolveRecipient)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> getPendingMembers() {
|
||||
public Set<RecipientId> getPendingMembers() {
|
||||
if (this.group == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return group.getPendingMembersList()
|
||||
.stream()
|
||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
|
||||
.map(recipientResolver::resolveRecipient)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SignalServiceAddress> getRequestingMembers() {
|
||||
public Set<RecipientId> getRequestingMembers() {
|
||||
if (this.group == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return group.getRequestingMembersList()
|
||||
.stream()
|
||||
.map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null))
|
||||
.map(recipientResolver::resolveRecipient)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,474 @@
|
|||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.util.Hex;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GroupStore {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(GroupStore.class);
|
||||
|
||||
private final File groupCachePath;
|
||||
private final Map<GroupId, GroupInfo> groups;
|
||||
private final RecipientResolver recipientResolver;
|
||||
private final Saver saver;
|
||||
|
||||
private GroupStore(
|
||||
final File groupCachePath,
|
||||
final Map<GroupId, GroupInfo> groups,
|
||||
final RecipientResolver recipientResolver,
|
||||
final Saver saver
|
||||
) {
|
||||
this.groupCachePath = groupCachePath;
|
||||
this.groups = groups;
|
||||
this.recipientResolver = recipientResolver;
|
||||
this.saver = saver;
|
||||
}
|
||||
|
||||
public GroupStore(
|
||||
final File groupCachePath, final RecipientResolver recipientResolver, final Saver saver
|
||||
) {
|
||||
this.groups = new HashMap<>();
|
||||
this.groupCachePath = groupCachePath;
|
||||
this.recipientResolver = recipientResolver;
|
||||
this.saver = saver;
|
||||
}
|
||||
|
||||
public static GroupStore fromStorage(
|
||||
final Storage storage,
|
||||
final File groupCachePath,
|
||||
final RecipientResolver recipientResolver,
|
||||
final Saver saver
|
||||
) {
|
||||
final var groups = storage.groups.stream().map(g -> {
|
||||
if (g instanceof Storage.GroupV1) {
|
||||
final var g1 = (Storage.GroupV1) g;
|
||||
final var members = g1.members.stream().map(m -> {
|
||||
if (m.recipientId == null) {
|
||||
return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid),
|
||||
m.number));
|
||||
}
|
||||
|
||||
return RecipientId.of(m.recipientId);
|
||||
}).collect(Collectors.toSet());
|
||||
|
||||
return new GroupInfoV1(GroupIdV1.fromBase64(g1.groupId),
|
||||
g1.expectedV2Id == null ? null : GroupIdV2.fromBase64(g1.expectedV2Id),
|
||||
g1.name,
|
||||
members,
|
||||
g1.color,
|
||||
g1.messageExpirationTime,
|
||||
g1.blocked,
|
||||
g1.archived);
|
||||
}
|
||||
|
||||
final var g2 = (Storage.GroupV2) g;
|
||||
var groupId = GroupIdV2.fromBase64(g2.groupId);
|
||||
GroupMasterKey masterKey;
|
||||
try {
|
||||
masterKey = new GroupMasterKey(Base64.getDecoder().decode(g2.masterKey));
|
||||
} catch (InvalidInputException | IllegalArgumentException e) {
|
||||
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
|
||||
}
|
||||
|
||||
return new GroupInfoV2(groupId, masterKey, g2.blocked);
|
||||
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
|
||||
|
||||
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
|
||||
}
|
||||
|
||||
public void updateGroup(GroupInfo group) {
|
||||
final Storage storage;
|
||||
synchronized (groups) {
|
||||
groups.put(group.getGroupId(), group);
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
|
||||
try {
|
||||
IOUtils.createPrivateDirectories(groupCachePath);
|
||||
try (var stream = new FileOutputStream(getGroupV2File(group.getGroupId()))) {
|
||||
((GroupInfoV2) group).getGroup().writeTo(stream);
|
||||
}
|
||||
final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId());
|
||||
if (groupFileLegacy.exists()) {
|
||||
groupFileLegacy.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
storage = toStorageLocked();
|
||||
}
|
||||
saver.save(storage);
|
||||
}
|
||||
|
||||
public void deleteGroupV1(GroupIdV1 groupId) {
|
||||
final Storage storage;
|
||||
synchronized (groups) {
|
||||
groups.remove(groupId);
|
||||
storage = toStorageLocked();
|
||||
}
|
||||
saver.save(storage);
|
||||
}
|
||||
|
||||
public GroupInfo getGroup(GroupId groupId) {
|
||||
synchronized (groups) {
|
||||
return getGroupLocked(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
|
||||
synchronized (groups) {
|
||||
var group = getGroupLocked(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
return (GroupInfoV1) group;
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
return new GroupInfoV1(groupId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<GroupInfo> getGroups() {
|
||||
synchronized (groups) {
|
||||
final var groups = this.groups.values();
|
||||
for (var group : groups) {
|
||||
loadDecryptedGroupLocked(group);
|
||||
}
|
||||
return new ArrayList<>(groups);
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeRecipients(final RecipientId recipientId, final RecipientId toBeMergedRecipientId) {
|
||||
synchronized (groups) {
|
||||
var modified = false;
|
||||
for (var group : this.groups.values()) {
|
||||
if (group instanceof GroupInfoV1) {
|
||||
var groupV1 = (GroupInfoV1) group;
|
||||
if (groupV1.isMember(toBeMergedRecipientId)) {
|
||||
groupV1.removeMember(toBeMergedRecipientId);
|
||||
groupV1.addMembers(List.of(recipientId));
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
saver.save(toStorageLocked());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GroupInfo getGroupLocked(final GroupId groupId) {
|
||||
var group = groups.get(groupId);
|
||||
if (group == null) {
|
||||
if (groupId instanceof GroupIdV1) {
|
||||
group = getGroupByV1IdLocked((GroupIdV1) groupId);
|
||||
} else if (groupId instanceof GroupIdV2) {
|
||||
group = getGroupV1ByV2IdLocked((GroupIdV2) groupId);
|
||||
}
|
||||
}
|
||||
loadDecryptedGroupLocked(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
private GroupInfo getGroupByV1IdLocked(final GroupIdV1 groupId) {
|
||||
return groups.get(GroupUtils.getGroupIdV2(groupId));
|
||||
}
|
||||
|
||||
private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) {
|
||||
for (var g : groups.values()) {
|
||||
if (g instanceof GroupInfoV1) {
|
||||
final var gv1 = (GroupInfoV1) g;
|
||||
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
|
||||
return gv1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadDecryptedGroupLocked(final GroupInfo group) {
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
|
||||
var groupFile = getGroupV2File(group.getGroupId());
|
||||
if (!groupFile.exists()) {
|
||||
groupFile = getGroupV2FileLegacy(group.getGroupId());
|
||||
}
|
||||
if (!groupFile.exists()) {
|
||||
return;
|
||||
}
|
||||
try (var stream = new FileInputStream(groupFile)) {
|
||||
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream), recipientResolver);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getGroupV2FileLegacy(final GroupId groupId) {
|
||||
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
|
||||
}
|
||||
|
||||
private File getGroupV2File(final GroupId groupId) {
|
||||
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
|
||||
}
|
||||
|
||||
private Storage toStorageLocked() {
|
||||
return new Storage(groups.values().stream().map(g -> {
|
||||
if (g instanceof GroupInfoV1) {
|
||||
final var g1 = (GroupInfoV1) g;
|
||||
return new Storage.GroupV1(g1.getGroupId().toBase64(),
|
||||
g1.getExpectedV2Id().toBase64(),
|
||||
g1.name,
|
||||
g1.color,
|
||||
g1.messageExpirationTime,
|
||||
g1.blocked,
|
||||
g1.archived,
|
||||
g1.members.stream()
|
||||
.map(m -> new Storage.GroupV1.Member(m.getId(), null, null))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
final var g2 = (GroupInfoV2) g;
|
||||
return new Storage.GroupV2(g2.getGroupId().toBase64(),
|
||||
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
|
||||
g2.isBlocked());
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public static class Storage {
|
||||
|
||||
// @JsonSerialize(using = GroupsSerializer.class)
|
||||
@JsonDeserialize(using = GroupsDeserializer.class)
|
||||
public List<Storage.Group> groups;
|
||||
|
||||
// For deserialization
|
||||
public Storage() {
|
||||
}
|
||||
|
||||
public Storage(final List<Storage.Group> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
private abstract static class Group {
|
||||
|
||||
}
|
||||
|
||||
private static class GroupV1 extends Group {
|
||||
|
||||
public String groupId;
|
||||
public String expectedV2Id;
|
||||
public String name;
|
||||
public String color;
|
||||
public int messageExpirationTime;
|
||||
public boolean blocked;
|
||||
public boolean archived;
|
||||
|
||||
@JsonDeserialize(using = MembersDeserializer.class)
|
||||
@JsonSerialize(using = MembersSerializer.class)
|
||||
public List<Member> members;
|
||||
|
||||
// For deserialization
|
||||
public GroupV1() {
|
||||
}
|
||||
|
||||
public GroupV1(
|
||||
final String groupId,
|
||||
final String expectedV2Id,
|
||||
final String name,
|
||||
final String color,
|
||||
final int messageExpirationTime,
|
||||
final boolean blocked,
|
||||
final boolean archived,
|
||||
final List<Member> members
|
||||
) {
|
||||
this.groupId = groupId;
|
||||
this.expectedV2Id = expectedV2Id;
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.messageExpirationTime = messageExpirationTime;
|
||||
this.blocked = blocked;
|
||||
this.archived = archived;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
private static final class Member {
|
||||
|
||||
public Long recipientId;
|
||||
|
||||
public String uuid;
|
||||
|
||||
public String number;
|
||||
|
||||
Member(Long recipientId, final String uuid, final String number) {
|
||||
this.recipientId = recipientId;
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JsonSignalServiceAddress {
|
||||
|
||||
public String uuid;
|
||||
|
||||
public String number;
|
||||
|
||||
// For deserialization
|
||||
public JsonSignalServiceAddress() {
|
||||
}
|
||||
|
||||
JsonSignalServiceAddress(final String uuid, final String number) {
|
||||
this.uuid = uuid;
|
||||
this.number = number;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersSerializer extends JsonSerializer<List<Member>> {
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
final List<Member> value, final JsonGenerator jgen, final SerializerProvider provider
|
||||
) throws IOException {
|
||||
jgen.writeStartArray(value.size());
|
||||
for (var address : value) {
|
||||
if (address.recipientId != null) {
|
||||
jgen.writeNumber(address.recipientId);
|
||||
} else if (address.uuid != null) {
|
||||
jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number));
|
||||
} else {
|
||||
jgen.writeString(address.number);
|
||||
}
|
||||
}
|
||||
jgen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MembersDeserializer extends JsonDeserializer<List<Member>> {
|
||||
|
||||
@Override
|
||||
public List<Member> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var addresses = new ArrayList<Member>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (var n : node) {
|
||||
if (n.isTextual()) {
|
||||
addresses.add(new Member(null, null, n.textValue()));
|
||||
} else if (n.isNumber()) {
|
||||
addresses.add(new Member(n.numberValue().longValue(), null, null));
|
||||
} else {
|
||||
var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class);
|
||||
addresses.add(new Member(null, address.uuid, address.number));
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class GroupV2 extends Group {
|
||||
|
||||
public String groupId;
|
||||
public String masterKey;
|
||||
public boolean blocked;
|
||||
|
||||
// For deserialization
|
||||
private GroupV2() {
|
||||
}
|
||||
|
||||
public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
|
||||
this.groupId = groupId;
|
||||
this.masterKey = masterKey;
|
||||
this.blocked = blocked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
|
||||
//
|
||||
// @Override
|
||||
// public void serialize(
|
||||
// final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
|
||||
// ) throws IOException {
|
||||
// jgen.writeStartArray(groups.size());
|
||||
// for (var group : groups) {
|
||||
// if (group instanceof GroupInfoV1) {
|
||||
// jgen.writeObject(group);
|
||||
// } else if (group instanceof GroupInfoV2) {
|
||||
// final var groupV2 = (GroupInfoV2) group;
|
||||
// jgen.writeStartObject();
|
||||
// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
|
||||
// jgen.writeStringField("masterKey",
|
||||
// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
|
||||
// jgen.writeBooleanField("blocked", groupV2.isBlocked());
|
||||
// jgen.writeEndObject();
|
||||
// } else {
|
||||
// throw new AssertionError("Unknown group version");
|
||||
// }
|
||||
// }
|
||||
// jgen.writeEndArray();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
|
||||
|
||||
@Override
|
||||
public List<Storage.Group> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var groups = new ArrayList<Storage.Group>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (var n : node) {
|
||||
Storage.Group g;
|
||||
if (n.hasNonNull("masterKey")) {
|
||||
// a v2 group
|
||||
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV2.class);
|
||||
} else {
|
||||
g = jsonParser.getCodec().treeToValue(n, Storage.GroupV1.class);
|
||||
}
|
||||
groups.add(g);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Saver {
|
||||
|
||||
void save(Storage storage);
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
package org.asamk.signal.manager.storage.groups;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.util.Hex;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class JsonGroupStore {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(JsonGroupStore.class);
|
||||
|
||||
private static final ObjectMapper jsonProcessor = new ObjectMapper();
|
||||
@JsonIgnore
|
||||
public File groupCachePath;
|
||||
|
||||
@JsonProperty("groups")
|
||||
@JsonSerialize(using = GroupsSerializer.class)
|
||||
@JsonDeserialize(using = GroupsDeserializer.class)
|
||||
private final Map<GroupId, GroupInfo> groups = new HashMap<>();
|
||||
|
||||
private JsonGroupStore() {
|
||||
}
|
||||
|
||||
public JsonGroupStore(final File groupCachePath) {
|
||||
this.groupCachePath = groupCachePath;
|
||||
}
|
||||
|
||||
public void updateGroup(GroupInfo group) {
|
||||
groups.put(group.getGroupId(), group);
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
|
||||
try {
|
||||
IOUtils.createPrivateDirectories(groupCachePath);
|
||||
try (var stream = new FileOutputStream(getGroupFile(group.getGroupId()))) {
|
||||
((GroupInfoV2) group).getGroup().writeTo(stream);
|
||||
}
|
||||
final var groupFileLegacy = getGroupFileLegacy(group.getGroupId());
|
||||
if (groupFileLegacy.exists()) {
|
||||
groupFileLegacy.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to cache group, ignoring: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteGroup(GroupId groupId) {
|
||||
groups.remove(groupId);
|
||||
}
|
||||
|
||||
public GroupInfo getGroup(GroupId groupId) {
|
||||
var group = groups.get(groupId);
|
||||
if (group == null) {
|
||||
if (groupId instanceof GroupIdV1) {
|
||||
group = groups.get(GroupUtils.getGroupIdV2((GroupIdV1) groupId));
|
||||
} else if (groupId instanceof GroupIdV2) {
|
||||
group = getGroupV1ByV2Id((GroupIdV2) groupId);
|
||||
}
|
||||
}
|
||||
loadDecryptedGroup(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
private GroupInfoV1 getGroupV1ByV2Id(GroupIdV2 groupIdV2) {
|
||||
for (var g : groups.values()) {
|
||||
if (g instanceof GroupInfoV1) {
|
||||
final var gv1 = (GroupInfoV1) g;
|
||||
if (groupIdV2.equals(gv1.getExpectedV2Id())) {
|
||||
return gv1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadDecryptedGroup(final GroupInfo group) {
|
||||
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
|
||||
var groupFile = getGroupFile(group.getGroupId());
|
||||
if (!groupFile.exists()) {
|
||||
groupFile = getGroupFileLegacy(group.getGroupId());
|
||||
}
|
||||
if (!groupFile.exists()) {
|
||||
return;
|
||||
}
|
||||
try (var stream = new FileInputStream(groupFile)) {
|
||||
((GroupInfoV2) group).setGroup(DecryptedGroup.parseFrom(stream));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getGroupFileLegacy(final GroupId groupId) {
|
||||
return new File(groupCachePath, Hex.toStringCondensed(groupId.serialize()));
|
||||
}
|
||||
|
||||
private File getGroupFile(final GroupId groupId) {
|
||||
return new File(groupCachePath, groupId.toBase64().replace("/", "_"));
|
||||
}
|
||||
|
||||
public GroupInfoV1 getOrCreateGroupV1(GroupIdV1 groupId) {
|
||||
var group = getGroup(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
return (GroupInfoV1) group;
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
return new GroupInfoV1(groupId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<GroupInfo> getGroups() {
|
||||
final var groups = this.groups.values();
|
||||
for (var group : groups) {
|
||||
loadDecryptedGroup(group);
|
||||
}
|
||||
return new ArrayList<>(groups);
|
||||
}
|
||||
|
||||
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider
|
||||
) throws IOException {
|
||||
final var groups = value.values();
|
||||
jgen.writeStartArray(groups.size());
|
||||
for (var group : groups) {
|
||||
if (group instanceof GroupInfoV1) {
|
||||
jgen.writeObject(group);
|
||||
} else if (group instanceof GroupInfoV2) {
|
||||
final var groupV2 = (GroupInfoV2) group;
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
|
||||
jgen.writeStringField("masterKey",
|
||||
Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
|
||||
jgen.writeBooleanField("blocked", groupV2.isBlocked());
|
||||
jgen.writeEndObject();
|
||||
} else {
|
||||
throw new AssertionError("Unknown group version");
|
||||
}
|
||||
}
|
||||
jgen.writeEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static class GroupsDeserializer extends JsonDeserializer<Map<GroupId, GroupInfo>> {
|
||||
|
||||
@Override
|
||||
public Map<GroupId, GroupInfo> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var groups = new HashMap<GroupId, GroupInfo>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
for (var n : node) {
|
||||
GroupInfo g;
|
||||
if (n.hasNonNull("masterKey")) {
|
||||
// a v2 group
|
||||
var groupId = GroupIdV2.fromBase64(n.get("groupId").asText());
|
||||
try {
|
||||
var masterKey = new GroupMasterKey(Base64.getDecoder().decode(n.get("masterKey").asText()));
|
||||
g = new GroupInfoV2(groupId, masterKey);
|
||||
} catch (InvalidInputException | IllegalArgumentException e) {
|
||||
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
|
||||
}
|
||||
g.setBlocked(n.get("blocked").asBoolean(false));
|
||||
} else {
|
||||
g = jsonProcessor.treeToValue(n, GroupInfoV1.class);
|
||||
}
|
||||
groups.put(g.getGroupId(), g);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue