]> nmode's Git Repositories - signal-cli/commitdiff
Refactor contact and profile store
authorAsamK <asamk@gmx.de>
Fri, 30 Apr 2021 20:17:13 +0000 (22:17 +0200)
committerAsamK <asamk@gmx.de>
Mon, 3 May 2021 16:43:45 +0000 (18:43 +0200)
31 files changed:
lib/src/main/java/org/asamk/signal/manager/HandleAction.java
lib/src/main/java/org/asamk/signal/manager/Manager.java
lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyCredentialProvider.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java
lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java [deleted file]
lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessProvider.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java [deleted file]
lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java [moved from lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java with 82% similarity]
lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java [moved from lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java with 82% similarity]
lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java
lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfile.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/signal/ReceiveMessageHandler.java
src/main/java/org/asamk/signal/commands/ListContactsCommand.java
src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java

index ba426fa1c994274a615f6a60800c354e150372f8..7fb80c34812a6d5197389b5a318de8ec72a56261 100644 (file)
@@ -1,6 +1,7 @@
 package org.asamk.signal.manager;
 
 import org.asamk.signal.manager.groups.GroupIdV1;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 import java.util.Objects;
@@ -160,15 +161,15 @@ class SendGroupInfoAction implements HandleAction {
 
 class RetrieveProfileAction implements HandleAction {
 
-    private final SignalServiceAddress address;
+    private final RecipientId recipientId;
 
-    public RetrieveProfileAction(final SignalServiceAddress address) {
-        this.address = address;
+    public RetrieveProfileAction(final RecipientId recipientId) {
+        this.recipientId = recipientId;
     }
 
     @Override
     public void execute(Manager m) throws Throwable {
-        m.getRecipientProfile(address, true);
+        m.getRecipientProfile(recipientId, true);
     }
 
     @Override
@@ -178,11 +179,11 @@ class RetrieveProfileAction implements HandleAction {
 
         final RetrieveProfileAction that = (RetrieveProfileAction) o;
 
-        return address.equals(that.address);
+        return recipientId.equals(that.recipientId);
     }
 
     @Override
     public int hashCode() {
-        return address.hashCode();
+        return recipientId.hashCode();
     }
 }
index 4bced5e2304a1d50640505724d290b61c325a963..f62af7ad42a1645a0f384d8831fc8dfa5284456d 100644 (file)
@@ -30,13 +30,13 @@ import org.asamk.signal.manager.helper.PinHelper;
 import org.asamk.signal.manager.helper.ProfileHelper;
 import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
 import org.asamk.signal.manager.storage.SignalAccount;
-import org.asamk.signal.manager.storage.contacts.ContactInfo;
 import org.asamk.signal.manager.storage.groups.GroupInfo;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
 import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.manager.storage.messageCache.CachedMessage;
-import org.asamk.signal.manager.storage.profiles.SignalProfile;
+import org.asamk.signal.manager.storage.recipients.Contact;
+import org.asamk.signal.manager.storage.recipients.Profile;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.stickers.Sticker;
 import org.asamk.signal.manager.util.AttachmentUtils;
@@ -243,13 +243,15 @@ public class Manager implements Closeable {
         this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey,
                 unidentifiedAccessHelper::getAccessFor,
                 unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
-                () -> messageReceiver);
+                () -> messageReceiver,
+                this::resolveSignalServiceAddress);
         this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
                 this::getRecipientProfile,
-                account::getSelfAddress,
+                account::getSelfRecipientId,
                 groupsV2Operations,
                 groupsV2Api,
-                this::getGroupAuthForToday);
+                this::getGroupAuthForToday,
+                this::resolveSignalServiceAddress);
         this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
         this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
     }
@@ -355,24 +357,26 @@ public class Manager implements Closeable {
      *                   if it's Optional.absent(), the avatar will be removed
      */
     public void setProfile(String name, String about, String aboutEmoji, Optional<File> avatar) throws IOException {
-        var profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress());
-        var profile = profileEntry == null ? null : profileEntry.getProfile();
-        var newProfile = new SignalProfile(profile == null ? null : profile.getIdentityKey(),
-                name != null ? name : profile == null || profile.getName() == null ? "" : profile.getName(),
-                about != null ? about : profile == null || profile.getAbout() == null ? "" : profile.getAbout(),
-                aboutEmoji != null
-                        ? aboutEmoji
-                        : profile == null || profile.getAboutEmoji() == null ? "" : profile.getAboutEmoji(),
-                profile == null ? null : profile.getUnidentifiedAccess(),
-                account.isUnrestrictedUnidentifiedAccess(),
-                profile == null ? null : profile.getCapabilities());
+        var profile = getRecipientProfile(account.getSelfRecipientId());
+        var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
+        if (name != null) {
+            builder.withGivenName(name);
+            builder.withFamilyName(null);
+        }
+        if (about != null) {
+            builder.withAbout(about);
+        }
+        if (aboutEmoji != null) {
+            builder.withAboutEmoji(aboutEmoji);
+        }
+        var newProfile = builder.build();
 
         try (final var streamDetails = avatar == null
                 ? avatarStore.retrieveProfileAvatar(getSelfAddress())
                 : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
             accountManager.setVersionedProfile(account.getUuid(),
                     account.getProfileKey(),
-                    newProfile.getName(),
+                    newProfile.getInternalServiceName(),
                     newProfile.getAbout(),
                     newProfile.getAboutEmoji(),
                     streamDetails);
@@ -386,12 +390,7 @@ public class Manager implements Closeable {
                 avatarStore.deleteProfileAvatar(getSelfAddress());
             }
         }
-        account.getProfileStore()
-                .updateProfile(getSelfAddress(),
-                        account.getProfileKey(),
-                        System.currentTimeMillis(),
-                        newProfile,
-                        profileEntry == null ? null : profileEntry.getProfileKeyCredential());
+        account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile);
 
         try {
             sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
@@ -527,99 +526,124 @@ public class Manager implements Closeable {
                 ServiceConfig.AUTOMATIC_NETWORK_RETRY);
     }
 
-    public SignalProfile getRecipientProfile(
+    public Profile getRecipientProfile(
             SignalServiceAddress address
     ) {
-        return getRecipientProfile(address, false);
+        return getRecipientProfile(resolveRecipient(address), false);
     }
 
-    SignalProfile getRecipientProfile(
-            SignalServiceAddress address, boolean force
+    public Profile getRecipientProfile(
+            RecipientId recipientId
+    ) {
+        return getRecipientProfile(recipientId, false);
+    }
+
+    private final Set<RecipientId> pendingProfileRequest = new HashSet<>();
+
+    Profile getRecipientProfile(
+            RecipientId recipientId, boolean force
     ) {
-        var profileEntry = account.getProfileStore().getProfileEntry(address);
-        if (profileEntry == null) {
-            // retrieve profile to get identity key
-            retrieveEncryptedProfile(address);
+        var profileKey = account.getProfileStore().getProfileKey(recipientId);
+        if (profileKey == null) {
+            if (force) {
+                // retrieve profile to get identity key
+                retrieveEncryptedProfile(recipientId);
+            }
             return null;
         }
+        var profile = account.getProfileStore().getProfile(recipientId);
+
         var now = new Date().getTime();
-        // Profiles are cached for 24h before retrieving them again
-        if (!profileEntry.isRequestPending() && (
-                force
-                        || profileEntry.getProfile() == null
-                        || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000
-        )) {
-            profileEntry.setRequestPending(true);
-            final SignalServiceProfile encryptedProfile;
-            try {
-                encryptedProfile = retrieveEncryptedProfile(address);
-            } finally {
-                profileEntry.setRequestPending(false);
+        // Profiles are cached for 24h before retrieving them again, unless forced
+        if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) {
+            return profile;
+        }
+
+        synchronized (pendingProfileRequest) {
+            if (pendingProfileRequest.contains(recipientId)) {
+                return profile;
             }
-            if (encryptedProfile == null) {
-                return null;
+            pendingProfileRequest.add(recipientId);
+        }
+        final SignalServiceProfile encryptedProfile;
+        try {
+            encryptedProfile = retrieveEncryptedProfile(recipientId);
+        } finally {
+            synchronized (pendingProfileRequest) {
+                pendingProfileRequest.remove(recipientId);
             }
-
-            final var profileKey = profileEntry.getProfileKey();
-            final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile);
-            account.getProfileStore()
-                    .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential());
-            return profile;
         }
-        return profileEntry.getProfile();
+        if (encryptedProfile == null) {
+            return null;
+        }
+
+        profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+        account.getProfileStore().storeProfile(recipientId, profile);
+
+        return profile;
     }
 
-    private SignalServiceProfile retrieveEncryptedProfile(SignalServiceAddress address) {
+    private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) {
         try {
-            final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE)
-                    .getProfile();
-            try {
-                account.getIdentityKeyStore()
-                        .saveIdentity(resolveRecipient(address),
-                                new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
-                                new Date());
-            } catch (InvalidKeyException ignored) {
-                logger.warn("Got invalid identity key in profile for {}", address.getLegacyIdentifier());
-            }
-            return profile;
+            return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile();
         } catch (IOException e) {
             logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
             return null;
         }
     }
 
-    private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) {
-        var profileEntry = account.getProfileStore().getProfileEntry(address);
-        if (profileEntry == null) {
+    private ProfileAndCredential retrieveProfileAndCredential(
+            final RecipientId recipientId, final SignalServiceProfile.RequestType requestType
+    ) throws IOException {
+        final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType);
+        final var profile = profileAndCredential.getProfile();
+
+        try {
+            account.getIdentityKeyStore()
+                    .saveIdentity(recipientId,
+                            new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
+                            new Date());
+        } catch (InvalidKeyException ignored) {
+            logger.warn("Got invalid identity key in profile for {}",
+                    resolveSignalServiceAddress(recipientId).getLegacyIdentifier());
+        }
+        return profileAndCredential;
+    }
+
+    private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) {
+        var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId);
+        if (profileKeyCredential != null) {
+            return profileKeyCredential;
+        }
+
+        ProfileAndCredential profileAndCredential;
+        try {
+            profileAndCredential = retrieveProfileAndCredential(recipientId,
+                    SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
+        } catch (IOException e) {
+            logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
             return null;
         }
-        if (profileEntry.getProfileKeyCredential() == null) {
-            ProfileAndCredential profileAndCredential;
-            try {
-                profileAndCredential = profileHelper.retrieveProfileSync(address,
-                        SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL);
-            } catch (IOException e) {
-                logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage());
-                return null;
-            }
 
-            var now = new Date().getTime();
-            final var profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
-            final var profile = decryptProfileAndDownloadAvatar(address,
-                    profileEntry.getProfileKey(),
+        profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull();
+        account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential);
+
+        var profileKey = account.getProfileStore().getProfileKey(recipientId);
+        if (profileKey != null) {
+            final var profile = decryptProfileAndDownloadAvatar(recipientId,
+                    profileKey,
                     profileAndCredential.getProfile());
-            account.getProfileStore()
-                    .updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential);
-            return profileKeyCredential;
+            account.getProfileStore().storeProfile(recipientId, profile);
         }
-        return profileEntry.getProfileKeyCredential();
+
+        return profileKeyCredential;
     }
 
-    private SignalProfile decryptProfileAndDownloadAvatar(
-            final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
+    private Profile decryptProfileAndDownloadAvatar(
+            final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
     ) {
         if (encryptedProfile.getAvatar() != null) {
-            downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey);
+            downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey);
         }
 
         return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
@@ -729,19 +753,23 @@ public class Manager implements Closeable {
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
         return sendUpdateGroupMessage(groupId,
                 name,
-                members == null ? null : getSignalServiceAddresses(members),
+                members == null
+                        ? null
+                        : getSignalServiceAddresses(members).stream()
+                                .map(this::resolveRecipient)
+                                .collect(Collectors.toSet()),
                 avatarFile);
     }
 
     private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
