From: AsamK Date: Sun, 2 May 2021 14:02:54 +0000 (+0200) Subject: Refactor group store X-Git-Tag: v0.8.2~34 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/5b8c0c4e2de174de892af65b75a7e4818cbc29c7 Refactor group store --- diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index d55362de..3c2d130d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -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> 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> 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> 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(); - 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> 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> 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 getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + private Set getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { final var signalServiceAddresses = new HashSet(numbers.size()); final var addressesMissingUuid = new HashSet(); @@ -1302,7 +1308,7 @@ public class Manager implements Closeable { } } - return signalServiceAddresses; + return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); } private Map getRegisteredUsers(final Set numbersMissingUuid) throws IOException { @@ -1316,10 +1322,8 @@ public class Manager implements Closeable { } private Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Collection recipients + SignalServiceDataMessage.Builder messageBuilder, Set 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(recipients.size()); - for (var address : recipients) { - final var contact = account.getContactStore().getContact(resolveRecipient(address)); + var results = new ArrayList(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 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java index 237a34b6..d2012fa0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java @@ -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); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 849314da..6abdff07 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -100,7 +100,7 @@ public class GroupHelper { groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); } - public GroupInfoV2 createGroupV2( + public Pair createGroupV2( String name, Set 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 { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 0b82fb18..87e23c1b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -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> getAccessFor(Collection recipients) { + public List> getAccessFor(List recipients) { return recipients.stream().map(this::getAccessFor).collect(Collectors.toList()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 0c5290b9..f2d2d91d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -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; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java index 51bc2fdf..e542f4e3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java @@ -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 { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 68af2b80..8f9146de 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -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 getMembers(); + public abstract Set getMembers(); - @JsonIgnore - public Set getPendingMembers() { + public Set getPendingMembers() { return Set.of(); } - @JsonIgnore - public Set getRequestingMembers() { + public Set getRequestingMembers() { return Set.of(); } - @JsonIgnore public abstract boolean isBlocked(); - @JsonIgnore public abstract void setBlocked(boolean blocked); - @JsonIgnore public abstract int getMessageExpirationTime(); - @JsonIgnore - public Set getMembersWithout(SignalServiceAddress address) { - return getMembers().stream().filter(member -> !member.matches(address)).collect(Collectors.toSet()); + public Set getMembersWithout(RecipientId recipientId) { + return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); } - @JsonIgnore - public Set getMembersIncludingPendingWithout(SignalServiceAddress address) { + public Set 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); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 970ec5c3..8cf6cb94 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -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 members = new HashSet<>(); - @JsonProperty + public Set 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 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 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.messageExpirationTime = messageExpirationTime; this.blocked = blocked; - this.inboxPosition = inboxPosition; this.archived = archived; - this.messageExpirationTime = messageExpirationTime; } @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 getMembers() { + public Set getMembers() { return members; } @@ -134,79 +89,11 @@ public class GroupInfoV1 extends GroupInfo { return messageExpirationTime; } - public void addMembers(Collection addresses) { - for (var address : addresses) { - if (this.members.contains(address)) { - continue; - } - removeMember(address); - this.members.add(address); - } - } - - 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> { - - @Override - public void serialize( - final Set 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(); - } + public void addMembers(Collection members) { + this.members.addAll(members); } - private static class MembersDeserializer extends JsonDeserializer> { - - @Override - public Set deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext - ) throws IOException { - var addresses = new HashSet(); - 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)); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 2092c03a..b37e50a9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -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 getMembers() { + public Set 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 getPendingMembers() { + public Set 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 getRequestingMembers() { + public Set 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()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java new file mode 100644 index 00000000..19fc7564 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -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 groups; + private final RecipientResolver recipientResolver; + private final Saver saver; + + private GroupStore( + final File groupCachePath, + final Map 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 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 groups; + + // For deserialization + public Storage() { + } + + public Storage(final List 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 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 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> { + + @Override + public void serialize( + final List 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> { + + @Override + public List deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext + ) throws IOException { + var addresses = new ArrayList(); + 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> { +// +// @Override +// public void serialize( +// final List 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> { + + @Override + public List deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext + ) throws IOException { + var groups = new ArrayList(); + 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); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java deleted file mode 100644 index 4bd19ea1..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/JsonGroupStore.java +++ /dev/null @@ -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 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 getGroups() { - final var groups = this.groups.values(); - for (var group : groups) { - loadDecryptedGroup(group); - } - return new ArrayList<>(groups); - } - - private static class GroupsSerializer extends JsonSerializer> { - - @Override - public void serialize( - final Map 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> { - - @Override - public Map deserialize( - JsonParser jsonParser, DeserializationContext deserializationContext - ) throws IOException { - var groups = new HashMap(); - 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; - } - } -} diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index bcd88ab4..1fc38c7b 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -46,7 +46,7 @@ public class JoinGroupCommand implements LocalCommand { final var results = m.joinGroup(linkUrl); var newGroupId = results.first(); - if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) { + if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) { writer.println("Requested to join group \"{}\"", newGroupId.toBase64()); } else { writer.println("Joined group \"{}\"", newGroupId.toBase64()); diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index a547cc15..a3a55608 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.PlainTextWriterImpl; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.recipients.RecipientId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -23,7 +24,7 @@ public class ListGroupsCommand implements LocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class); - private static Set resolveMembers(Manager m, Set addresses) { + private static Set resolveMembers(Manager m, Set addresses) { return addresses.stream() .map(m::resolveSignalServiceAddress) .map(SignalServiceAddress::getLegacyIdentifier) @@ -40,7 +41,7 @@ public class ListGroupsCommand implements LocalCommand { "Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), - group.isMember(m.getSelfAddress()), + group.isMember(m.getSelfRecipientId()), group.isBlocked(), resolveMembers(m, group.getMembers()), resolveMembers(m, group.getPendingMembers()), @@ -50,7 +51,7 @@ public class ListGroupsCommand implements LocalCommand { writer.println("Id: {} Name: {} Active: {} Blocked: {}", group.getGroupId().toBase64(), group.getTitle(), - group.isMember(m.getSelfAddress()), + group.isMember(m.getSelfRecipientId()), group.isBlocked()); } } @@ -80,7 +81,7 @@ public class ListGroupsCommand implements LocalCommand { jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(), group.getTitle(), - group.isMember(m.getSelfAddress()), + group.isMember(m.getSelfRecipientId()), group.isBlocked(), resolveMembers(m, group.getMembers()), resolveMembers(m, group.getPendingMembers()), diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index e58f7829..ae19c978 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -472,7 +472,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return false; } else { - return group.isMember(m.getSelfAddress()); + return group.isMember(m.getSelfRecipientId()); } } }