-            GroupId groupId, String name, Collection<SignalServiceAddress> members, File avatarFile
+            GroupId groupId, String name, Set<RecipientId> members, File avatarFile
     ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
         GroupInfo g;
         SignalServiceDataMessage.Builder messageBuilder;
         if (groupId == null) {
             // Create new group
             var gv2 = groupHelper.createGroupV2(name == null ? "" : name,
-                    members == null ? List.of() : members,
+                    members == null ? Set.of() : members,
                     avatarFile);
             if (gv2 == null) {
                 var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
@@ -774,7 +802,7 @@ public class Manager implements Closeable {
                     final var newMembers = new HashSet<>(members);
                     newMembers.removeAll(group.getMembers()
                             .stream()
-                            .map(this::resolveSignalServiceAddress)
+                            .map(this::resolveRecipient)
                             .collect(Collectors.toSet()));
                     if (newMembers.size() > 0) {
                         var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
@@ -810,18 +838,18 @@ public class Manager implements Closeable {
     }
 
     private void updateGroupV1(
-            final GroupInfoV1 g,
-            final String name,
-            final Collection<SignalServiceAddress> members,
-            final File avatarFile
+            final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
     ) throws IOException {
         if (name != null) {
             g.name = name;
         }
 
         if (members != null) {
+            final var memberAddresses = members.stream()
+                    .map(this::resolveSignalServiceAddress)
+                    .collect(Collectors.toList());
             final var newE164Members = new HashSet<String>();
-            for (var member : members) {
+            for (var member : memberAddresses) {
                 if (g.isMember(member) || !member.getNumber().isPresent()) {
                     continue;
                 }
@@ -837,7 +865,7 @@ public class Manager implements Closeable {
                         + " to group: Not registered on Signal");
             }
 
-            g.addMembers(members);
+            g.addMembers(memberAddresses);
         }
 
         if (avatarFile != null) {
@@ -973,7 +1001,7 @@ public class Manager implements Closeable {
                 System.currentTimeMillis());
 
         createMessageSender().sendReceipt(remoteAddress,
-                unidentifiedAccessHelper.getAccessFor(remoteAddress),
+                unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)),
                 receiptMessage);
     }
 
@@ -1053,36 +1081,26 @@ public class Manager implements Closeable {
     }
 
     public String getContactName(String number) throws InvalidNumberException {
-        var contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number));
-        if (contact == null) {
-            return "";
-        } else {
-            return contact.name;
-        }
+        var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number));
+        return contact == null || contact.getName() == null ? "" : contact.getName();
     }
 
     public void setContactName(String number, String name) throws InvalidNumberException {
-        final var address = canonicalizeAndResolveSignalServiceAddress(number);
-        var contact = account.getContactStore().getContact(address);
-        if (contact == null) {
-            contact = new ContactInfo(address);
-        }
-        contact.name = name;
-        account.getContactStore().updateContact(contact);
+        final var recipientId = canonicalizeAndResolveRecipient(number);
+        var contact = account.getContactStore().getContact(recipientId);
+        final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
+        account.getContactStore().storeContact(recipientId, builder.withName(name).build());
         account.save();
     }
 
     public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
-        setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked);
+        setContactBlocked(canonicalizeAndResolveRecipient(number), blocked);
     }
 
-    private void setContactBlocked(SignalServiceAddress address, boolean blocked) {
-        var contact = account.getContactStore().getContact(address);
-        if (contact == null) {
-            contact = new ContactInfo(address);
-        }
-        contact.blocked = blocked;
-        account.getContactStore().updateContact(contact);
+    private void setContactBlocked(RecipientId recipientId, boolean blocked) {
+        var contact = account.getContactStore().getContact(recipientId);
+        final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
+        account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build());
         account.save();
     }
 
@@ -1097,15 +1115,14 @@ public class Manager implements Closeable {
         account.save();
     }
 
-    /**
-     * Change the expiration timer for a contact
-     */
-    public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException {
-        var contact = account.getContactStore().getContact(address);
-        contact.messageExpirationTime = messageExpirationTimer;
-        account.getContactStore().updateContact(contact);
-        sendExpirationTimerUpdate(address);
-        account.save();
+    private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
+        var contact = account.getContactStore().getContact(recipientId);
+        if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
+            return;
+        }
+        final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
+        account.getContactStore()
+                .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
     }
 
     private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException {
@@ -1119,8 +1136,10 @@ public class Manager implements Closeable {
     public void setExpirationTimer(
             String number, int messageExpirationTimer
     ) throws IOException, InvalidNumberException {
-        var address = canonicalizeAndResolveSignalServiceAddress(number);
-        setExpirationTimer(address, messageExpirationTimer);
+        var recipientId = canonicalizeAndResolveRecipient(number);
+        setExpirationTimer(recipientId, messageExpirationTimer);
+        sendExpirationTimerUpdate(resolveSignalServiceAddress(recipientId));
+        account.save();
     }
 
     /**
@@ -1298,6 +1317,7 @@ public class Manager implements Closeable {
             SignalServiceDataMessage.Builder messageBuilder, Collection<SignalServiceAddress> recipients
     ) 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();
@@ -1310,7 +1330,7 @@ public class Manager implements Closeable {
                     var messageSender = createMessageSender();
                     final var isRecipientUpdate = false;
                     var result = messageSender.sendMessage(new ArrayList<>(recipients),
-                            unidentifiedAccessHelper.getAccessFor(recipients),
+                            unidentifiedAccessHelper.getAccessFor(recipientIds),
                             isRecipientUpdate,
                             message);
 
@@ -1332,8 +1352,8 @@ public class Manager implements Closeable {
                 messageBuilder.withProfileKey(account.getProfileKey().serialize());
                 var results = new ArrayList<SendMessageResult>(recipients.size());
                 for (var address : recipients) {
-                    final var contact = account.getContactStore().getContact(address);
-                    final var expirationTime = contact != null ? contact.messageExpirationTime : 0;
+                    final var contact = account.getContactStore().getContact(resolveRecipient(address));
+                    final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
                     messageBuilder.withExpiration(expirationTime);
                     message = messageBuilder.build();
                     results.add(sendMessage(address, message));
@@ -1358,10 +1378,10 @@ public class Manager implements Closeable {
         getOrCreateMessagePipe();
         getOrCreateUnidentifiedMessagePipe();
         try {
-            final var address = getSelfAddress();
+            final var recipientId = account.getSelfRecipientId();
 
-            final var contact = account.getContactStore().getContact(address);
-            final var expirationTime = contact != null ? contact.messageExpirationTime : 0;
+            final var contact = account.getContactStore().getContact(recipientId);
+            final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0;
             messageBuilder.withExpiration(expirationTime);
 
             var message = messageBuilder.build();
@@ -1377,7 +1397,7 @@ public class Manager implements Closeable {
 
         var recipient = account.getSelfAddress();
 
-        final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient);
+        final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(resolveRecipient(recipient));
         var transcript = new SentTranscriptMessage(Optional.of(recipient),
                 message.getTimestamp(),
                 message,
@@ -1404,7 +1424,9 @@ public class Manager implements Closeable {
         var messageSender = createMessageSender();
 
         try {
-            return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
+            return messageSender.sendMessage(address,
+                    unidentifiedAccessHelper.getAccessFor(resolveRecipient(address)),
+                    message);
         } catch (UntrustedIdentityException e) {
             return SendMessageResult.identityFailure(address, e.getIdentityKey());
         }
@@ -1520,14 +1542,7 @@ public class Manager implements Closeable {
                     // disappearing message timer already stored in the DecryptedGroup
                 }
             } else if (conversationPartnerAddress != null) {
-                var contact = account.getContactStore().getContact(conversationPartnerAddress);
-                if (contact == null) {
-                    contact = new ContactInfo(conversationPartnerAddress);
-                }
-                if (contact.messageExpirationTime != message.getExpiresInSeconds()) {
-                    contact.messageExpirationTime = message.getExpiresInSeconds();
-                    account.getContactStore().updateContact(contact);
-                }
+                setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds());
             }
         }
         if (!ignoreAttachments) {
@@ -1554,7 +1569,7 @@ public class Manager implements Closeable {
             if (source.matches(account.getSelfAddress())) {
                 this.account.setProfileKey(profileKey);
             }
-            this.account.getProfileStore().storeProfileKey(source, profileKey);
+            this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey);
         }
         if (message.getPreviews().isPresent()) {
             final var previews = message.getPreviews().get();
@@ -1632,7 +1647,7 @@ public class Manager implements Closeable {
 
     private void storeProfileKeysFromMembers(final DecryptedGroup group) {
         for (var member : group.getMembersList()) {
-            final var address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid()
+            final var address = resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid()
                     .toByteArray()), null));
             try {
                 account.getProfileStore()
@@ -1789,7 +1804,7 @@ public class Manager implements Closeable {
                 if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
                     final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception)
                             .getName());
-                    queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId)));
+                    queuedActions.add(new RetrieveProfileAction(recipientId));
                     if (!envelope.hasSource()) {
                         try {
                             cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
@@ -1816,8 +1831,8 @@ public class Manager implements Closeable {
         } else {
             return false;
         }
-        var sourceContact = account.getContactStore().getContact(source);
-        if (sourceContact != null && sourceContact.blocked) {
+        final var recipientId = resolveRecipient(source);
+        if (isContactBlocked(recipientId)) {
             return true;
         }
 
@@ -1834,6 +1849,16 @@ public class Manager implements Closeable {
         return false;
     }
 
+    public boolean isContactBlocked(final String identifier) throws InvalidNumberException {
+        final var recipientId = canonicalizeAndResolveRecipient(identifier);
+        return isContactBlocked(recipientId);
+    }
+
+    private boolean isContactBlocked(final RecipientId recipientId) {
+        var sourceContact = account.getContactStore().getContact(recipientId);
+        return sourceContact != null && sourceContact.isBlocked();
+    }
+
     private boolean isNotAGroupMember(
             SignalServiceEnvelope envelope, SignalServiceContent content
     ) {
@@ -1876,8 +1901,6 @@ public class Manager implements Closeable {
             } else {
                 sender = content.getSender();
             }
-            // Store uuid if we don't have it already
-            resolveSignalServiceAddress(sender);
 
             if (content.getDataMessage().isPresent()) {
                 var message = content.getDataMessage().get();
@@ -1974,7 +1997,7 @@ public class Manager implements Closeable {
                 if (syncMessage.getBlockedList().isPresent()) {
                     final var blockedListMessage = syncMessage.getBlockedList().get();
                     for (var address : blockedListMessage.getAddresses()) {
-                        setContactBlocked(resolveSignalServiceAddress(address), true);
+                        setContactBlocked(resolveRecipient(address), true);
                     }
                     for (var groupId : blockedListMessage.getGroupIds()
                             .stream()
@@ -2001,19 +2024,19 @@ public class Manager implements Closeable {
                                 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
                                     account.setProfileKey(c.getProfileKey().get());
                                 }
-                                final var address = resolveSignalServiceAddress(c.getAddress());
-                                var contact = account.getContactStore().getContact(address);
-                                if (contact == null) {
-                                    contact = new ContactInfo(address);
-                                }
+                                final var recipientId = resolveRecipientTrusted(c.getAddress());
+                                var contact = account.getContactStore().getContact(recipientId);
+                                final var builder = contact == null
+                                        ? Contact.newBuilder()
+                                        : Contact.newBuilder(contact);
                                 if (c.getName().isPresent()) {
-                                    contact.name = c.getName().get();
+                                    builder.withName(c.getName().get());
                                 }
                                 if (c.getColor().isPresent()) {
-                                    contact.color = c.getColor().get();
+                                    builder.withColor(c.getColor().get());
                                 }
                                 if (c.getProfileKey().isPresent()) {
-                                    account.getProfileStore().storeProfileKey(address, c.getProfileKey().get());
+                                    account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
                                 }
                                 if (c.getVerified().isPresent()) {
                                     final var verifiedMessage = c.getVerified().get();
@@ -2023,15 +2046,14 @@ public class Manager implements Closeable {
                                                     TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
                                 }
                                 if (c.getExpirationTimer().isPresent()) {
-                                    contact.messageExpirationTime = c.getExpirationTimer().get();
+                                    builder.withMessageExpirationTime(c.getExpirationTimer().get());
                                 }
-                                contact.blocked = c.isBlocked();
-                                contact.inboxPosition = c.getInboxPosition().orNull();
-                                contact.archived = c.isArchived();
-                                account.getContactStore().updateContact(contact);
+                                builder.withBlocked(c.isBlocked());
+                                builder.withArchived(c.isArchived());
+                                account.getContactStore().storeContact(recipientId, builder.build());
 
                                 if (c.getAvatar().isPresent()) {
-                                    downloadContactAvatar(c.getAvatar().get(), contact.getAddress());
+                                    downloadContactAvatar(c.getAvatar().get(), c.getAddress());
                                 }
                             }
                         }
@@ -2079,7 +2101,7 @@ public class Manager implements Closeable {
                 if (syncMessage.getFetchType().isPresent()) {
                     switch (syncMessage.getFetchType().get()) {
                         case LOCAL_PROFILE:
-                            getRecipientProfile(getSelfAddress(), true);
+                            getRecipientProfile(account.getSelfRecipientId(), true);
                         case STORAGE_MANIFEST:
                             // TODO
                     }
@@ -2294,28 +2316,31 @@ public class Manager implements Closeable {
         try {
             try (OutputStream fos = new FileOutputStream(contactsFile)) {
                 var out = new DeviceContactsOutputStream(fos);
-                for (var record : account.getContactStore().getContacts()) {
+                for (var contactPair : account.getContactStore().getContacts()) {
+                    final var recipientId = contactPair.first();
+                    final var contact = contactPair.second();
+                    final var address = resolveSignalServiceAddress(recipientId);
+
+                    var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
                     VerifiedMessage verifiedMessage = null;
-                    var currentIdentity = account.getIdentityKeyStore()
-                            .getIdentity(resolveRecipientTrusted(record.getAddress()));
                     if (currentIdentity != null) {
-                        verifiedMessage = new VerifiedMessage(record.getAddress(),
+                        verifiedMessage = new VerifiedMessage(address,
                                 currentIdentity.getIdentityKey(),
                                 currentIdentity.getTrustLevel().toVerifiedState(),
                                 currentIdentity.getDateAdded().getTime());
                     }
 
-                    var profileKey = account.getProfileStore().getProfileKey(record.getAddress());
-                    out.write(new DeviceContact(record.getAddress(),
-                            Optional.fromNullable(record.name),
-                            createContactAvatarAttachment(record.getAddress()),
-                            Optional.fromNullable(record.color),
+                    var profileKey = account.getProfileStore().getProfileKey(recipientId);
+                    out.write(new DeviceContact(address,
+                            Optional.fromNullable(contact.getName()),
+                            createContactAvatarAttachment(address),
+                            Optional.fromNullable(contact.getColor()),
                             Optional.fromNullable(verifiedMessage),
                             Optional.fromNullable(profileKey),
-                            record.blocked,
-                            Optional.of(record.messageExpirationTime),
-                            Optional.fromNullable(record.inboxPosition),
-                            record.archived));
+                            contact.isBlocked(),
+                            Optional.of(contact.getMessageExpirationTime()),
+                            Optional.absent(),
+                            contact.isArchived()));
                 }
 
                 if (account.getProfileKey() != null) {
@@ -2356,8 +2381,8 @@ public class Manager implements Closeable {
     void sendBlockedList() throws IOException, UntrustedIdentityException {
         var addresses = new ArrayList<SignalServiceAddress>();
         for (var record : account.getContactStore().getContacts()) {
-            if (record.blocked) {
-                addresses.add(record.getAddress());
+            if (record.second().isBlocked()) {
+                addresses.add(resolveSignalServiceAddress(record.first()));
             }
         }
         var groupIds = new ArrayList<byte[]>();
@@ -2379,22 +2404,25 @@ public class Manager implements Closeable {
         sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
     }
 
-    public List<ContactInfo> getContacts() {
+    public List<Pair<RecipientId, Contact>> getContacts() {
         return account.getContactStore().getContacts();
     }
 
-    public String getContactOrProfileName(String number) {
-        final var address = Utils.getSignalServiceAddressFromIdentifier(number);
+    public String getContactOrProfileName(String number) throws InvalidNumberException {
+        final var recipientId = canonicalizeAndResolveRecipient(number);
+        final var recipient = account.getRecipientStore().getRecipient(recipientId);
+        if (recipient == null) {
+            return null;
+        }
 
-        final var contact = account.getContactStore().getContact(address);
-        if (contact != null && !Util.isEmpty(contact.name)) {
-            return contact.name;
+        if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) {
+            return recipient.getContact().getName();
         }
 
-        final var profileEntry = account.getProfileStore().getProfileEntry(address);
-        if (profileEntry != null && profileEntry.getProfile() != null) {
-            return profileEntry.getProfile().getDisplayName();
+        if (recipient.getProfile() != null && recipient.getProfile() != null) {
+            return recipient.getProfile().getDisplayName();
         }
+
         return null;
     }
 
@@ -2530,11 +2558,11 @@ public class Manager implements Closeable {
     }
 
     public RecipientId resolveRecipient(SignalServiceAddress address) {
-        return account.getRecipientStore().resolveRecipientUntrusted(address);
+        return account.getRecipientStore().resolveRecipient(address);
     }
 
     private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
-        return account.getRecipientStore().resolveRecipient(address);
+        return account.getRecipientStore().resolveRecipientTrusted(address);
     }
 
     @Override
index 9c1eecce60d0573be00330dc5964e5e4c09006a3..b3c524c62d492e7974c84fda5c44763b590edc08 100644 (file)
@@ -165,7 +165,7 @@ public class RegistrationManager implements Closeable {
         account.setUuid(UuidUtil.parseOrNull(response.getUuid()));
         account.setRegistrationLockPin(pin);
         account.getSessionStore().archiveAllSessions();
-        final var recipientId = account.getRecipientStore().resolveRecipient(account.getSelfAddress());
+        final var recipientId = account.getRecipientStore().resolveRecipientTrusted(account.getSelfAddress());
         final var publicKey = account.getIdentityKeyPair().getPublicKey();
         account.getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
         account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
index c76075be5c8977f3b7e81f054393b637f5e0feaa..849314da75ff47a0d2fe9297fbea6dcc6bb41544 100644 (file)
@@ -5,7 +5,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
 import org.asamk.signal.manager.groups.GroupLinkPassword;
 import org.asamk.signal.manager.groups.GroupUtils;
 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
-import org.asamk.signal.manager.storage.profiles.SignalProfile;
+import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.util.IOUtils;
 import org.signal.storageservice.protos.groups.AccessControl;
 import org.signal.storageservice.protos.groups.GroupChange;
@@ -38,7 +39,6 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Collection;
 import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
@@ -51,7 +51,7 @@ public class GroupHelper {
 
     private final ProfileProvider profileProvider;
 
-    private final SelfAddressProvider selfAddressProvider;
+    private final SelfRecipientIdProvider selfRecipientIdProvider;
 
     private final GroupsV2Operations groupsV2Operations;
 
@@ -59,20 +59,24 @@ public class GroupHelper {
 
     private final GroupAuthorizationProvider groupAuthorizationProvider;
 
+    private final SignalServiceAddressResolver addressResolver;
+
     public GroupHelper(
             final ProfileKeyCredentialProvider profileKeyCredentialProvider,
             final ProfileProvider profileProvider,
-            final SelfAddressProvider selfAddressProvider,
+            final SelfRecipientIdProvider selfRecipientIdProvider,
             final GroupsV2Operations groupsV2Operations,
             final GroupsV2Api groupsV2Api,
-            final GroupAuthorizationProvider groupAuthorizationProvider
+            final GroupAuthorizationProvider groupAuthorizationProvider,
+            final SignalServiceAddressResolver addressResolver
     ) {
         this.profileKeyCredentialProvider = profileKeyCredentialProvider;
         this.profileProvider = profileProvider;
-        this.selfAddressProvider = selfAddressProvider;
+        this.selfRecipientIdProvider = selfRecipientIdProvider;
         this.groupsV2Operations = groupsV2Operations;
         this.groupsV2Api = groupsV2Api;
         this.groupAuthorizationProvider = groupAuthorizationProvider;
+        this.addressResolver = addressResolver;
     }
 
     public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
@@ -97,7 +101,7 @@ public class GroupHelper {
     }
 
     public GroupInfoV2 createGroupV2(
-            String name, Collection<SignalServiceAddress> members, File avatarFile
+            String name, Set<RecipientId> members, File avatarFile
     ) throws IOException {
         final var avatarBytes = readAvatarBytes(avatarFile);
         final var newGroup = buildNewGroupV2(name, members, avatarBytes);
@@ -139,9 +143,9 @@ public class GroupHelper {
     }
 
     private GroupsV2Operations.NewGroup buildNewGroupV2(
-            String name, Collection<SignalServiceAddress> members, byte[] avatar
+            String name, Set<RecipientId> members, byte[] avatar
     ) {
-        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddressProvider.getSelfAddress());
+        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientIdProvider.getSelfRecipientId());
         if (profileKeyCredential == null) {
             logger.warn("Cannot create a V2 group as self does not have a versioned profile");
             return null;
@@ -149,10 +153,11 @@ public class GroupHelper {
 
         if (!areMembersValid(members)) return null;
 
-        var self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
-                Optional.fromNullable(profileKeyCredential));
+        var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
+                .getUuid()
+                .orNull(), Optional.fromNullable(profileKeyCredential));
         var candidates = members.stream()
-                .map(member -> new GroupCandidate(member.getUuid().get(),
+                .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
                         Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
                 .collect(Collectors.toSet());
 
@@ -166,8 +171,9 @@ public class GroupHelper {
                 0);
     }
 
-    private boolean areMembersValid(final Collection<SignalServiceAddress> members) {
+    private boolean areMembersValid(final Set<RecipientId> members) {
         final var noUuidCapability = members.stream()
+                .map(addressResolver::resolveSignalServiceAddress)
                 .filter(address -> !address.getUuid().isPresent())
                 .map(SignalServiceAddress::getLegacyIdentifier)
                 .collect(Collectors.toSet());
@@ -179,11 +185,11 @@ public class GroupHelper {
 
         final var noGv2Capability = members.stream()
                 .map(profileProvider::getProfile)
-                .filter(profile -> profile != null && !profile.getCapabilities().gv2)
+                .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
                 .collect(Collectors.toSet());
         if (noGv2Capability.size() > 0) {
             logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
-                    noGv2Capability.stream().map(SignalProfile::getDisplayName).collect(Collectors.joining(", ")));
+                    noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
             return false;
         }
 
@@ -206,7 +212,8 @@ public class GroupHelper {
             change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
         }
 
-        final var uuid = this.selfAddressProvider.getSelfAddress().getUuid();
+        final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId())
+                .getUuid();
         if (uuid.isPresent()) {
             change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
         }
@@ -215,7 +222,7 @@ public class GroupHelper {
     }
 
     public Pair<DecryptedGroup, GroupChange> updateGroupV2(
-            GroupInfoV2 groupInfoV2, Set<SignalServiceAddress> newMembers
+            GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
     ) throws IOException {
         final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
         var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
@@ -225,24 +232,25 @@ public class GroupHelper {
         }
 
         var candidates = newMembers.stream()
-                .map(member -> new GroupCandidate(member.getUuid().get(),
+                .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(),
                         Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
                 .collect(Collectors.toSet());
 
-        final var change = groupOperations.createModifyGroupMembershipChange(candidates,
-                selfAddressProvider.getSelfAddress().getUuid().get());
+        final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
+                .getUuid()
+                .get();
+        final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid);
 
-        final var uuid = this.selfAddressProvider.getSelfAddress().getUuid();
-        if (uuid.isPresent()) {
-            change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
-        }
+        change.setSourceUuid(UuidUtil.toByteString(uuid));
 
         return commitChange(groupInfoV2, change);
     }
 
     public Pair<DecryptedGroup, GroupChange> leaveGroup(GroupInfoV2 groupInfoV2) throws IOException {
         var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
-        final var selfUuid = selfAddressProvider.getSelfAddress().getUuid().get();
+        final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
+                .getUuid()
+                .get();
         var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid);
 
         if (selfPendingMember.isPresent()) {
@@ -260,8 +268,8 @@ public class GroupHelper {
         final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
         final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
 
-        final var selfAddress = this.selfAddressProvider.getSelfAddress();
-        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress);
+        final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
+        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
         if (profileKeyCredential == null) {
             throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
         }
@@ -271,7 +279,9 @@ public class GroupHelper {
                 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
                 : groupOperations.createGroupJoinDirect(profileKeyCredential);
 
-        change.setSourceUuid(UuidUtil.toByteString(selfAddress.getUuid().get()));
+        change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId)
+                .getUuid()
+                .get()));
 
         return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
     }
@@ -280,15 +290,15 @@ public class GroupHelper {
         final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
         final var groupOperations = groupsV2Operations.forGroup(groupSecretParams);
 
-        final var selfAddress = this.selfAddressProvider.getSelfAddress();
-        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfAddress);
+        final var selfRecipientId = this.selfRecipientIdProvider.getSelfRecipientId();
+        final var profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(selfRecipientId);
         if (profileKeyCredential == null) {
             throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
         }
 
         final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
 
-        final var uuid = selfAddress.getUuid();
+        final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid();
         if (uuid.isPresent()) {
             change.setSourceUuid(UuidUtil.toByteString(uuid.get()));
         }
@@ -330,7 +340,9 @@ public class GroupHelper {
 
         try {
             decryptedChange = groupOperations.decryptChange(changeActions,
-                    selfAddressProvider.getSelfAddress().getUuid().get());
+                    addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId())
+                            .getUuid()
+                            .get());
             decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
         } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
             throw new IOException(e);
index 5411bb06549a7b7d89107ac34081211bee7b3a21..2676135ba41d37d4961458e03d3a77b52f92b963 100644 (file)
@@ -1,5 +1,6 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
@@ -27,23 +28,27 @@ public final class ProfileHelper {
 
     private final MessageReceiverProvider messageReceiverProvider;
 
+    private final SignalServiceAddressResolver addressResolver;
+
     public ProfileHelper(
             final ProfileKeyProvider profileKeyProvider,
             final UnidentifiedAccessProvider unidentifiedAccessProvider,
             final MessagePipeProvider messagePipeProvider,
-            final MessageReceiverProvider messageReceiverProvider
+            final MessageReceiverProvider messageReceiverProvider,
+            final SignalServiceAddressResolver addressResolver
     ) {
         this.profileKeyProvider = profileKeyProvider;
         this.unidentifiedAccessProvider = unidentifiedAccessProvider;
         this.messagePipeProvider = messagePipeProvider;
         this.messageReceiverProvider = messageReceiverProvider;
+        this.addressResolver = addressResolver;
     }
 
     public ProfileAndCredential retrieveProfileSync(
-            SignalServiceAddress recipient, SignalServiceProfile.RequestType requestType
+            RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) throws IOException {
         try {
-            return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS);
+            return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS);
         } catch (ExecutionException e) {
             if (e.getCause() instanceof PushNetworkException) {
                 throw (PushNetworkException) e.getCause();
@@ -58,11 +63,12 @@ public final class ProfileHelper {
     }
 
     public ListenableFuture<ProfileAndCredential> retrieveProfile(
-            SignalServiceAddress address, SignalServiceProfile.RequestType requestType
+            RecipientId recipientId, SignalServiceProfile.RequestType requestType
     ) {
-        var unidentifiedAccess = getUnidentifiedAccess(address);
-        var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address));
+        var unidentifiedAccess = getUnidentifiedAccess(recipientId);
+        var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId));
 
+        final var address = addressResolver.resolveSignalServiceAddress(recipientId);
         if (unidentifiedAccess.isPresent()) {
             return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address,
                     profileKey,
@@ -126,8 +132,8 @@ public final class ProfileHelper {
         }
     }
 
-    private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
-        var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient);
+    private Optional<UnidentifiedAccess> getUnidentifiedAccess(RecipientId recipientId) {
+        var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId);
 
         if (unidentifiedAccess.isPresent()) {
             return unidentifiedAccess.get().getTargetUnidentifiedAccess();
index ebb728c1c66755e629d73ef2990dcff72f28d293..71d50046a4f6b615915ccc453a8d32ca543cd039 100644 (file)
@@ -1,9 +1,9 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.zkgroup.profiles.ProfileKeyCredential;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 public interface ProfileKeyCredentialProvider {
 
-    ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address);
+    ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
 }
index 9172710e99eeafcd2fb58950a5659cb98f751482..b98d674ed631c474e0fae6ad3d42866fe390f766 100644 (file)
@@ -1,9 +1,9 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.zkgroup.profiles.ProfileKey;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 public interface ProfileKeyProvider {
 
-    ProfileKey getProfileKey(SignalServiceAddress address);
+    ProfileKey getProfileKey(RecipientId address);
 }
index c16b5e0d86835cd0cd44425f6cdf6d30ab56227e..22915a95cf99d744380ea3ec862000ea53deeece 100644 (file)
@@ -1,9 +1,9 @@
 package org.asamk.signal.manager.helper;
 
-import org.asamk.signal.manager.storage.profiles.SignalProfile;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 
 public interface ProfileProvider {
 
-    SignalProfile getProfile(SignalServiceAddress address);
+    Profile getProfile(RecipientId address);
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/SelfAddressProvider.java
deleted file mode 100644 (file)
index 3591064..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.asamk.signal.manager.helper;
-
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-
-public interface SelfAddressProvider {
-
-    SignalServiceAddress getSelfAddress();
-}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/SelfRecipientIdProvider.java
new file mode 100644 (file)
index 0000000..83eb3b2
--- /dev/null
@@ -0,0 +1,8 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+
+public interface SelfRecipientIdProvider {
+
+    RecipientId getSelfRecipientId();
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java b/lib/src/main/java/org/asamk/signal/manager/helper/SignalServiceAddressResolver.java
new file mode 100644 (file)
index 0000000..73efd32
--- /dev/null
@@ -0,0 +1,9 @@
+package org.asamk.signal.manager.helper;
+
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public interface SignalServiceAddressResolver {
+
+    SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId);
+}
index a3b8e3b556dcd1e5529de412934c7b7948228928..0b82fb18ef03b83b041bc08c202f6b5e658651b7 100644 (file)
@@ -1,10 +1,10 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 import java.util.Collection;
 import java.util.List;
@@ -38,22 +38,25 @@ public class UnidentifiedAccessHelper {
         return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
     }
 
-    public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
-        var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
-        if (theirProfileKey == null) {
-            return null;
-        }
-
+    public byte[] getTargetUnidentifiedAccessKey(RecipientId recipient) {
         var targetProfile = profileProvider.getProfile(recipient);
-        if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
+        if (targetProfile == null) {
             return null;
         }
 
-        if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
-            return createUnrestrictedUnidentifiedAccess();
+        switch (targetProfile.getUnidentifiedAccessMode()) {
+            case ENABLED:
+                var theirProfileKey = profileKeyProvider.getProfileKey(recipient);
+                if (theirProfileKey == null) {
+                    return null;
+                }
+
+                return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
+            case UNRESTRICTED:
+                return createUnrestrictedUnidentifiedAccess();
+            default:
+                return null;
         }
-
-        return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
     }
 
     public Optional<UnidentifiedAccessPair> getAccessForSync() {
@@ -73,11 +76,11 @@ public class UnidentifiedAccessHelper {
         }
     }
 
-    public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
+    public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<RecipientId> recipients) {
         return recipients.stream().map(this::getAccessFor).collect(Collectors.toList());
     }
 
-    public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
+    public Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipient) {
         var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
         var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
         var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
index a4b65a6f6e5d33d555649593a0d505bd43ec46ee..cf2c4e4c2cf1875ef9281692d7b7519504619451 100644 (file)
@@ -1,10 +1,10 @@
 package org.asamk.signal.manager.helper;
 
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
 public interface UnidentifiedAccessProvider {
 
-    Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress address);
+    Optional<UnidentifiedAccessPair> getAccessFor(RecipientId recipientId);
 }
index 9aa283a285c58b8e2f8c32a7147286f5fbe045da..a8cb4591a25255a6cf3f7c8ef804f8d76c3f1f0b 100644 (file)
@@ -10,18 +10,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 
 import org.asamk.signal.manager.groups.GroupId;
-import org.asamk.signal.manager.storage.contacts.ContactInfo;
-import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
+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.identities.IdentityKeyStore;
 import org.asamk.signal.manager.storage.messageCache.MessageCache;
 import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
 import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
+import org.asamk.signal.manager.storage.profiles.LegacyProfileStore;
 import org.asamk.signal.manager.storage.profiles.ProfileStore;
 import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
 import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
+import org.asamk.signal.manager.storage.recipients.Contact;
 import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
+import org.asamk.signal.manager.storage.recipients.Profile;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.recipients.RecipientStore;
 import org.asamk.signal.manager.storage.sessions.SessionStore;
@@ -45,6 +48,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.kbs.MasterKey;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.storage.StorageKey;
+import org.whispersystems.signalservice.api.util.UuidUtil;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -57,9 +61,9 @@ import java.nio.channels.ClosedChannelException;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.util.Base64;
+import java.util.HashSet;
 import java.util.List;
 import java.util.UUID;
-import java.util.stream.Collectors;
 
 public class SignalAccount implements Closeable {
 
@@ -88,9 +92,7 @@ public class SignalAccount implements Closeable {
     private SessionStore sessionStore;
     private IdentityKeyStore identityKeyStore;
     private JsonGroupStore groupStore;
-    private JsonContactsStore contactStore;
     private RecipientStore recipientStore;
-    private ProfileStore profileStore;
     private StickerStore stickerStore;
 
     private MessageCache messageCache;
@@ -136,7 +138,6 @@ public class SignalAccount implements Closeable {
         account.username = username;
         account.profileKey = profileKey;
         account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
-        account.contactStore = new JsonContactsStore();
         account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
                 account::mergeRecipients);
         account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
@@ -151,7 +152,6 @@ public class SignalAccount implements Closeable {
                 account.signedPreKeyStore,
                 account.sessionStore,
                 account.identityKeyStore);
-        account.profileStore = new ProfileStore();
         account.stickerStore = new StickerStore();
 
         account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@@ -188,9 +188,9 @@ public class SignalAccount implements Closeable {
         account.profileKey = profileKey;
         account.deviceId = deviceId;
         account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
-        account.contactStore = new JsonContactsStore();
         account.recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username),
                 account::mergeRecipients);
+        account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
         account.preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
         account.signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
         account.sessionStore = new SessionStore(getSessionsPath(dataPath, username),
@@ -203,7 +203,6 @@ public class SignalAccount implements Closeable {
                 account.signedPreKeyStore,
                 account.sessionStore,
                 account.identityKeyStore);
-        account.profileStore = new ProfileStore();
         account.stickerStore = new StickerStore();
 
         account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
@@ -222,23 +221,8 @@ public class SignalAccount implements Closeable {
             setProfileKey(KeyUtils.createProfileKey());
             save();
         }
-        // Store profile keys only in profile store
-        for (var contact : getContactStore().getContacts()) {
-            var profileKeyString = contact.profileKey;
-            if (profileKeyString == null) {
-                continue;
-            }
-            final ProfileKey profileKey;
-            try {
-                profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
-            } catch (InvalidInputException ignored) {
-                continue;
-            }
-            contact.profileKey = null;
-            getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
-        }
         // Ensure our profile key is stored in profile store
-        getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
+        getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
     }
 
     private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
@@ -354,13 +338,15 @@ public class SignalAccount implements Closeable {
         }
 
         recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
+
         var legacyRecipientStoreNode = rootNode.get("recipientStore");
         if (legacyRecipientStoreNode != null) {
             logger.debug("Migrating legacy recipient store.");
             var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
             if (legacyRecipientStore != null) {
-                recipientStore.resolveRecipients(legacyRecipientStore.getAddresses());
+                recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
             }
+            recipientStore.resolveRecipientTrusted(getSelfAddress());
         }
 
         var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
@@ -414,9 +400,9 @@ public class SignalAccount implements Closeable {
                 identityKeyPair,
                 registrationId);
         if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
-            logger.debug("Migrating identity session store.");
+            logger.debug("Migrating legacy identity session store.");
             for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
-                RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
+                RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
                 identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
                 identityKeyStore.setIdentityTrustLevel(recipientId,
                         identity.getIdentityKey(),
@@ -436,20 +422,67 @@ public class SignalAccount implements Closeable {
             groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
         }
 
-        var contactStoreNode = rootNode.get("contactStore");
-        if (contactStoreNode != null) {
-            contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
-        }
-        if (contactStore == null) {
-            contactStore = new JsonContactsStore();
+        if (rootNode.hasNonNull("contactStore")) {
+            logger.debug("Migrating legacy contact store.");
+            final var contactStoreNode = rootNode.get("contactStore");
+            final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
+            for (var contact : contactStore.getContacts()) {
+                final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
+                recipientStore.storeContact(recipientId,
+                        new Contact(contact.name,
+                                contact.color,
+                                contact.messageExpirationTime,
+                                contact.blocked,
+                                contact.archived));
+
+                // Store profile keys only in profile store
+                var profileKeyString = contact.profileKey;
+                if (profileKeyString != null) {
+                    final ProfileKey profileKey;
+                    try {
+                        profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
+                        getProfileStore().storeProfileKey(recipientId, profileKey);
+                    } catch (InvalidInputException e) {
+                        logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
+                    }
+                }
+            }
         }
 
-        var profileStoreNode = rootNode.get("profileStore");
-        if (profileStoreNode != null) {
-            profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
-        }
-        if (profileStore == null) {
-            profileStore = new ProfileStore();
+        if (rootNode.hasNonNull("profileStore")) {
+            logger.debug("Migrating legacy profile store.");
+            var profileStoreNode = rootNode.get("profileStore");
+            final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
+            for (var profileEntry : legacyProfileStore.getProfileEntries()) {
+                var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress());
+                recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
+                recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
+                final var profile = profileEntry.getProfile();
+                if (profile != null) {
+                    final var capabilities = new HashSet<Profile.Capability>();
+                    if (profile.getCapabilities().gv1Migration) {
+                        capabilities.add(Profile.Capability.gv1Migration);
+                    }
+                    if (profile.getCapabilities().gv2) {
+                        capabilities.add(Profile.Capability.gv2);
+                    }
+                    if (profile.getCapabilities().storage) {
+                        capabilities.add(Profile.Capability.storage);
+                    }
+                    final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
+                            profile.getGivenName(),
+                            profile.getFamilyName(),
+                            profile.getAbout(),
+                            profile.getAboutEmoji(),
+                            profile.isUnrestrictedUnidentifiedAccess()
+                                    ? Profile.UnidentifiedAccessMode.UNRESTRICTED
+                                    : profile.getUnidentifiedAccess() != null
+                                            ? Profile.UnidentifiedAccessMode.ENABLED
+                                            : Profile.UnidentifiedAccessMode.DISABLED,
+                            capabilities);
+                    recipientStore.storeProfile(recipientId, newProfile);
+                }
+            }
         }
 
         var stickerStoreNode = rootNode.get("stickerStore");
@@ -460,24 +493,6 @@ public class SignalAccount implements Closeable {
             stickerStore = new StickerStore();
         }
 
-        if (recipientStore.isEmpty()) {
-            recipientStore.resolveRecipient(getSelfAddress());
-
-            recipientStore.resolveRecipients(contactStore.getContacts()
-                    .stream()
-                    .map(ContactInfo::getAddress)
-                    .collect(Collectors.toList()));
-
-            for (var group : groupStore.getGroups()) {
-                if (group instanceof GroupInfoV1) {
-                    var groupInfoV1 = (GroupInfoV1) group;
-                    groupInfoV1.members = groupInfoV1.members.stream()
-                            .map(m -> recipientStore.resolveServiceAddress(m))
-                            .collect(Collectors.toSet());
-                }
-            }
-        }
-
         messageCache = new MessageCache(getMessageCachePath(dataPath, username));
 
         var threadStoreNode = rootNode.get("threadStore");
@@ -489,10 +504,15 @@ public class SignalAccount implements Closeable {
                     continue;
                 }
                 try {
-                    var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
-                    if (contactInfo != null) {
-                        contactInfo.messageExpirationTime = thread.messageExpirationTime;
-                        contactStore.updateContact(contactInfo);
+                    if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
+                        final var recipientId = recipientStore.resolveRecipient(thread.id);
+                        var contact = recipientStore.getContact(recipientId);
+                        if (contact != null) {
+                            recipientStore.storeContact(recipientId,
+                                    Contact.newBuilder(contact)
+                                            .withMessageExpirationTime(thread.messageExpirationTime)
+                                            .build());
+                        }
                     } else {
                         var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
                         if (groupInfo instanceof GroupInfoV1) {
@@ -500,7 +520,8 @@ public class SignalAccount implements Closeable {
                             groupStore.updateGroup(groupInfo);
                         }
                     }
-                } catch (Exception ignored) {
+                } catch (Exception e) {
+                    logger.warn("Failed to read legacy thread info: {}", e.getMessage());
                 }
             }
         }
@@ -533,8 +554,6 @@ public class SignalAccount implements Closeable {
                 .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
                 .put("registered", registered)
                 .putPOJO("groupStore", groupStore)
-                .putPOJO("contactStore", contactStore)
-                .putPOJO("profileStore", profileStore)
                 .putPOJO("stickerStore", stickerStore);
         try {
             try (var output = new ByteArrayOutputStream()) {
@@ -602,8 +621,8 @@ public class SignalAccount implements Closeable {
         return groupStore;
     }
 
-    public JsonContactsStore getContactStore() {
-        return contactStore;
+    public ContactsStore getContactStore() {
+        return recipientStore;
     }
 
     public RecipientStore getRecipientStore() {
@@ -611,7 +630,7 @@ public class SignalAccount implements Closeable {
     }
 
     public ProfileStore getProfileStore() {
-        return profileStore;
+        return recipientStore;
     }
 
     public StickerStore getStickerStore() {
@@ -638,6 +657,10 @@ public class SignalAccount implements Closeable {
         return new SignalServiceAddress(uuid, username);
     }
 
+    public RecipientId getSelfRecipientId() {
+        return recipientStore.resolveRecipientTrusted(getSelfAddress());
+    }
+
     public int getDeviceId() {
         return deviceId;
     }
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactsStore.java
new file mode 100644 (file)
index 0000000..25d429e
--- /dev/null
@@ -0,0 +1,16 @@
+package org.asamk.signal.manager.storage.contacts;
+
+import org.asamk.signal.manager.storage.recipients.Contact;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.whispersystems.libsignal.util.Pair;
+
+import java.util.List;
+
+public interface ContactsStore {
+
+    void storeContact(RecipientId recipientId, Contact contact);
+
+    Contact getContact(RecipientId recipientId);
+
+    List<Pair<RecipientId, Contact>> getContacts();
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/JsonContactsStore.java
deleted file mode 100644 (file)
index b80dfe9..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.asamk.signal.manager.storage.contacts;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class JsonContactsStore {
-
-    @JsonProperty("contacts")
-    private List<ContactInfo> contacts = new ArrayList<>();
-
-    public void updateContact(ContactInfo contact) {
-        final var contactAddress = contact.getAddress();
-        for (var i = 0; i < contacts.size(); i++) {
-            if (contacts.get(i).getAddress().matches(contactAddress)) {
-                contacts.set(i, contact);
-                return;
-            }
-        }
-
-        contacts.add(contact);
-    }
-
-    public ContactInfo getContact(SignalServiceAddress address) {
-        for (var contact : contacts) {
-            if (contact.getAddress().matches(address)) {
-                if (contact.uuid == null) {
-                    contact.uuid = address.getUuid().orNull();
-                } else if (contact.number == null) {
-                    contact.number = address.getNumber().orNull();
-                }
-
-                return contact;
-            }
-        }
-        return null;
-    }
-
-    public List<ContactInfo> getContacts() {
-        return new ArrayList<>(contacts);
-    }
-
-    /**
-     * Remove all contacts from the store
-     */
-    public void clear() {
-        contacts.clear();
-    }
-}
similarity index 82%
rename from lib/src/main/java/org/asamk/signal/manager/storage/contacts/ContactInfo.java
rename to lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java
index 4dd132f74af7d67af2db70b307ca256df9837977..a90f87e0fbc1ce5a12084ba6f8eb6e1bcfb0a37d 100644 (file)
@@ -9,7 +9,7 @@ import java.util.UUID;
 
 import static com.fasterxml.jackson.annotation.JsonProperty.Access.WRITE_ONLY;
 
-public class ContactInfo {
+public class LegacyContactInfo {
 
     @JsonProperty
     public String name;
@@ -38,12 +38,7 @@ public class ContactInfo {
     @JsonProperty(defaultValue = "false")
     public boolean archived;
 
-    public ContactInfo() {
-    }
-
-    public ContactInfo(SignalServiceAddress address) {
-        this.number = address.getNumber().orNull();
-        this.uuid = address.getUuid().orNull();
+    public LegacyContactInfo() {
     }
 
     @JsonIgnore
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyJsonContactsStore.java
new file mode 100644 (file)
index 0000000..229e1e9
--- /dev/null
@@ -0,0 +1,19 @@
+package org.asamk.signal.manager.storage.contacts;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LegacyJsonContactsStore {
+
+    @JsonProperty("contacts")
+    private final List<LegacyContactInfo> contacts = new ArrayList<>();
+
+    private LegacyJsonContactsStore() {
+    }
+
+    public List<LegacyContactInfo> getContacts() {
+        return contacts;
+    }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java
new file mode 100644 (file)
index 0000000..8e1d5c8
--- /dev/null
@@ -0,0 +1,75 @@
+package org.asamk.signal.manager.storage.profiles;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+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.ObjectMapper;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+public class LegacyProfileStore {
+
+    private static final ObjectMapper jsonProcessor = new ObjectMapper();
+
+    @JsonProperty("profiles")
+    @JsonDeserialize(using = ProfileStoreDeserializer.class)
+    private final List<LegacySignalProfileEntry> profiles = new ArrayList<>();
+
+    public List<LegacySignalProfileEntry> getProfileEntries() {
+        return profiles;
+    }
+
+    public static class ProfileStoreDeserializer extends JsonDeserializer<List<LegacySignalProfileEntry>> {
+
+        @Override
+        public List<LegacySignalProfileEntry> deserialize(
+                JsonParser jsonParser, DeserializationContext deserializationContext
+        ) throws IOException {
+            JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+
+            var profileEntries = new ArrayList<LegacySignalProfileEntry>();
+
+            if (node.isArray()) {
+                for (var entry : node) {
+                    var name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
+                    var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
+                    final var serviceAddress = new SignalServiceAddress(uuid, name);
+                    ProfileKey profileKey = null;
+                    try {
+                        profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText()));
+                    } catch (InvalidInputException ignored) {
+                    }
+                    ProfileKeyCredential profileKeyCredential = null;
+                    if (entry.hasNonNull("profileKeyCredential")) {
+                        try {
+                            profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
+                                    .decode(entry.get("profileKeyCredential").asText()));
+                        } catch (Throwable ignored) {
+                        }
+                    }
+                    var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
+                    var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
+                    profileEntries.add(new LegacySignalProfileEntry(serviceAddress,
+                            profileKey,
+                            lastUpdateTimestamp,
+                            profile,
+                            profileKeyCredential));
+                }
+            }
+
+            return profileEntries;
+        }
+    }
+}
similarity index 82%
rename from lib/src/main/java/org/asamk/signal/manager/storage/profiles/SignalProfileEntry.java
rename to lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java
index a81fbcb51c49cefc3a3c0d29be935684feb9cfca..1e2f7ec84e3640b9415c17ab6674003a727faa6d 100644 (file)
@@ -4,7 +4,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
 import org.signal.zkgroup.profiles.ProfileKeyCredential;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 
-public class SignalProfileEntry {
+public class LegacySignalProfileEntry {
 
     private final SignalServiceAddress serviceAddress;
 
@@ -16,9 +16,7 @@ public class SignalProfileEntry {
 
     private final ProfileKeyCredential profileKeyCredential;
 
-    private boolean requestPending;
-
-    public SignalProfileEntry(
+    public LegacySignalProfileEntry(
             final SignalServiceAddress serviceAddress,
             final ProfileKey profileKey,
             final long lastUpdateTimestamp,
@@ -51,12 +49,4 @@ public class SignalProfileEntry {
     public ProfileKeyCredential getProfileKeyCredential() {
         return profileKeyCredential;
     }
-
-    public boolean isRequestPending() {
-        return requestPending;
-    }
-
-    public void setRequestPending(final boolean requestPending) {
-        this.requestPending = requestPending;
-    }
 }
index dc69a7ee72bd1a5c10d97df461095b207e4db71a..c600f0fba87f6ca0fe84ca1ac97955f7997341dd 100644 (file)
 package org.asamk.signal.manager.storage.profiles;
 
-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.signal.zkgroup.InvalidInputException;
+import org.asamk.signal.manager.storage.recipients.Profile;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.signal.zkgroup.profiles.ProfileKeyCredential;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.util.UuidUtil;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.List;
-
-public class ProfileStore {
-
-    private static final ObjectMapper jsonProcessor = new ObjectMapper();
-
-    @JsonProperty("profiles")
-    @JsonDeserialize(using = ProfileStoreDeserializer.class)
-    @JsonSerialize(using = ProfileStoreSerializer.class)
-    private final List<SignalProfileEntry> profiles = new ArrayList<>();
-
-    public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
-        for (var entry : profiles) {
-            if (entry.getServiceAddress().matches(serviceAddress)) {
-                return entry;
-            }
-        }
-        return null;
-    }
-
-    public ProfileKey getProfileKey(SignalServiceAddress serviceAddress) {
-        for (var entry : profiles) {
-            if (entry.getServiceAddress().matches(serviceAddress)) {
-                return entry.getProfileKey();
-            }
-        }
-        return null;
-    }
-
-    public void updateProfile(
-            SignalServiceAddress serviceAddress,
-            ProfileKey profileKey,
-            long now,
-            SignalProfile profile,
-            ProfileKeyCredential profileKeyCredential
-    ) {
-        var newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile, profileKeyCredential);
-        for (var i = 0; i < profiles.size(); i++) {
-            if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
-                profiles.set(i, newEntry);
-                return;
-            }
-        }
-
-        profiles.add(newEntry);
-    }
-
-    public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
-        var newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null);
-        for (var i = 0; i < profiles.size(); i++) {
-            if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
-                if (!profiles.get(i).getProfileKey().equals(profileKey)) {
-                    profiles.set(i, newEntry);
-                }
-                return;
-            }
-        }
-
-        profiles.add(newEntry);
-    }
 
-    public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
+public interface ProfileStore {
 
-        @Override
-        public List<SignalProfileEntry> deserialize(
-                JsonParser jsonParser, DeserializationContext deserializationContext
-        ) throws IOException {
-            JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+    Profile getProfile(RecipientId recipientId);
 
-            var addresses = new ArrayList<SignalProfileEntry>();
+    ProfileKey getProfileKey(RecipientId recipientId);
 
-            if (node.isArray()) {
-                for (var entry : node) {
-                    var name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
-                    var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
-                    final var serviceAddress = new SignalServiceAddress(uuid, name);
-                    ProfileKey profileKey = null;
-                    try {
-                        profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText()));
-                    } catch (InvalidInputException ignored) {
-                    }
-                    ProfileKeyCredential profileKeyCredential = null;
-                    if (entry.hasNonNull("profileKeyCredential")) {
-                        try {
-                            profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
-                                    .decode(entry.get("profileKeyCredential").asText()));
-                        } catch (Throwable ignored) {
-                        }
-                    }
-                    var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
-                    var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
-                    addresses.add(new SignalProfileEntry(serviceAddress,
-                            profileKey,
-                            lastUpdateTimestamp,
-                            profile,
-                            profileKeyCredential));
-                }
-            }
+    ProfileKeyCredential getProfileKeyCredential(RecipientId recipientId);
 
-            return addresses;
-        }
-    }
+    void storeProfile(RecipientId recipientId, Profile profile);
 
-    public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
+    void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
 
-        @Override
-        public void serialize(
-                List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider
-        ) throws IOException {
-            json.writeStartArray();
-            for (var profileEntry : profiles) {
-                final var address = profileEntry.getServiceAddress();
-                json.writeStartObject();
-                if (address.getNumber().isPresent()) {
-                    json.writeStringField("name", address.getNumber().get());
-                }
-                if (address.getUuid().isPresent()) {
-                    json.writeStringField("uuid", address.getUuid().get().toString());
-                }
-                json.writeStringField("profileKey",
-                        Base64.getEncoder().encodeToString(profileEntry.getProfileKey().serialize()));
-                json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
-                json.writeObjectField("profile", profileEntry.getProfile());
-                if (profileEntry.getProfileKeyCredential() != null) {
-                    json.writeStringField("profileKeyCredential",
-                            Base64.getEncoder().encodeToString(profileEntry.getProfileKeyCredential().serialize()));
-                }
-                json.writeEndObject();
-            }
-            json.writeEndArray();
-        }
-    }
+    void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential);
 }
index 45201e188156136c53190ecb87c0a8b03c4c5b81..60d9aefc78e85ec445d88e88ccad88a68c0f34bf 100644 (file)
@@ -3,12 +3,11 @@ package org.asamk.signal.manager.storage.profiles;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
-
 public class SignalProfile {
 
     @JsonProperty
-    private final String identityKey;
+    @JsonIgnore
+    private String identityKey;
 
     @JsonProperty
     private final String name;
@@ -29,28 +28,6 @@ public class SignalProfile {
     private final Capabilities capabilities;
 
     public SignalProfile(
-            final String identityKey,
-            final String name,
-            final String about,
-            final String aboutEmoji,
-            final String unidentifiedAccess,
-            final boolean unrestrictedUnidentifiedAccess,
-            final SignalServiceProfile.Capabilities capabilities
-    ) {
-        this.identityKey = identityKey;
-        this.name = name;
-        this.about = about;
-        this.aboutEmoji = aboutEmoji;
-        this.unidentifiedAccess = unidentifiedAccess;
-        this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
-        this.capabilities = new Capabilities();
-        this.capabilities.storage = capabilities.isStorage();
-        this.capabilities.gv1Migration = capabilities.isGv1Migration();
-        this.capabilities.gv2 = capabilities.isGv2();
-    }
-
-    public SignalProfile(
-            @JsonProperty("identityKey") final String identityKey,
             @JsonProperty("name") final String name,
             @JsonProperty("about") final String about,
             @JsonProperty("aboutEmoji") final String aboutEmoji,
@@ -58,7 +35,6 @@ public class SignalProfile {
             @JsonProperty("unrestrictedUnidentifiedAccess") final boolean unrestrictedUnidentifiedAccess,
             @JsonProperty("capabilities") final Capabilities capabilities
     ) {
-        this.identityKey = identityKey;
         this.name = name;
         this.about = about;
         this.aboutEmoji = aboutEmoji;
@@ -67,17 +43,24 @@ public class SignalProfile {
         this.capabilities = capabilities;
     }
 
-    public String getIdentityKey() {
-        return identityKey;
-    }
+    public String getGivenName() {
+        if (name == null) {
+            return null;
+        }
+
+        String[] parts = name.split("\0");
 
-    public String getName() {
-        return name;
+        return parts.length < 1 ? null : parts[0];
     }
 
-    public String getDisplayName() {
-        // First name and last name (if set) are separated by a NULL char + trim space in case only one is filled
-        return name == null ? "" : name.replace("\0", " ").trim();
+    public String getFamilyName() {
+        if (name == null) {
+            return null;
+        }
+
+        String[] parts = name.split("\0");
+
+        return parts.length < 2 ? null : parts[1];
     }
 
     public String getAbout() {
@@ -100,31 +83,6 @@ public class SignalProfile {
         return capabilities;
     }
 
-    @Override
-    public String toString() {
-        return "SignalProfile{"
-                + "identityKey='"
-                + identityKey
-                + '\''
-                + ", name='"
-                + name
-                + '\''
-                + ", about='"
-                + about
-                + '\''
-                + ", aboutEmoji='"
-                + aboutEmoji
-                + '\''
-                + ", unidentifiedAccess='"
-                + unidentifiedAccess
-                + '\''
-                + ", unrestrictedUnidentifiedAccess="
-                + unrestrictedUnidentifiedAccess
-                + ", capabilities="
-                + capabilities
-                + '}';
-    }
-
     public static class Capabilities {
 
         @JsonIgnore
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Contact.java
new file mode 100644 (file)
index 0000000..f2867c9
--- /dev/null
@@ -0,0 +1,111 @@
+package org.asamk.signal.manager.storage.recipients;
+
+public class Contact {
+
+    private final String name;
+
+    private final String color;
+
+    private final int messageExpirationTime;
+
+    private final boolean blocked;
+
+    private final boolean archived;
+
+    public Contact(
+            final String name,
+            final String color,
+            final int messageExpirationTime,
+            final boolean blocked,
+            final boolean archived
+    ) {
+        this.name = name;
+        this.color = color;
+        this.messageExpirationTime = messageExpirationTime;
+        this.blocked = blocked;
+        this.archived = archived;
+    }
+
+    private Contact(final Builder builder) {
+        name = builder.name;
+        color = builder.color;
+        messageExpirationTime = builder.messageExpirationTime;
+        blocked = builder.blocked;
+        archived = builder.archived;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(final Contact copy) {
+        Builder builder = new Builder();
+        builder.name = copy.getName();
+        builder.color = copy.getColor();
+        builder.messageExpirationTime = copy.getMessageExpirationTime();
+        builder.blocked = copy.isBlocked();
+        builder.archived = copy.isArchived();
+        return builder;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public int getMessageExpirationTime() {
+        return messageExpirationTime;
+    }
+
+    public boolean isBlocked() {
+        return blocked;
+    }
+
+    public boolean isArchived() {
+        return archived;
+    }
+
+    public static final class Builder {
+
+        private String name;
+        private String color;
+        private int messageExpirationTime;
+        private boolean blocked;
+        private boolean archived;
+
+        private Builder() {
+        }
+
+        public Builder withName(final String val) {
+            name = val;
+            return this;
+        }
+
+        public Builder withColor(final String val) {
+            color = val;
+            return this;
+        }
+
+        public Builder withMessageExpirationTime(final int val) {
+            messageExpirationTime = val;
+            return this;
+        }
+
+        public Builder withBlocked(final boolean val) {
+            blocked = val;
+            return this;
+        }
+
+        public Builder withArchived(final boolean val) {
+            archived = val;
+            return this;
+        }
+
+        public Contact build() {
+            return new Contact(this);
+        }
+    }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java
new file mode 100644 (file)
index 0000000..dcdc896
--- /dev/null
@@ -0,0 +1,199 @@
+package org.asamk.signal.manager.storage.recipients;
+
+import org.whispersystems.signalservice.internal.util.Util;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class Profile {
+
+    private final long lastUpdateTimestamp;
+
+    private final String givenName;
+
+    private final String familyName;
+
+    private final String about;
+
+    private final String aboutEmoji;
+
+    private final UnidentifiedAccessMode unidentifiedAccessMode;
+
+    private final Set<Capability> capabilities;
+
+    public Profile(
+            final long lastUpdateTimestamp,
+            final String givenName,
+            final String familyName,
+            final String about,
+            final String aboutEmoji,
+            final UnidentifiedAccessMode unidentifiedAccessMode,
+            final Set<Capability> capabilities
+    ) {
+        this.lastUpdateTimestamp = lastUpdateTimestamp;
+        this.givenName = givenName;
+        this.familyName = familyName;
+        this.about = about;
+        this.aboutEmoji = aboutEmoji;
+        this.unidentifiedAccessMode = unidentifiedAccessMode;
+        this.capabilities = capabilities;
+    }
+
+    private Profile(final Builder builder) {
+        lastUpdateTimestamp = builder.lastUpdateTimestamp;
+        givenName = builder.givenName;
+        familyName = builder.familyName;
+        about = builder.about;
+        aboutEmoji = builder.aboutEmoji;
+        unidentifiedAccessMode = builder.unidentifiedAccessMode;
+        capabilities = builder.capabilities;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(final Profile copy) {
+        Builder builder = new Builder();
+        builder.lastUpdateTimestamp = copy.getLastUpdateTimestamp();
+        builder.givenName = copy.getGivenName();
+        builder.familyName = copy.getFamilyName();
+        builder.about = copy.getAbout();
+        builder.aboutEmoji = copy.getAboutEmoji();
+        builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
+        builder.capabilities = copy.getCapabilities();
+        return builder;
+    }
+
+    public long getLastUpdateTimestamp() {
+        return lastUpdateTimestamp;
+    }
+
+    public String getGivenName() {
+        return givenName;
+    }
+
+    public String getFamilyName() {
+        return familyName;
+    }
+
+    public String getInternalServiceName() {
+        if (familyName == null) {
+            return givenName == null ? "" : givenName;
+        }
+        return String.join("\0", givenName == null ? "" : givenName, familyName);
+    }
+
+    public String getDisplayName() {
+        final var noGivenName = Util.isEmpty(givenName);
+        final var noFamilyName = Util.isEmpty(familyName);
+
+        if (noGivenName && noFamilyName) {
+            return "";
+        } else if (noGivenName) {
+            return familyName;
+        } else if (noFamilyName) {
+            return givenName;
+        }
+
+        return givenName + " " + familyName;
+    }
+
+    public String getAbout() {
+        return about;
+    }
+
+    public String getAboutEmoji() {
+        return aboutEmoji;
+    }
+
+    public UnidentifiedAccessMode getUnidentifiedAccessMode() {
+        return unidentifiedAccessMode;
+    }
+
+    public Set<Capability> getCapabilities() {
+        return capabilities;
+    }
+
+    public enum UnidentifiedAccessMode {
+        UNKNOWN,
+        DISABLED,
+        ENABLED,
+        UNRESTRICTED;
+
+        static UnidentifiedAccessMode valueOfOrUnknown(String value) {
+            try {
+                return valueOf(value);
+            } catch (IllegalArgumentException ignored) {
+                return UNKNOWN;
+            }
+        }
+    }
+
+    public enum Capability {
+        gv2,
+        storage,
+        gv1Migration;
+
+        static Capability valueOfOrNull(String value) {
+            try {
+                return valueOf(value);
+            } catch (IllegalArgumentException ignored) {
+                return null;
+            }
+        }
+    }
+
+    public static final class Builder {
+
+        private String givenName;
+        private String familyName;
+        private String about;
+        private String aboutEmoji;
+        private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
+        private Set<Capability> capabilities = Collections.emptySet();
+        private long lastUpdateTimestamp = 0;
+
+        private Builder() {
+        }
+
+        public Builder withGivenName(final String val) {
+            givenName = val;
+            return this;
+        }
+
+        public Builder withFamilyName(final String val) {
+            familyName = val;
+            return this;
+        }
+
+        public Builder withAbout(final String val) {
+            about = val;
+            return this;
+        }
+
+        public Builder withAboutEmoji(final String val) {
+            aboutEmoji = val;
+            return this;
+        }
+
+        public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) {
+            unidentifiedAccessMode = val;
+            return this;
+        }
+
+        public Builder withCapabilities(final Set<Capability> val) {
+            capabilities = val;
+            return this;
+        }
+
+        public Profile build() {
+            return new Profile(this);
+        }
+
+        public Builder withLastUpdateTimestamp(final long val) {
+            lastUpdateTimestamp = val;
+            return this;
+        }
+    }
+}
diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java
new file mode 100644 (file)
index 0000000..3ccf821
--- /dev/null
@@ -0,0 +1,131 @@
+package org.asamk.signal.manager.storage.recipients;
+
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+
+public class Recipient {
+
+    private final RecipientId recipientId;
+
+    private final SignalServiceAddress address;
+
+    private final Contact contact;
+
+    private final ProfileKey profileKey;
+
+    private final ProfileKeyCredential profileKeyCredential;
+
+    private final Profile profile;
+
+    public Recipient(
+            final RecipientId recipientId,
+            final SignalServiceAddress address,
+            final Contact contact,
+            final ProfileKey profileKey,
+            final ProfileKeyCredential profileKeyCredential,
+            final Profile profile
+    ) {
+        this.recipientId = recipientId;
+        this.address = address;
+        this.contact = contact;
+        this.profileKey = profileKey;
+        this.profileKeyCredential = profileKeyCredential;
+        this.profile = profile;
+    }
+
+    private Recipient(final Builder builder) {
+        recipientId = builder.recipientId;
+        address = builder.address;
+        contact = builder.contact;
+        profileKey = builder.profileKey;
+        profileKeyCredential = builder.profileKeyCredential;
+        profile = builder.profile;
+    }
+
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    public static Builder newBuilder(final Recipient copy) {
+        Builder builder = new Builder();
+        builder.recipientId = copy.getRecipientId();
+        builder.address = copy.getAddress();
+        builder.contact = copy.getContact();
+        builder.profileKey = copy.getProfileKey();
+        builder.profileKeyCredential = copy.getProfileKeyCredential();
+        builder.profile = copy.getProfile();
+        return builder;
+    }
+
+    public RecipientId getRecipientId() {
+        return recipientId;
+    }
+
+    public SignalServiceAddress getAddress() {
+        return address;
+    }
+
+    public Contact getContact() {
+        return contact;
+    }
+
+    public ProfileKey getProfileKey() {
+        return profileKey;
+    }
+
+    public ProfileKeyCredential getProfileKeyCredential() {
+        return profileKeyCredential;
+    }
+
+    public Profile getProfile() {
+        return profile;
+    }
+
+    public static final class Builder {
+
+        private RecipientId recipientId;
+        private SignalServiceAddress address;
+        private Contact contact;
+        private ProfileKey profileKey;
+        private ProfileKeyCredential profileKeyCredential;
+        private Profile profile;
+
+        private Builder() {
+        }
+
+        public Builder withRecipientId(final RecipientId val) {
+            recipientId = val;
+            return this;
+        }
+
+        public Builder withAddress(final SignalServiceAddress val) {
+            address = val;
+            return this;
+        }
+
+        public Builder withContact(final Contact val) {
+            contact = val;
+            return this;
+        }
+
+        public Builder withProfileKey(final ProfileKey val) {
+            profileKey = val;
+            return this;
+        }
+
+        public Builder withProfileKeyCredential(final ProfileKeyCredential val) {
+            profileKeyCredential = val;
+            return this;
+        }
+
+        public Builder withProfile(final Profile val) {
+            profile = val;
+            return this;
+        }
+
+        public Recipient build() {
+            return new Recipient(this);
+        }
+    }
+}
index 7235b6626cadff64e6a7bf653ad37b9a2652f705..b400e45d4c9ab1b2ddbf1c5e44cecb5c93f35048 100644 (file)
@@ -3,6 +3,11 @@ package org.asamk.signal.manager.storage.recipients;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.asamk.signal.manager.storage.Utils;
+import org.asamk.signal.manager.storage.contacts.ContactsStore;
+import org.asamk.signal.manager.storage.profiles.ProfileStore;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.profiles.ProfileKey;
+import org.signal.zkgroup.profiles.ProfileKeyCredential;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.libsignal.util.Pair;
@@ -17,14 +22,17 @@ import java.io.FileNotFoundException;
 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.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
-public class RecipientStore {
+public class RecipientStore implements ContactsStore, ProfileStore {
 
     private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class);
 
@@ -32,7 +40,7 @@ public class RecipientStore {
     private final File file;
     private final RecipientMergeHandler recipientMergeHandler;
 
-    private final Map<RecipientId, SignalServiceAddress> recipients;
+    private final Map<RecipientId, Recipient> recipients;
     private final Map<RecipientId, RecipientId> recipientsMerged = new HashMap<>();
 
     private long lastId;
@@ -40,16 +48,57 @@ public class RecipientStore {
     public static RecipientStore load(File file, RecipientMergeHandler recipientMergeHandler) throws IOException {
         final var objectMapper = Utils.createStorageObjectMapper();
         try (var inputStream = new FileInputStream(file)) {
-            var storage = objectMapper.readValue(inputStream, Storage.class);
-            return new RecipientStore(objectMapper,
-                    file,
-                    recipientMergeHandler,
-                    storage.recipients.stream()
-                            .collect(Collectors.toMap(r -> new RecipientId(r.id),
-                                    r -> new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
-                                            r.uuid).transform(UuidUtil::parseOrThrow),
-                                            org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.name)))),
-                    storage.lastId);
+            final var storage = objectMapper.readValue(inputStream, Storage.class);
+            final var recipients = storage.recipients.stream().map(r -> {
+                final var recipientId = new RecipientId(r.id);
+                final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable(
+                        r.uuid).transform(UuidUtil::parseOrThrow),
+                        org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number));
+
+                Contact contact = null;
+                if (r.contact != null) {
+                    contact = new Contact(r.contact.name,
+                            r.contact.color,
+                            r.contact.messageExpirationTime,
+                            r.contact.blocked,
+                            r.contact.archived);
+                }
+
+                ProfileKey profileKey = null;
+                if (r.profileKey != null) {
+                    try {
+                        profileKey = new ProfileKey(Base64.getDecoder().decode(r.profileKey));
+                    } catch (InvalidInputException ignored) {
+                    }
+                }
+
+                ProfileKeyCredential profileKeyCredential = null;
+                if (r.profileKeyCredential != null) {
+                    try {
+                        profileKeyCredential = new ProfileKeyCredential(Base64.getDecoder()
+                                .decode(r.profileKeyCredential));
+                    } catch (Throwable ignored) {
+                    }
+                }
+
+                Profile profile = null;
+                if (r.profile != null) {
+                    profile = new Profile(r.profile.lastUpdateTimestamp,
+                            r.profile.givenName,
+                            r.profile.familyName,
+                            r.profile.about,
+                            r.profile.aboutEmoji,
+                            Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
+                            r.profile.capabilities.stream()
+                                    .map(Profile.Capability::valueOfOrNull)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.toSet()));
+                }
+
+                return new Recipient(recipientId, address, contact, profileKey, profileKeyCredential, profile);
+            }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));
+
+            return new RecipientStore(objectMapper, file, recipientMergeHandler, recipients, storage.lastId);
         } catch (FileNotFoundException e) {
             logger.debug("Creating new recipient store.");
             return new RecipientStore(objectMapper, file, recipientMergeHandler, new HashMap<>(), 0);
@@ -60,7 +109,7 @@ public class RecipientStore {
             final ObjectMapper objectMapper,
             final File file,
             final RecipientMergeHandler recipientMergeHandler,
-            final Map<RecipientId, SignalServiceAddress> recipients,
+            final Map<RecipientId, Recipient> recipients,
             final long lastId
     ) {
         this.objectMapper = objectMapper;
@@ -71,6 +120,12 @@ public class RecipientStore {
     }
 
     public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) {
+        synchronized (recipients) {
+            return getRecipient(recipientId).getAddress();
+        }
+    }
+
+    public Recipient getRecipient(RecipientId recipientId) {
         synchronized (recipients) {
             while (recipientsMerged.containsKey(recipientId)) {
                 recipientId = recipientsMerged.get(recipientId);
@@ -92,11 +147,11 @@ public class RecipientStore {
         return resolveRecipient(new SignalServiceAddress(null, number), false);
     }
 
-    public RecipientId resolveRecipient(SignalServiceAddress address) {
+    public RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
         return resolveRecipient(address, true);
     }
 
-    public List<RecipientId> resolveRecipients(List<SignalServiceAddress> addresses) {
+    public List<RecipientId> resolveRecipientsTrusted(List<SignalServiceAddress> addresses) {
         final List<RecipientId> recipientIds;
         final List<Pair<RecipientId, RecipientId>> toBeMerged = new ArrayList<>();
         synchronized (recipients) {
@@ -114,10 +169,84 @@ public class RecipientStore {
         return recipientIds;
     }
 
-    public RecipientId resolveRecipientUntrusted(SignalServiceAddress address) {
+    public RecipientId resolveRecipient(SignalServiceAddress address) {
         return resolveRecipient(address, false);
     }
 
+    @Override
+    public void storeContact(final RecipientId recipientId, final Contact contact) {
+        synchronized (recipients) {
+            final var recipient = recipients.get(recipientId);
+            storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withContact(contact).build());
+        }
+    }
+
+    @Override
+    public Contact getContact(final RecipientId recipientId) {
+        final var recipient = getRecipient(recipientId);
+        return recipient == null ? null : recipient.getContact();
+    }
+
+    @Override
+    public List<Pair<RecipientId, Contact>> getContacts() {
+        return recipients.entrySet()
+                .stream()
+                .filter(e -> e.getValue().getContact() != null)
+                .map(e -> new Pair<>(e.getKey(), e.getValue().getContact()))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public Profile getProfile(final RecipientId recipientId) {
+        final var recipient = getRecipient(recipientId);
+        return recipient == null ? null : recipient.getProfile();
+    }
+
+    @Override
+    public ProfileKey getProfileKey(final RecipientId recipientId) {
+        final var recipient = getRecipient(recipientId);
+        return recipient == null ? null : recipient.getProfileKey();
+    }
+
+    @Override
+    public ProfileKeyCredential getProfileKeyCredential(final RecipientId recipientId) {
+        final var recipient = getRecipient(recipientId);
+        return recipient == null ? null : recipient.getProfileKeyCredential();
+    }
+
+    @Override
+    public void storeProfile(final RecipientId recipientId, final Profile profile) {
+        synchronized (recipients) {
+            final var recipient = recipients.get(recipientId);
+            storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfile(profile).build());
+        }
+    }
+
+    @Override
+    public void storeProfileKey(final RecipientId recipientId, final ProfileKey profileKey) {
+        synchronized (recipients) {
+            final var recipient = recipients.get(recipientId);
+            storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withProfileKey(profileKey).build());
+        }
+    }
+
+    @Override
+    public void storeProfileKeyCredential(
+            final RecipientId recipientId, final ProfileKeyCredential profileKeyCredential
+    ) {
+        synchronized (recipients) {
+            final var recipient = recipients.get(recipientId);
+            storeRecipientLocked(recipientId,
+                    Recipient.newBuilder(recipient).withProfileKeyCredential(profileKeyCredential).build());
+        }
+    }
+
+    public boolean isEmpty() {
+        synchronized (recipients) {
+            return recipients.isEmpty();
+        }
+    }
+
     /**
      * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source.
      *                    Has no effect, if the address contains only a number or a uuid.
@@ -141,20 +270,20 @@ public class RecipientStore {
             SignalServiceAddress address, boolean isHighTrust
     ) {
         final var byNumber = !address.getNumber().isPresent()
-                ? Optional.<RecipientId>empty()
-                : findByName(address.getNumber().get());
+                ? Optional.<Recipient>empty()
+                : findByNameLocked(address.getNumber().get());
         final var byUuid = !address.getUuid().isPresent()
-                ? Optional.<RecipientId>empty()
-                : findByUuid(address.getUuid().get());
+                ? Optional.<Recipient>empty()
+                : findByUuidLocked(address.getUuid().get());
 
         if (byNumber.isEmpty() && byUuid.isEmpty()) {
             logger.debug("Got new recipient, both uuid and number are unknown");
 
             if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) {
-                return new Pair<>(addNewRecipient(address), Optional.empty());
+                return new Pair<>(addNewRecipientLocked(address), Optional.empty());
             }
 
-            return new Pair<>(addNewRecipient(new SignalServiceAddress(address.getUuid().get(), null)),
+            return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)),
                     Optional.empty());
         }
 
@@ -162,79 +291,138 @@ public class RecipientStore {
                 || !address.getUuid().isPresent()
                 || !address.getNumber().isPresent()
                 || byNumber.equals(byUuid)) {
-            return new Pair<>(byUuid.orElseGet(byNumber::get), Optional.empty());
+            return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty());
         }
 
         if (byNumber.isEmpty()) {
             logger.debug("Got recipient existing with uuid, updating with high trust number");
-            recipients.put(byUuid.get(), address);
-            save();
-            return new Pair<>(byUuid.get(), Optional.empty());
+            updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
+            return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
         }
 
         if (byUuid.isEmpty()) {
             logger.debug("Got recipient existing with number, updating with high trust uuid");
-            recipients.put(byNumber.get(), address);
-            save();
-            return new Pair<>(byNumber.get(), Optional.empty());
+            updateRecipientAddressLocked(byNumber.get().getRecipientId(), address);
+            return new Pair<>(byNumber.get().getRecipientId(), Optional.empty());
         }
 
-        final var byNumberAddress = recipients.get(byNumber.get());
-        if (byNumberAddress.getUuid().isPresent()) {
+        if (byNumber.get().getAddress().getUuid().isPresent()) {
             logger.debug(
                     "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number");
 
-            recipients.put(byNumber.get(), new SignalServiceAddress(byNumberAddress.getUuid().get(), null));
-            recipients.put(byUuid.get(), address);
-            save();
-            return new Pair<>(byUuid.get(), Optional.empty());
+            updateRecipientAddressLocked(byNumber.get().getRecipientId(),
+                    new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null));
+            updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
+            return new Pair<>(byUuid.get().getRecipientId(), Optional.empty());
         }
 
         logger.debug("Got separate recipients for high trust number and uuid, need to merge them");
-        recipients.put(byUuid.get(), address);
-        recipients.remove(byNumber.get());
-        save();
-        return new Pair<>(byUuid.get(), byNumber);
+        updateRecipientAddressLocked(byUuid.get().getRecipientId(), address);
+        mergeRecipientsLocked(byUuid.get().getRecipientId(), byNumber.get().getRecipientId());
+        return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId));
     }
 
-    private RecipientId addNewRecipient(final SignalServiceAddress serviceAddress) {
-        final var nextRecipientId = nextId();
-        recipients.put(nextRecipientId, serviceAddress);
-        save();
+    private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) {
+        final var nextRecipientId = nextIdLocked();
+        storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null));
         return nextRecipientId;
     }
 
-    private Optional<RecipientId> findByName(final String number) {
+    private void updateRecipientAddressLocked(
+            final RecipientId recipientId, final SignalServiceAddress address
+    ) {
+        final var nextRecipientId = nextIdLocked();
+        final var recipient = recipients.get(recipientId);
+        storeRecipientLocked(nextRecipientId, Recipient.newBuilder(recipient).withAddress(address).build());
+    }
+
+    private void storeRecipientLocked(
+            final RecipientId recipientId, final Recipient recipient
+    ) {
+        recipients.put(recipientId, recipient);
+        saveLocked();
+    }
+
+    private void mergeRecipientsLocked(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
+        final var recipient = recipients.get(recipientId);
+        final var toBeMergedRecipient = recipients.get(toBeMergedRecipientId);
+        recipients.put(recipientId,
+                new Recipient(recipientId,
+                        recipient.getAddress(),
+                        recipient.getContact() != null ? recipient.getContact() : toBeMergedRecipient.getContact(),
+                        recipient.getProfileKey() != null
+                                ? recipient.getProfileKey()
+                                : toBeMergedRecipient.getProfileKey(),
+                        recipient.getProfileKeyCredential() != null
+                                ? recipient.getProfileKeyCredential()
+                                : toBeMergedRecipient.getProfileKeyCredential(),
+                        recipient.getProfile() != null ? recipient.getProfile() : toBeMergedRecipient.getProfile()));
+        recipients.remove(toBeMergedRecipientId);
+        saveLocked();
+    }
+
+    private Optional<Recipient> findByNameLocked(final String number) {
         return recipients.entrySet()
                 .stream()
-                .filter(entry -> entry.getValue().getNumber().isPresent() && number.equals(entry.getValue()
+                .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue()
+                        .getAddress()
                         .getNumber()
                         .get()))
                 .findFirst()
-                .map(Map.Entry::getKey);
+                .map(Map.Entry::getValue);
     }
 
-    private Optional<RecipientId> findByUuid(final UUID uuid) {
+    private Optional<Recipient> findByUuidLocked(final UUID uuid) {
         return recipients.entrySet()
                 .stream()
-                .filter(entry -> entry.getValue().getUuid().isPresent() && uuid.equals(entry.getValue()
+                .filter(entry -> entry.getValue().getAddress().getUuid().isPresent() && uuid.equals(entry.getValue()
+                        .getAddress()
                         .getUuid()
                         .get()))
                 .findFirst()
-                .map(Map.Entry::getKey);
+                .map(Map.Entry::getValue);
     }
 
-    private RecipientId nextId() {
+    private RecipientId nextIdLocked() {
         return new RecipientId(++this.lastId);
     }
 
-    private void save() {
-        var storage = new Storage(recipients.entrySet()
-                .stream()
-                .map(pair -> new Storage.Recipient(pair.getKey().getId(),
-                        pair.getValue().getNumber().orNull(),
-                        pair.getValue().getUuid().transform(UUID::toString).orNull()))
-                .collect(Collectors.toList()), lastId);
+    private void saveLocked() {
+        final var base64 = Base64.getEncoder();
+        var storage = new Storage(recipients.entrySet().stream().map(pair -> {
+            final var recipient = pair.getValue();
+            final var contact = recipient.getContact() == null
+                    ? null
+                    : new Storage.Recipient.Contact(recipient.getContact().getName(),
+                            recipient.getContact().getColor(),
+                            recipient.getContact().getMessageExpirationTime(),
+                            recipient.getContact().isBlocked(),
+                            recipient.getContact().isArchived());
+            final var profile = recipient.getProfile() == null
+                    ? null
+                    : new Storage.Recipient.Profile(recipient.getProfile().getLastUpdateTimestamp(),
+                            recipient.getProfile().getGivenName(),
+                            recipient.getProfile().getFamilyName(),
+                            recipient.getProfile().getAbout(),
+                            recipient.getProfile().getAboutEmoji(),
+                            recipient.getProfile().getUnidentifiedAccessMode().name(),
+                            recipient.getProfile()
+                                    .getCapabilities()
+                                    .stream()
+                                    .map(Enum::name)
+                                    .collect(Collectors.toSet()));
+            return new Storage.Recipient(pair.getKey().getId(),
+                    recipient.getAddress().getNumber().orNull(),
+                    recipient.getAddress().getUuid().transform(UUID::toString).orNull(),
+                    recipient.getProfileKey() == null
+                            ? null
+                            : base64.encodeToString(recipient.getProfileKey().serialize()),
+                    recipient.getProfileKeyCredential() == null
+                            ? null
+                            : base64.encodeToString(recipient.getProfileKeyCredential().serialize()),
+                    contact,
+                    profile);
+        }).collect(Collectors.toList()), lastId);
 
         // Write to memory first to prevent corrupting the file in case of serialization errors
         try (var inMemoryOutput = new ByteArrayOutputStream()) {
@@ -249,17 +437,11 @@ public class RecipientStore {
         }
     }
 
-    public boolean isEmpty() {
-        synchronized (recipients) {
-            return recipients.isEmpty();
-        }
-    }
-
     private static class Storage {
 
-        private List<Recipient> recipients;
+        public List<Recipient> recipients;
 
-        private long lastId;
+        public long lastId;
 
         // For deserialization
         private Storage() {
@@ -270,40 +452,96 @@ public class RecipientStore {
             this.lastId = lastId;
         }
 
-        public List<Recipient> getRecipients() {
-            return recipients;
-        }
-
-        public long getLastId() {
-            return lastId;
-        }
+        private static class Recipient {
 
-        public static class Recipient {
-
-            private long id;
-            private String name;
-            private String uuid;
+            public long id;
+            public String number;
+            public String uuid;
+            public String profileKey;
+            public String profileKeyCredential;
+            public Contact contact;
+            public Profile profile;
 
             // For deserialization
             private Recipient() {
             }
 
-            public Recipient(final long id, final String name, final String uuid) {
+            public Recipient(
+                    final long id,
+                    final String number,
+                    final String uuid,
+                    final String profileKey,
+                    final String profileKeyCredential,
+                    final Contact contact,
+                    final Profile profile
+            ) {
                 this.id = id;
-                this.name = name;
+                this.number = number;
                 this.uuid = uuid;
+                this.profileKey = profileKey;
+                this.profileKeyCredential = profileKeyCredential;
+                this.contact = contact;
+                this.profile = profile;
             }
 
-            public long getId() {
-                return id;
-            }
+            private static class Contact {
 
-            public String getName() {
-                return name;
+                public String name;
+                public String color;
+                public int messageExpirationTime;
+                public boolean blocked;
+                public boolean archived;
+
+                // For deserialization
+                public Contact() {
+                }
+
+                public Contact(
+                        final String name,
+                        final String color,
+                        final int messageExpirationTime,
+                        final boolean blocked,
+                        final boolean archived
+                ) {
+                    this.name = name;
+                    this.color = color;
+                    this.messageExpirationTime = messageExpirationTime;
+                    this.blocked = blocked;
+                    this.archived = archived;
+                }
             }
 
-            public String getUuid() {
-                return uuid;
+            private static class Profile {
+
+                public long lastUpdateTimestamp;
+                public String givenName;
+                public String familyName;
+                public String about;
+                public String aboutEmoji;
+                public String unidentifiedAccessMode;
+                public Set<String> capabilities;
+
+                // For deserialization
+                private Profile() {
+                }
+
+                public Profile(
+                        final long lastUpdateTimestamp,
+                        final String givenName,
+                        final String familyName,
+                        final String about,
+                        final String aboutEmoji,
+                        final String unidentifiedAccessMode,
+                        final Set<String> capabilities
+                ) {
+                    this.lastUpdateTimestamp = lastUpdateTimestamp;
+                    this.givenName = givenName;
+                    this.familyName = familyName;
+                    this.about = about;
+                    this.aboutEmoji = aboutEmoji;
+                    this.unidentifiedAccessMode = unidentifiedAccessMode;
+                    this.capabilities = capabilities;
+                }
             }
         }
     }
index 63820b517271f25160daab15d2d1ea59e7afb44a..244f34f6bfe815c489fe9b3b3377149cc46392c3 100644 (file)
@@ -1,16 +1,19 @@
 package org.asamk.signal.manager.util;
 
-import org.asamk.signal.manager.storage.profiles.SignalProfile;
+import org.asamk.signal.manager.storage.recipients.Profile;
 import org.signal.zkgroup.profiles.ProfileKey;
+import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
 import org.whispersystems.signalservice.api.crypto.ProfileCipher;
 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
 
 import java.util.Base64;
+import java.util.Date;
+import java.util.HashSet;
 
 public class ProfileUtils {
 
-    public static SignalProfile decryptProfile(
+    public static Profile decryptProfile(
             final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
     ) {
         var profileCipher = new ProfileCipher(profileKey);
@@ -28,13 +31,28 @@ public class ProfileUtils {
             } catch (IllegalArgumentException e) {
                 unidentifiedAccess = null;
             }
-            return new SignalProfile(encryptedProfile.getIdentityKey(),
-                    name,
+            final var nameParts = splitName(name);
+            final var capabilities = new HashSet<Profile.Capability>();
+            if (encryptedProfile.getCapabilities().isGv1Migration()) {
+                capabilities.add(Profile.Capability.gv1Migration);
+            }
+            if (encryptedProfile.getCapabilities().isGv2()) {
+                capabilities.add(Profile.Capability.gv2);
+            }
+            if (encryptedProfile.getCapabilities().isStorage()) {
+                capabilities.add(Profile.Capability.storage);
+            }
+            return new Profile(new Date().getTime(),
+                    nameParts.first(),
+                    nameParts.second(),
                     about,
                     aboutEmoji,
-                    unidentifiedAccess,
-                    encryptedProfile.isUnrestrictedUnidentifiedAccess(),
-                    encryptedProfile.getCapabilities());
+                    encryptedProfile.isUnrestrictedUnidentifiedAccess()
+                            ? Profile.UnidentifiedAccessMode.UNRESTRICTED
+                            : unidentifiedAccess != null
+                                    ? Profile.UnidentifiedAccessMode.ENABLED
+                                    : Profile.UnidentifiedAccessMode.DISABLED,
+                    capabilities);
         } catch (InvalidCiphertextException e) {
             return null;
         }
@@ -51,4 +69,17 @@ public class ProfileUtils {
             return null;
         }
     }
+
+    private static Pair<String, String> splitName(String name) {
+        String[] parts = name.split("\0");
+
+        switch (parts.length) {
+            case 0:
+                return new Pair<>(null, null);
+            case 1:
+                return new Pair<>(parts[0], null);
+            default:
+                return new Pair<>(parts[0], parts[1]);
+        }
+    }
 }
index 0093ab9b19e35d70ac31f05a5e0abfd1d1d5b331..e7a21b88a721bd8d2376a1b6229b7980e2daba10 100644 (file)
@@ -87,7 +87,7 @@ public interface Signal extends DBusInterface {
 
     void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure;
 
-    boolean isContactBlocked(final String number);
+    boolean isContactBlocked(final String number) throws Error.InvalidNumber;
 
     boolean isGroupBlocked(final byte[] groupId);
 
index 91df4b0697a17c132b63194242ec30a622223b82..2aa7dfca546b64c72a9ea64d6141189f5078b404 100644 (file)
@@ -18,6 +18,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.messages.shared.SharedContact;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.util.InvalidNumberException;
 
 import java.io.IOException;
 import java.util.Base64;
@@ -667,7 +668,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
 
     private String formatContact(SignalServiceAddress address) {
         final var number = address.getLegacyIdentifier();
-        var name = m.getContactOrProfileName(number);
+        String name = null;
+        try {
+            name = m.getContactOrProfileName(number);
+        } catch (InvalidNumberException ignored) {
+        }
         if (name == null || name.isEmpty()) {
             return number;
         } else {
index 4b27e17d3558d0b41aeaac779457043c9829e000..577db3db3e53b2df9d901aec4e015e772ae1e270 100644 (file)
@@ -18,7 +18,10 @@ public class ListContactsCommand implements LocalCommand {
 
         var contacts = m.getContacts();
         for (var c : contacts) {
-            writer.println("Number: {} Name: {} Blocked: {}", c.number, c.name, c.blocked);
+            writer.println("Number: {} Name: {} Blocked: {}",
+                    m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier(),
+                    c.second().getName(),
+                    c.second().isBlocked());
         }
     }
 }
index f50363cb3d2e5223689766ddd11ceac14dac7b1b..e58f78299cf7b1c9b6e9218183390e093a6cd68b 100644 (file)
@@ -11,6 +11,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException;
 import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.util.ErrorUtils;
 import org.freedesktop.dbus.exceptions.DBusExecutionException;
+import org.whispersystems.libsignal.util.Pair;
 import org.whispersystems.libsignal.util.guava.Optional;
 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
 import org.whispersystems.signalservice.api.messages.SendMessageResult;
@@ -248,7 +249,7 @@ public class DbusSignalImpl implements Signal {
     public String getContactName(final String number) {
         try {
             return m.getContactOrProfileName(number);
-        } catch (Exception e) {
+        } catch (InvalidNumberException e) {
             throw new Error.InvalidNumber(e.getMessage());
         }
     }
@@ -383,11 +384,10 @@ public class DbusSignalImpl implements Signal {
     // all numbers the system knows
     @Override
     public List<String> listNumbers() {
-        return Stream.concat(m.getIdentities()
-                .stream()
-                .map(IdentityInfo::getRecipientId)
+        return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
+                m.getContacts().stream().map(Pair::first))
                 .map(m::resolveSignalServiceAddress)
-                .map(a -> a.getNumber().orNull()), m.getContacts().stream().map(c -> c.number))
+                .map(a -> a.getNumber().orNull())
                 .filter(Objects::nonNull)
                 .distinct()
                 .collect(Collectors.toList());
@@ -399,8 +399,8 @@ public class DbusSignalImpl implements Signal {
         var numbers = new ArrayList<String>();
         var contacts = m.getContacts();
         for (var c : contacts) {
-            if (c.name != null && c.name.equals(name)) {
-                numbers.add(c.number);
+            if (name.equals(c.second().getName())) {
+                numbers.add(m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier());
             }
         }
         // Try profiles if no contact name was found
@@ -449,13 +449,11 @@ public class DbusSignalImpl implements Signal {
 
     @Override
     public boolean isContactBlocked(final String number) {
-        var contacts = m.getContacts();
-        for (var c : contacts) {
-            if (c.number.equals(number)) {
-                return c.blocked;
-            }
+        try {
+            return m.isContactBlocked(number);
+        } catch (InvalidNumberException e) {
+            throw new Error.InvalidNumber(e.getMessage());
         }
-        return false;
     }
 
     @Override