From 7ac6c9a170eba9dab7f9f0ebc6c3bffc017ab691 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 May 2022 15:27:02 +0200 Subject: [PATCH 01/16] Cleanup fileChannel if file locking fails --- .../signal/manager/storage/SignalAccount.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 33b9e80c..79f43670 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -884,17 +884,25 @@ public class SignalAccount implements Closeable { private static Pair openFileChannel(File fileName, boolean waitForLock) throws IOException { var fileChannel = new RandomAccessFile(fileName, "rw").getChannel(); - var lock = fileChannel.tryLock(); - if (lock == null) { - if (!waitForLock) { - logger.debug("Config file is in use by another instance."); - throw new IOException("Config file is in use by another instance."); + try { + var lock = fileChannel.tryLock(); + if (lock == null) { + if (!waitForLock) { + logger.debug("Config file is in use by another instance."); + throw new IOException("Config file is in use by another instance."); + } + logger.info("Config file is in use by another instance, waiting…"); + lock = fileChannel.lock(); + logger.info("Config file lock acquired."); + } + final var result = new Pair<>(fileChannel, lock); + fileChannel = null; + return result; + } finally { + if (fileChannel != null) { + fileChannel.close(); } - logger.info("Config file is in use by another instance, waiting…"); - lock = fileChannel.lock(); - logger.info("Config file lock acquired."); } - return new Pair<>(fileChannel, lock); } public void addPreKeys(ServiceIdType serviceIdType, List records) { -- 2.51.0 From 06a9884e99f620ca5c85015d4fa8ee23abbad48d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 May 2022 19:12:20 +0200 Subject: [PATCH 02/16] Don't reset avatar url path when updating profile with same avatar --- .../java/org/asamk/signal/manager/helper/ProfileHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 982acddd..7d68124a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -157,7 +157,9 @@ public final class ProfileHelper { paymentsAddress, avatarUploadParams, List.of(/* TODO */)); - builder.withAvatarUrlPath(avatarPath.orElse(null)); + if (!avatarUploadParams.keepTheSame) { + builder.withAvatarUrlPath(avatarPath.orElse(null)); + } newProfile = builder.build(); } } -- 2.51.0 From b1e56faab2b2417af4f4a7f1033ffd752a9d16e4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 May 2022 15:26:34 +0200 Subject: [PATCH 03/16] Store profile sharing enabled for contacts Automatically enable it when sending direct messages --- .../signal/manager/helper/ContactHelper.java | 3 +++ .../signal/manager/helper/SendHelper.java | 17 +++++++++--- .../signal/manager/helper/StorageHelper.java | 19 ++++++++----- .../signal/manager/storage/SignalAccount.java | 3 ++- .../manager/storage/recipients/Contact.java | 21 +++++++++++++-- .../storage/recipients/RecipientStore.java | 27 ++++++++++++++----- .../asamk/signal/dbus/DbusManagerImpl.java | 2 +- 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java index 71b2ded8..164fea76 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java @@ -36,6 +36,9 @@ public class ContactHelper { public void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + if (blocked) { + builder.withProfileSharingEnabled(false); + } account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 0797f84c..59a7a671 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; +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.sendLog.MessageSendLogEntry; @@ -73,10 +74,20 @@ public class SendHelper { public SendMessageResult sendMessage( final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId ) throws IOException { - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + var contact = account.getContactStore().getContact(recipientId); + if (contact == null || !contact.isProfileSharingEnabled()) { + final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + contact = contactBuilder.withProfileSharingEnabled(true).build(); + account.getContactStore().storeContact(recipientId, contact); + } + + final var expirationTime = contact.getMessageExpirationTime(); messageBuilder.withExpiration(expirationTime); - messageBuilder.withProfileKey(account.getProfileKey().serialize()); + + if (!contact.isBlocked()) { + final var profileKey = account.getProfileKey().serialize(); + messageBuilder.withProfileKey(profileKey); + } final var message = messageBuilder.build(); return sendMessage(message, recipientId); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 4ca383e3..de28638e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -88,13 +88,18 @@ public class StorageHelper { final var recipientId = account.getRecipientStore().resolveRecipient(address); final var contact = account.getContactStore().getContact(recipientId); - if (contactRecord.getGivenName().isPresent() || contactRecord.getFamilyName().isPresent() || ( - (contact == null || !contact.isBlocked()) && contactRecord.isBlocked() - )) { - final var newContact = (contact == null ? Contact.newBuilder() : Contact.newBuilder(contact)).withBlocked( - contactRecord.isBlocked()).withName(( - contactRecord.getGivenName().orElse("") + " " + contactRecord.getFamilyName().orElse("") - ).trim()).build(); + final var blocked = contact != null && contact.isBlocked(); + final var profileShared = contact != null && contact.isProfileSharingEnabled(); + if (contactRecord.getGivenName().isPresent() + || contactRecord.getFamilyName().isPresent() + || blocked != contactRecord.isBlocked() + || profileShared != contactRecord.isProfileSharingEnabled()) { + final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + final var name = contactRecord.getGivenName().orElse("") + " " + contactRecord.getFamilyName().orElse(""); + final var newContact = contactBuilder.withBlocked(contactRecord.isBlocked()) + .withName(name.trim()) + .withProfileSharingEnabled(contactRecord.isProfileSharingEnabled()) + .build(); account.getContactStore().storeContact(recipientId, newContact); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 79f43670..6b2d063b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -715,7 +715,8 @@ public class SignalAccount implements Closeable { contact.color, contact.messageExpirationTime, contact.blocked, - contact.archived)); + contact.archived, + false)); // Store profile keys only in profile store var profileKeyString = contact.profileKey; 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 index c03f5c7f..25d6151f 100644 --- 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 @@ -14,18 +14,22 @@ public class Contact { private final boolean archived; + private final boolean profileSharingEnabled; + public Contact( final String name, final String color, final int messageExpirationTime, final boolean blocked, - final boolean archived + final boolean archived, + final boolean profileSharingEnabled ) { this.name = name; this.color = color; this.messageExpirationTime = messageExpirationTime; this.blocked = blocked; this.archived = archived; + this.profileSharingEnabled = profileSharingEnabled; } private Contact(final Builder builder) { @@ -34,6 +38,7 @@ public class Contact { messageExpirationTime = builder.messageExpirationTime; blocked = builder.blocked; archived = builder.archived; + profileSharingEnabled = builder.profileSharingEnabled; } public static Builder newBuilder() { @@ -47,6 +52,7 @@ public class Contact { builder.messageExpirationTime = copy.getMessageExpirationTime(); builder.blocked = copy.isBlocked(); builder.archived = copy.isArchived(); + builder.profileSharingEnabled = copy.isProfileSharingEnabled(); return builder; } @@ -70,6 +76,10 @@ public class Contact { return archived; } + public boolean isProfileSharingEnabled() { + return profileSharingEnabled; + } + @Override public boolean equals(final Object o) { if (this == o) return true; @@ -78,13 +88,14 @@ public class Contact { return messageExpirationTime == contact.messageExpirationTime && blocked == contact.blocked && archived == contact.archived + && profileSharingEnabled == contact.profileSharingEnabled && Objects.equals(name, contact.name) && Objects.equals(color, contact.color); } @Override public int hashCode() { - return Objects.hash(name, color, messageExpirationTime, blocked, archived); + return Objects.hash(name, color, messageExpirationTime, blocked, archived, profileSharingEnabled); } public static final class Builder { @@ -94,6 +105,7 @@ public class Contact { private int messageExpirationTime; private boolean blocked; private boolean archived; + private boolean profileSharingEnabled; private Builder() { } @@ -123,6 +135,11 @@ public class Contact { return this; } + public Builder withProfileSharingEnabled(final boolean val) { + profileSharingEnabled = val; + return this; + } + public Contact build() { return new Contact(this); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 619ec418..16ec9bed 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -26,6 +26,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -75,7 +76,8 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile r.contact.color, r.contact.messageExpirationTime, r.contact.blocked, - r.contact.archived); + r.contact.archived, + r.contact.profileSharingEnabled); } ProfileKey profileKey = null; @@ -149,10 +151,6 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile this.lastId = lastId; } - public boolean isBulkUpdating() { - return isBulkUpdating; - } - public void setBulkUpdating(final boolean bulkUpdating) { isBulkUpdating = bulkUpdating; if (!bulkUpdating) { @@ -174,6 +172,15 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile } } + public Collection getRecipientIdsWithEnabledProfileSharing() { + synchronized (recipients) { + return recipients.values().stream().filter(r -> { + final var contact = r.getContact(); + return contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled(); + }).map(Recipient::getRecipientId).toList(); + } + } + @Override public RecipientId resolveRecipient(ServiceId serviceId) { return resolveRecipient(new RecipientAddress(serviceId.uuid()), false, false); @@ -545,7 +552,8 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile recipientContact.getColor(), recipientContact.getMessageExpirationTime(), recipientContact.isBlocked(), - recipientContact.isArchived()); + recipientContact.isArchived(), + recipientContact.isProfileSharingEnabled()); final var recipientProfile = recipient.getProfile(); final var profile = recipientProfile == null ? null @@ -599,7 +607,12 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile ) { private record Contact( - String name, String color, int messageExpirationTime, boolean blocked, boolean archived + String name, + String color, + int messageExpirationTime, + boolean blocked, + boolean archived, + boolean profileSharingEnabled ) {} private record Profile( diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 29889c3d..5b33b039 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -554,7 +554,7 @@ public class DbusManagerImpl implements Manager { return null; } return new Pair<>(new RecipientAddress(null, n), - new Contact(contactName, null, 0, signal.isContactBlocked(n), false)); + new Contact(contactName, null, 0, signal.isContactBlocked(n), false, false)); }).filter(Objects::nonNull).toList(); } -- 2.51.0 From cf1626ea315ba3d7d76fc8f9ec0acc1c7597d631 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 May 2022 12:19:06 +0200 Subject: [PATCH 04/16] Rotate profile key after blocking a contact/group --- .../org/asamk/signal/manager/ManagerImpl.java | 27 ++++++++-- .../manager/actions/SendProfileKeyAction.java | 33 ++++++++++++ .../UpdateAccountAttributesAction.java | 20 +++++++ .../signal/manager/helper/GroupHelper.java | 18 +++++++ .../signal/manager/helper/GroupV2Helper.java | 32 +++++++++++ .../helper/IncomingMessageHandler.java | 9 ++++ .../signal/manager/helper/ProfileHelper.java | 54 +++++++++++++++---- .../signal/manager/helper/SendHelper.java | 15 ++++++ .../signal/manager/helper/StorageHelper.java | 1 + .../signal/manager/storage/SignalAccount.java | 3 +- .../storage/profiles/ProfileStore.java | 2 + .../storage/recipients/RecipientStore.java | 23 +++++--- 12 files changed, 218 insertions(+), 19 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index f6205dba..b3e6a0ae 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -694,27 +694,48 @@ class ManagerImpl implements Manager { return; } final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients); + final var selfRecipientId = account.getSelfRecipientId(); + boolean shouldRotateProfileKey = false; for (final var recipientId : recipientIds) { + if (context.getContactHelper().isContactBlocked(recipientId) == blocked) { + continue; + } context.getContactHelper().setContactBlocked(recipientId, blocked); + // if we don't have a common group with the blocked contact we need to rotate the profile key + shouldRotateProfileKey = blocked && ( + shouldRotateProfileKey || account.getGroupStore() + .getGroups() + .stream() + .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId)) + ); + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - // TODO cycle our profile key, if we're not together in a group with recipient context.getSyncHelper().sendBlockedList(); } @Override public void setGroupsBlocked( final Collection groupIds, final boolean blocked - ) throws GroupNotFoundException, NotMasterDeviceException { + ) throws GroupNotFoundException, NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } if (groupIds.size() == 0) { return; } + boolean shouldRotateProfileKey = false; for (final var groupId : groupIds) { + if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) { + continue; + } context.getGroupHelper().setGroupBlocked(groupId, blocked); + shouldRotateProfileKey = blocked; + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - // TODO cycle our profile key context.getSyncHelper().sendBlockedList(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java new file mode 100644 index 00000000..5695e458 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java @@ -0,0 +1,33 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.helper.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.util.Objects; + +public class SendProfileKeyAction implements HandleAction { + + private final RecipientId recipientId; + + public SendProfileKeyAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSendHelper().sendProfileKey(recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendProfileKeyAction that = (SendProfileKeyAction) o; + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return Objects.hash(recipientId); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java new file mode 100644 index 00000000..da04dd18 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.helper.Context; + +public class UpdateAccountAttributesAction implements HandleAction { + + private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction(); + + private UpdateAccountAttributesAction() { + } + + public static UpdateAccountAttributesAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccountHelper().updateAccountAttributes(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 9d4f712c..ab3e1264 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -254,6 +254,24 @@ public class GroupHelper { return result; } + public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException { + var group = getGroupForUpdating(groupId); + + if (group instanceof GroupInfoV2 groupInfoV2) { + Pair groupChangePair; + try { + groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + groupInfoV2 = (GroupInfoV2) getGroup(groupId, true); + groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); + } + if (groupChangePair != null) { + sendUpdateGroupV2Message(groupInfoV2, groupChangePair.first(), groupChangePair.second()); + } + } + } + public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, InactiveGroupLinkException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 06a0b89a..385acc59 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -25,6 +25,7 @@ import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; +import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -340,6 +342,36 @@ class GroupV2Helper { return commitChange(groupInfoV2, change); } + Pair updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException { + Optional selfInGroup = groupInfoV2.getGroup() == null + ? Optional.empty() + : DecryptedGroupUtil.findMemberByUuid(groupInfoV2.getGroup().getMembersList(), getSelfAci().uuid()); + if (selfInGroup.isEmpty()) { + logger.trace("Not updating group, self not in group " + groupInfoV2.getGroupId().toBase64()); + return null; + } + + final var profileKey = context.getAccount().getProfileKey(); + if (Arrays.equals(profileKey.serialize(), selfInGroup.get().getProfileKey().toByteArray())) { + logger.trace("Not updating group, own Profile Key is already up to date in group " + + groupInfoV2.getGroupId().toBase64()); + return null; + } + logger.debug("Updating own profile key in group " + groupInfoV2.getGroupId().toBase64()); + + final var selfRecipientId = context.getAccount().getSelfRecipientId(); + final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId); + if (profileKeyCredential == null) { + logger.trace("Cannot update profile key as self does not have a versioned profile"); + return null; + } + + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential); + change.setSourceUuid(getSelfAci().toByteString()); + return commitChange(groupInfoV2, change); + } + GroupChange joinGroup( GroupMasterKey groupMasterKey, GroupLinkPassword groupLinkPassword, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 489be834..5e310a48 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendPniIdentityKeyAction; +import org.asamk.signal.manager.actions.SendProfileKeyAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; @@ -18,6 +19,7 @@ import org.asamk.signal.manager.actions.SendSyncConfigurationAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncKeysAction; +import org.asamk.signal.manager.actions.UpdateAccountAttributesAction; import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.StickerPackId; @@ -246,6 +248,13 @@ public final class IncomingMessageHandler { if (content.isNeedsReceipt()) { actions.add(new SendReceiptAction(sender, message.getTimestamp())); + } else { + // Message wasn't sent as unidentified sender message + final var contact = context.getAccount().getContactStore().getContact(sender); + if (contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled()) { + actions.add(UpdateAccountAttributesAction.create()); + actions.add(new SendProfileKeyAction(sender)); + } } actions.addAll(handleSignalServiceDataMessage(message, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7d68124a..4bc00317 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -4,11 +4,15 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.protocol.IdentityKey; @@ -57,6 +61,35 @@ public final class ProfileHelper { this.context = context; } + public void rotateProfileKey() throws IOException { + var profileKey = KeyUtils.createProfileKey(); + account.setProfileKey(profileKey); + context.getAccountHelper().updateAccountAttributes(); + setProfile(true, true, null, null, null, null, null); + // TODO update profile key in storage + + final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing(); + for (final var recipientId : recipientIds) { + context.getSendHelper().sendProfileKey(recipientId); + } + + final var selfRecipientId = account.getSelfRecipientId(); + final var activeGroupIds = account.getGroupStore() + .getGroups() + .stream() + .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId)) + .map(g -> (GroupInfoV2) g) + .map(GroupInfoV2::getGroupId) + .toList(); + for (final var groupId : activeGroupIds) { + try { + context.getGroupHelper().updateGroupProfileKey(groupId); + } catch (GroupNotFoundException | NotAGroupMemberException | IOException e) { + logger.warn("Failed to update group profile key: {}", e.getMessage()); + } + } + } + public Profile getRecipientProfile(RecipientId recipientId) { return getRecipientProfile(recipientId, false); } @@ -106,11 +139,12 @@ public final class ProfileHelper { public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { - setProfile(true, givenName, familyName, about, aboutEmoji, avatar); + setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar); } public void setProfile( boolean uploadProfile, + boolean forceUploadAvatar, String givenName, final String familyName, String about, @@ -134,13 +168,14 @@ public final class ProfileHelper { var newProfile = builder.build(); if (uploadProfile) { - try (final var streamDetails = avatar != null && avatar.isPresent() ? Utils.createStreamDetailsFromFile( - avatar.get()) : null) { - final var avatarUploadParams = avatar == null - ? AvatarUploadParams.unchanged(true) - : avatar.isPresent() - ? AvatarUploadParams.forAvatar(streamDetails) - : AvatarUploadParams.unchanged(false); + final var streamDetails = avatar != null && avatar.isPresent() + ? Utils.createStreamDetailsFromFile(avatar.get()) + : forceUploadAvatar && avatar == null ? context.getAvatarStore() + .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null; + try (streamDetails) { + final var avatarUploadParams = streamDetails != null + ? AvatarUploadParams.forAvatar(streamDetails) + : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> { try { return SignalServiceProtos.PaymentAddress.parseFrom(data); @@ -148,6 +183,7 @@ public final class ProfileHelper { return null; } }); + logger.debug("Uploading new profile"); final var avatarPath = dependencies.getAccountManager() .setVersionedProfile(account.getAci(), account.getProfileKey(), @@ -156,7 +192,7 @@ public final class ProfileHelper { newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), paymentsAddress, avatarUploadParams, - List.of(/* TODO */)); + List.of(/* TODO implement support for badges */)); if (!avatarUploadParams.keepTheSame) { builder.withAvatarUrlPath(avatarPath.orElse(null)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 59a7a671..a2e2379b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -139,6 +139,21 @@ public class SendHelper { return result; } + public SendMessageResult sendProfileKey(RecipientId recipientId) { + logger.debug("Sending updated profile key to recipient: {}", recipientId); + final var profileKey = account.getProfileKey().serialize(); + final var message = SignalServiceDataMessage.newBuilder() + .asProfileKeyUpdate(true) + .withProfileKey(profileKey) + .build(); + return handleSendMessage(recipientId, + (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address, + unidentifiedAccess, + ContentHint.IMPLICIT, + message, + SignalServiceMessageSender.IndividualSendEvents.EMPTY)); + } + public SendMessageResult sendRetryReceipt( DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional groupId ) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index de28638e..469ca02e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -229,6 +229,7 @@ public class StorageHelper { context.getProfileHelper() .setProfile(false, + false, accountRecord.getGivenName().orElse(null), accountRecord.getFamilyName().orElse(null), null, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 6b2d063b..ac41d791 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -373,7 +373,7 @@ public class SignalAccount implements Closeable { setProfileKey(KeyUtils.createProfileKey()); } // Ensure our profile key is stored in profile store - getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey()); + getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey()); if (previousStorageVersion < 3) { for (final var group : groupStore.getGroups()) { if (group instanceof GroupInfoV2 && group.getDistributionId() == null) { @@ -1266,6 +1266,7 @@ public class SignalAccount implements Closeable { } this.profileKey = profileKey; save(); + getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey()); } public byte[] getSelfUnidentifiedAccessKey() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index 0ff20042..df65db0d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -15,6 +15,8 @@ public interface ProfileStore { void storeProfile(RecipientId recipientId, Profile profile); + void storeSelfProfileKey(RecipientId recipientId, ProfileKey profileKey); + void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 16ec9bed..299a3980 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -325,8 +325,17 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile } } + @Override + public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) { + storeProfileKey(recipientId, profileKey, false); + } + @Override public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) { + storeProfileKey(recipientId, profileKey, true); + } + + private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) { synchronized (recipients) { final var recipient = recipients.get(recipientId); if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && ( @@ -339,13 +348,15 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile return; } - final var newRecipient = Recipient.newBuilder(recipient) + final var builder = Recipient.newBuilder(recipient) .withProfileKey(profileKey) - .withProfileKeyCredential(null) - .withProfile(recipient.getProfile() == null - ? null - : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build()) - .build(); + .withProfileKeyCredential(null); + if (resetProfile) { + builder.withProfile(recipient.getProfile() == null + ? null + : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build()); + } + final var newRecipient = builder.build(); storeRecipientLocked(recipientId, newRecipient); } } -- 2.51.0 From be28d13d0dfb46df57b8406d9c33651bce18eee9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 10:32:57 +0200 Subject: [PATCH 05/16] Update libsignal-service-java --- build.gradle.kts | 3 +-- lib/build.gradle.kts | 4 ++-- .../java/org/asamk/signal/manager/config/LiveConfig.java | 6 +++--- .../java/org/asamk/signal/manager/config/StagingConfig.java | 6 +++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d0ef32f6..926ae182 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,7 +21,6 @@ graalvmNative { binaries { this["main"].run { configurationFileDirectories.from(file("graalvm-config-dir")) - buildArgs.add("--allow-incomplete-classpath") buildArgs.add("--report-unsupported-elements-at-runtime") } } @@ -34,7 +33,7 @@ repositories { dependencies { implementation("org.bouncycastle", "bcprov-jdk15on", "1.70") - implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.2.2") + implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.3") implementation("net.sourceforge.argparse4j", "argparse4j", "0.9.0") implementation("com.github.hypfvieh", "dbus-java-transport-native-unixsocket", "4.0.0") implementation("org.slf4j", "slf4j-api", "1.7.36") diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index e1590dc2..cad9ca8b 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,8 +14,8 @@ repositories { } dependencies { - implementation("com.github.turasa", "signal-service-java", "2.15.3_unofficial_48") - implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.2.2") + implementation("com.github.turasa", "signal-service-java", "2.15.3_unofficial_49") + implementation("com.fasterxml.jackson.core", "jackson-databind", "2.13.3") implementation("com.google.protobuf", "protobuf-javalite", "3.11.4") implementation("org.bouncycastle", "bcprov-jdk15on", "1.70") implementation("org.slf4j", "slf4j-api", "1.7.36") diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 9c15bc71..98e1f026 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -6,7 +6,7 @@ import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -39,7 +39,7 @@ class LiveConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; private final static String STORAGE_URL = "https://storage.signal.org"; - private final static String SIGNAL_CDSH_URL = ""; + private final static String SIGNAL_CDSI_URL = ""; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.empty(); @@ -60,7 +60,7 @@ class LiveConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, - new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, + new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, interceptors, dns, proxy, diff --git a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java index bba5b333..8faec0eb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/StagingConfig.java @@ -6,7 +6,7 @@ import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -39,7 +39,7 @@ class StagingConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org"; private final static String STORAGE_URL = "https://storage-staging.signal.org"; - private final static String SIGNAL_CDSH_URL = "https://cdsh.staging.signal.org"; + private final static String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.empty(); @@ -60,7 +60,7 @@ class StagingConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, - new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, + new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, interceptors, dns, proxy, -- 2.51.0 From 06e281101223662c6ad4e1243a3d997b26cb8bc4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 12:23:35 +0200 Subject: [PATCH 06/16] Only update profile keys from authoritative group changes --- .../signal/manager/helper/GroupHelper.java | 82 ++++++++++++++++++- .../signal/manager/helper/GroupV2Helper.java | 74 +++++++++++++++-- 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index ab3e1264..9346372c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -31,9 +31,11 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; @@ -50,8 +52,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -123,12 +127,22 @@ public class GroupHelper { if (signedGroupChange != null && groupInfoV2.getGroup() != null && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = context.getGroupV2Helper() - .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey); + final var decryptedGroupChange = context.getGroupV2Helper() + .getDecryptedGroupChange(signedGroupChange, groupMasterKey); + + if (decryptedGroupChange != null) { + storeProfileKeyFromChange(decryptedGroupChange); + group = context.getGroupV2Helper() + .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), decryptedGroupChange); + } } if (group == null) { try { group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams); + + if (group != null) { + storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group); + } } catch (NotAGroupMemberException ignored) { } } @@ -373,6 +387,17 @@ public class GroupHelper { groupInfoV2.setPermissionDenied(true); decryptedGroup = null; } + if (decryptedGroup != null) { + try { + storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, decryptedGroup); + } catch (NotAGroupMemberException ignored) { + } + storeProfileKeysFromMembers(decryptedGroup); + final var avatar = decryptedGroup.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar); + } + } groupInfoV2.setGroup(decryptedGroup, account.getRecipientStore()); account.getGroupStore().updateGroup(group); } @@ -417,14 +442,63 @@ public class GroupHelper { for (var member : group.getMembersList()) { final var serviceId = ServiceId.fromByteString(member.getUuid()); final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + final var profileStore = account.getProfileStore(); + if (profileStore.getProfileKey(recipientId) != null) { + // We already have a profile key, not updating it from a non-authoritative source + continue; + } try { - account.getProfileStore() - .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); + profileStore.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); } catch (InvalidInputException ignored) { } } } + private void storeProfileKeyFromChange(final DecryptedGroupChange decryptedGroupChange) { + final var profileKeyFromChange = context.getGroupV2Helper() + .getAuthoritativeProfileKeyFromChange(decryptedGroupChange); + + if (profileKeyFromChange != null) { + final var serviceId = profileKeyFromChange.first(); + final var profileKey = profileKeyFromChange.second(); + final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + account.getProfileStore().storeProfileKey(recipientId, profileKey); + } + } + + private void storeProfileKeysFromHistory( + final GroupSecretParams groupSecretParams, + final GroupInfoV2 localGroup, + final DecryptedGroup newDecryptedGroup + ) throws NotAGroupMemberException { + final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup); + final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().getRevision(); + var fromRevision = Math.max(revisionWeWereAdded, localRevision); + final var newProfileKeys = new HashMap(); + while (true) { + final var page = context.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams, fromRevision); + page.getResults() + .stream() + .map(DecryptedGroupHistoryEntry::getChange) + .filter(Optional::isPresent) + .map(Optional::get) + .map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange) + .filter(Objects::nonNull) + .forEach(p -> { + final var serviceId = p.first(); + final var profileKey = p.second(); + final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + newProfileKeys.put(recipientId, profileKey); + }); + if (!page.getPagingData().hasMorePages()) { + break; + } + fromRevision = page.getPagingData().getNextPageRevision(); + } + + newProfileKeys.forEach(account.getProfileStore()::storeProfileKey); + } + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { var g = getGroup(groupId); if (g == null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 385acc59..9b934580 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager.helper; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.SignalDependencies; @@ -19,6 +20,7 @@ import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.groups.UuidCiphertext; +import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; @@ -27,10 +29,12 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; +import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; +import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; @@ -40,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.File; import java.io.FileInputStream; @@ -53,7 +58,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; class GroupV2Helper { @@ -96,6 +103,35 @@ class GroupV2Helper { getGroupAuthForToday(groupSecretParams)); } + GroupHistoryPage getDecryptedGroupHistoryPage( + final GroupSecretParams groupSecretParams, int fromRevision + ) throws NotAGroupMemberException { + try { + final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); + return dependencies.getGroupsV2Api() + .getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 403) { + throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); + } + logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); + return null; + } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { + logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); + return null; + } + } + + int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) { + ByteString bytes = UuidUtil.toByteString(getSelfAci().uuid()); + for (DecryptedMember decryptedMember : partialDecryptedGroup.getMembersList()) { + if (decryptedMember.getUuid().equals(bytes)) { + return decryptedMember.getJoinedAtRevision(); + } + } + return partialDecryptedGroup.getRevision(); + } + Pair createGroup( String name, Set members, File avatarFile ) throws IOException { @@ -522,21 +558,43 @@ class GroupV2Helper { Optional.ofNullable(password).map(GroupLinkPassword::serialize)); } - DecryptedGroup getUpdatedDecryptedGroup( - DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey - ) { + Pair getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) { + UUID editor = UuidUtil.fromByteStringOrNull(change.getEditor()); + final var editorProfileKeyBytes = Stream.concat(Stream.of(change.getNewMembersList().stream(), + change.getPromotePendingMembersList().stream(), + change.getModifiedProfileKeysList().stream()) + .flatMap(Function.identity()) + .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor)) + .map(DecryptedMember::getProfileKey), + change.getNewRequestingMembersList() + .stream() + .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor)) + .map(DecryptedRequestingMember::getProfileKey)).findFirst(); + + if (editorProfileKeyBytes.isEmpty()) { + return null; + } + + ProfileKey profileKey; + try { + profileKey = new ProfileKey(editorProfileKeyBytes.get().toByteArray()); + } catch (InvalidInputException e) { + logger.debug("Bad profile key in group"); + return null; + } + + return new Pair<>(ServiceId.from(editor), profileKey); + } + + DecryptedGroup getUpdatedDecryptedGroup(DecryptedGroup group, DecryptedGroupChange decryptedGroupChange) { try { - final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey); - if (decryptedGroupChange == null) { - return null; - } return DecryptedGroupUtil.apply(group, decryptedGroupChange); } catch (NotAbleToApplyGroupV2ChangeException e) { return null; } } - private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { + DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { if (signedGroupChange != null) { var groupOperations = dependencies.getGroupsV2Operations() .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); -- 2.51.0 From 470aeadbd93e4607312037f555e6004cb7d76402 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 13:06:57 +0200 Subject: [PATCH 07/16] Bump version --- CHANGELOG.md | 17 +++++++++++++++++ build.gradle.kts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de705aed..f88e83f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## [Unreleased] +## [0.10.6] - 2022-05-19 +**Attention**: Now requires native libsignal-client version 0.17 + +### Added +- Check if account is used on the environment it was registered (live or staging) +- New command `deleteLocalAccountData` to delete all local data of an unregistered account +- New parameter `-g` for `listGroups` command to filter for specific groups + +### Fixed +- Fix deleting a recipient which has no uuid + +### Changed +- Show warning when sending a message and no profile name has been set. + (A profile name may become mandatory in the future) +- After blocking a contact/group the profile key is now rotated +- Only update profile keys from authoritative group changes + ## [0.10.5] - 2022-04-11 **Attention**: Now requires native libsignal-client version 0.15 diff --git a/build.gradle.kts b/build.gradle.kts index 926ae182..22cc27f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("org.graalvm.buildtools.native") version "0.9.11" } -version = "0.10.5" +version = "0.10.6" java { sourceCompatibility = JavaVersion.VERSION_17 -- 2.51.0 From 496cd5e6219b54e01d0a3b175cb77457001ce7dc Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 13:48:34 +0200 Subject: [PATCH 08/16] Fix repackage if building with multiple java versions --- .github/workflows/repackage-native-libs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repackage-native-libs.yml b/.github/workflows/repackage-native-libs.yml index 9a87f723..fc5fa780 100644 --- a/.github/workflows/repackage-native-libs.yml +++ b/.github/workflows/repackage-native-libs.yml @@ -31,7 +31,7 @@ jobs: run: | #echo ${GITHUB_REF#refs/tag/} tree . - mv ./*/*.tar.gz . + mv ./$(ls */ -d | tail -n1)/*.tar.gz . ver=$(ls ./*.tar.gz | xargs basename | sed -E 's/signal-cli-(.*).tar.gz/\1/') echo $ver echo "::set-output name=signal_cli_version::${ver}" -- 2.51.0 From 5f941004f5006b286690df2a93bc47ace3b59270 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 May 2022 11:46:03 +0200 Subject: [PATCH 09/16] Extend listContacts command with profiles and filtering --- graalvm-config-dir/reflect-config.json | 25 ++++++ graalvm-config-dir/resource-config.json | 3 + .../org/asamk/signal/manager/Manager.java | 10 ++- .../org/asamk/signal/manager/ManagerImpl.java | 53 +++++++------ .../signal/manager/helper/ProfileHelper.java | 16 +++- .../manager/helper/RecipientHelper.java | 4 +- .../signal/manager/helper/SendHelper.java | 2 +- .../storage/recipients/RecipientStore.java | 18 +++++ .../signal/commands/ListContactsCommand.java | 79 ++++++++++++++++--- .../asamk/signal/dbus/DbusManagerImpl.java | 29 +++++-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 38 ++------- 11 files changed, 195 insertions(+), 82 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index f9fe367f..69b19703 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -527,6 +527,20 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact$JsonProfile", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[ + {"name":"about","parameterTypes":[] }, + {"name":"aboutEmoji","parameterTypes":[] }, + {"name":"familyName","parameterTypes":[] }, + {"name":"givenName","parameterTypes":[] }, + {"name":"lastUpdateTimestamp","parameterTypes":[] }, + {"name":"paymentAddress","parameterTypes":[] } + ] +}, { "name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice", "allDeclaredFields":true, @@ -1797,6 +1811,17 @@ {"name":"userId_"} ] }, +{ + "name":"org.signal.storageservice.protos.groups.GroupChanges", + "fields":[{"name":"groupChanges_"}] +}, +{ + "name":"org.signal.storageservice.protos.groups.GroupChanges$GroupChangeState", + "fields":[ + {"name":"groupChange_"}, + {"name":"groupState_"} + ] +}, { "name":"org.signal.storageservice.protos.groups.GroupInviteLink", "fields":[ diff --git a/graalvm-config-dir/resource-config.json b/graalvm-config-dir/resource-config.json index 08a9dcc5..542c8f31 100644 --- a/graalvm-config-dir/resource-config.json +++ b/graalvm-config-dir/resource-config.json @@ -73,6 +73,9 @@ { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E" }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E" + }, { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E" }, diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 8f49126f..cc3ce0ce 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -28,9 +28,8 @@ import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.Recipient; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.Closeable; @@ -215,7 +214,12 @@ public interface Manager extends Closeable { void sendContacts() throws IOException; - List> getContacts(); + List getRecipients( + boolean onlyContacts, + Optional blocked, + Collection address, + Optional name + ); String getContactOrProfileName(RecipientIdentifier.Single recipient); diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index b3e6a0ae..f2d080fd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -51,9 +51,8 @@ import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; -import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.Recipient; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack; import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore; @@ -84,6 +83,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -101,7 +101,6 @@ class ManagerImpl implements Manager { private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); private SignalAccount account; - private AccountFileUpdater accountFileUpdater; private final SignalDependencies dependencies; private final Context context; @@ -123,7 +122,6 @@ class ManagerImpl implements Manager { String userAgent ) { this.account = account; - this.accountFileUpdater = accountFileUpdater; final var sessionLock = new SignalSessionLock() { private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); @@ -337,7 +335,7 @@ class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient)); } @@ -495,7 +493,7 @@ class ManagerImpl implements Manager { @Override public SendMessageResults sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException { + ) { final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, @@ -507,7 +505,7 @@ class ManagerImpl implements Manager { @Override public SendMessageResults sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException { + ) { final var timestamp = System.currentTimeMillis(); var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, @@ -520,7 +518,7 @@ class ManagerImpl implements Manager { final RecipientIdentifier.Single sender, final long timestamp, final SignalServiceReceiptMessage receiptMessage - ) throws IOException { + ) { try { final var result = context.getSendHelper() .sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender)); @@ -592,7 +590,7 @@ class ManagerImpl implements Manager { } } - private ArrayList resolveMentions(final List mentionList) throws IOException, UnregisteredRecipientException { + private ArrayList resolveMentions(final List mentionList) throws UnregisteredRecipientException { final var mentions = new ArrayList(); for (final var m : mentionList) { final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient()); @@ -676,7 +674,7 @@ class ManagerImpl implements Manager { @Override public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException { + ) throws NotMasterDeviceException, UnregisteredRecipientException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } @@ -932,7 +930,7 @@ class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException | UnregisteredRecipientException e) { + } catch (UnregisteredRecipientException e) { return false; } return context.getContactHelper().isContactBlocked(recipientId); @@ -944,12 +942,22 @@ class ManagerImpl implements Manager { } @Override - public List> getContacts() { - return account.getContactStore() - .getContacts() - .stream() - .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second())) - .toList(); + public List getRecipients( + boolean onlyContacts, + Optional blocked, + Collection recipients, + Optional name + ) { + final var recipientIds = recipients.stream().map(a -> { + try { + return context.getRecipientHelper().resolveRecipient(a); + } catch (UnregisteredRecipientException e) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toSet()); + // refresh profiles of explicitly given recipients + context.getProfileHelper().refreshRecipientProfiles(recipientIds); + return account.getRecipientStore().getRecipients(onlyContacts, blocked, recipientIds, name); } @Override @@ -957,7 +965,7 @@ class ManagerImpl implements Manager { final RecipientId recipientId; try { recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException | UnregisteredRecipientException e) { + } catch (UnregisteredRecipientException e) { return null; } @@ -1007,7 +1015,7 @@ class ManagerImpl implements Manager { try { identity = account.getIdentityKeyStore() .getIdentity(context.getRecipientHelper().resolveRecipient(recipient)); - } catch (IOException | UnregisteredRecipientException e) { + } catch (UnregisteredRecipientException e) { identity = null; } return identity == null ? List.of() : List.of(toIdentity(identity)); @@ -1044,12 +1052,7 @@ class ManagerImpl implements Manager { private boolean trustIdentity( RecipientIdentifier.Single recipient, Function trustMethod ) throws UnregisteredRecipientException { - RecipientId recipientId; - try { - recipientId = context.getRecipientHelper().resolveRecipient(recipient); - } catch (IOException e) { - return false; - } + final var recipientId = context.getRecipientHelper().resolveRecipient(recipient); final var updated = trustMethod.apply(recipientId); if (updated && this.isReceiving()) { context.getReceiveHelper().setNeedsToRetryFailedMessages(true); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 4bc00317..8a64c8b7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.util.Base64; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; @@ -94,10 +95,18 @@ public final class ProfileHelper { return getRecipientProfile(recipientId, false); } + public List getRecipientProfiles(Collection recipientIds) { + return getRecipientProfiles(recipientIds, false); + } + public void refreshRecipientProfile(RecipientId recipientId) { getRecipientProfile(recipientId, true); } + public void refreshRecipientProfiles(Collection recipientIds) { + getRecipientProfiles(recipientIds, true); + } + public List getRecipientProfileKeyCredential(List recipientIds) { try { account.getRecipientStore().setBulkUpdating(true); @@ -216,11 +225,12 @@ public final class ProfileHelper { return getRecipientProfile(account.getSelfRecipientId()); } - public List getRecipientProfile(List recipientIds) { + private List getRecipientProfiles(Collection recipientIds, boolean force) { + final var profileStore = account.getProfileStore(); try { account.getRecipientStore().setBulkUpdating(true); final var profileFetches = Flowable.fromIterable(recipientIds) - .filter(recipientId -> isProfileRefreshRequired(account.getProfileStore().getProfile(recipientId))) + .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId))) .map(recipientId -> retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE).onErrorComplete()); Maybe.merge(profileFetches, 10).blockingSubscribe(); @@ -228,7 +238,7 @@ public final class ProfileHelper { account.getRecipientStore().setBulkUpdating(false); } - return recipientIds.stream().map(r -> account.getProfileStore().getProfile(r)).toList(); + return recipientIds.stream().map(profileStore::getProfile).toList(); } private Profile getRecipientProfile(RecipientId recipientId, boolean force) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java index 15508cd4..c253d602 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java @@ -69,7 +69,7 @@ public class RecipientHelper { return account.getRecipientStore().resolveRecipient(address); } - public Set resolveRecipients(Collection recipients) throws IOException, UnregisteredRecipientException { + public Set resolveRecipients(Collection recipients) throws UnregisteredRecipientException { final var recipientIds = new HashSet(recipients.size()); for (var number : recipients) { final var recipientId = resolveRecipient(number); @@ -78,7 +78,7 @@ public class RecipientHelper { return recipientIds; } - public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException { if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) { return account.getRecipientStore().resolveRecipient(ServiceId.from(uuidRecipient.uuid())); } else { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index a2e2379b..8b2a054e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -475,7 +475,7 @@ public class SendHelper { final var senderKeyTargets = new HashSet(); final var recipientList = new ArrayList<>(recipientIds); - final var profiles = context.getProfileHelper().getRecipientProfile(recipientList).iterator(); + final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator(); for (final var recipientId : recipientList) { final var profile = profiles.next(); if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 299a3980..850b6270 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -277,6 +277,24 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile .toList(); } + public List getRecipients( + boolean onlyContacts, Optional blocked, Set recipientIds, Optional name + ) { + return recipients.values() + .stream() + .filter(r -> !onlyContacts || r.getContact() != null) + .filter(r -> blocked.isEmpty() || ( + blocked.get() == ( + r.getContact() != null && r.getContact().isBlocked() + ) + )) + .filter(r -> recipientIds.isEmpty() || (recipientIds.contains(r.getRecipientId()))) + .filter(r -> name.isEmpty() + || (r.getContact() != null && name.get().equals(r.getContact().getName())) + || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName()))) + .toList(); + } + @Override public void deleteContact(RecipientId recipientId) { synchronized (recipients) { diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index b83440ec..0bfc7ab9 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -1,13 +1,20 @@ package org.asamk.signal.commands; +import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.OutputWriter; import org.asamk.signal.output.PlainTextWriter; +import org.asamk.signal.util.CommandUtil; +import java.util.Base64; +import java.util.Optional; import java.util.UUID; public class ListContactsCommand implements JsonRpcLocalCommand { @@ -19,19 +26,39 @@ public class ListContactsCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { - subparser.help("Show a list of known contacts with names."); + subparser.help("Show a list of known contacts with names and profiles."); + subparser.addArgument("recipient").help("Specify one ore more phone numbers to show.").nargs("*"); + subparser.addArgument("-a", "--all-recipients") + .action(Arguments.storeTrue()) + .help("Include all known recipients, not only contacts."); + subparser.addArgument("--blocked") + .type(Boolean.class) + .help("Specify if only blocked or unblocked contacts should be shown (default: all contacts)"); + subparser.addArgument("--name").help("Find contacts with the given contact or profile name."); } @Override - public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) { - var contacts = m.getContacts(); + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var allRecipients = Boolean.TRUE.equals(ns.getBoolean("all-recipients")); + final var blocked = ns.getBoolean("blocked"); + final var recipientStrings = ns.getList("recipient"); + final var recipientIdentifiers = CommandUtil.getSingleRecipientIdentifiers(recipientStrings, m.getSelfNumber()); + final var name = ns.getString("name"); + final var recipients = m.getRecipients(!allRecipients, + Optional.ofNullable(blocked), + recipientIdentifiers, + Optional.ofNullable(name)); if (outputWriter instanceof PlainTextWriter writer) { - for (var c : contacts) { - final var contact = c.second(); - writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}", - c.first().getLegacyIdentifier(), + for (var r : recipients) { + final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact(); + final var profile = r.getProfile() == null ? Profile.newBuilder().build() : r.getProfile(); + writer.println("Number: {} Name: {} Profile name: {} Blocked: {} Message expiration: {}", + r.getAddress().getLegacyIdentifier(), contact.getName(), + profile.getDisplayName(), contact.isBlocked(), contact.getMessageExpirationTime() == 0 ? "disabled" @@ -39,19 +66,47 @@ public class ListContactsCommand implements JsonRpcLocalCommand { } } else { final var writer = (JsonWriter) outputWriter; - final var jsonContacts = contacts.stream().map(contactPair -> { - final var address = contactPair.first(); - final var contact = contactPair.second(); + final var jsonContacts = recipients.stream().map(r -> { + final var address = r.getAddress(); + final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact(); return new JsonContact(address.number().orElse(null), address.uuid().map(UUID::toString).orElse(null), contact.getName(), contact.isBlocked(), - contact.getMessageExpirationTime()); + contact.getMessageExpirationTime(), + r.getProfile() == null + ? null + : new JsonContact.JsonProfile(r.getProfile().getLastUpdateTimestamp(), + r.getProfile().getGivenName(), + r.getProfile().getFamilyName(), + r.getProfile().getAbout(), + r.getProfile().getAboutEmoji(), + r.getProfile().getPaymentAddress() == null + ? null + : Base64.getEncoder() + .encodeToString(r.getProfile().getPaymentAddress()))); }).toList(); writer.write(jsonContacts); } } - private record JsonContact(String number, String uuid, String name, boolean isBlocked, int messageExpirationTime) {} + private record JsonContact( + String number, + String uuid, + String name, + boolean isBlocked, + int messageExpirationTime, + JsonProfile profile + ) { + + private record JsonProfile( + long lastUpdateTimestamp, + String givenName, + String familyName, + String about, + String aboutEmoji, + String paymentAddress + ) {} + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 5b33b039..11800be2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -32,6 +32,7 @@ import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.Recipient; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.freedesktop.dbus.DBusMap; import org.freedesktop.dbus.DBusPath; @@ -547,14 +548,32 @@ public class DbusManagerImpl implements Manager { } @Override - public List> getContacts() { - return signal.listNumbers().stream().map(n -> { + public List getRecipients( + final boolean onlyContacts, + final Optional blocked, + final Collection addresses, + final Optional name + ) { + final var numbers = addresses.stream() + .filter(s -> s instanceof RecipientIdentifier.Number) + .map(s -> ((RecipientIdentifier.Number) s).number()) + .collect(Collectors.toSet()); + return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> { + final var contactBlocked = signal.isContactBlocked(n); + if (blocked.isPresent() && blocked.get() != contactBlocked) { + return null; + } final var contactName = signal.getContactName(n); - if (contactName.length() == 0) { + if (onlyContacts && contactName.length() == 0) { + return null; + } + if (name.isPresent() && !name.get().equals(contactName)) { return null; } - return new Pair<>(new RecipientAddress(null, n), - new Contact(contactName, null, 0, signal.isContactBlocked(n), false, false)); + return Recipient.newBuilder() + .withAddress(new RecipientAddress(null, n)) + .withContact(new Contact(contactName, null, 0, contactBlocked, false, false)) + .build(); }).filter(Objects::nonNull).toList(); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index b4485e32..f4a9cda8 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -4,14 +4,12 @@ import org.asamk.Signal; import org.asamk.signal.BaseConfig; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.AttachmentInvalidException; -import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.InvalidNumberException; import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.NotMasterDeviceException; -import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendMessageResult; import org.asamk.signal.manager.api.SendMessageResults; @@ -28,7 +26,6 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.SendMessageResultUtils; import org.freedesktop.dbus.DBusPath; @@ -55,7 +52,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; public class DbusSignalImpl implements Signal { @@ -719,9 +715,9 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities().stream().map(Identity::recipient), - m.getContacts().stream().map(Pair::first)) - .map(a -> a.number().orElse(null)) + return m.getRecipients(false, Optional.empty(), Set.of(), Optional.empty()) + .stream() + .map(r -> r.getAddress().number().orElse(null)) .filter(Objects::nonNull) .distinct() .toList(); @@ -729,30 +725,10 @@ public class DbusSignalImpl implements Signal { @Override public List getContactNumber(final String name) { - // Contact names have precedence. - var numbers = new ArrayList(); - var contacts = m.getContacts(); - for (var c : contacts) { - if (name.equals(c.second().getName())) { - numbers.add(c.first().getLegacyIdentifier()); - } - } - // Try profiles if no contact name was found - for (var identity : m.getIdentities()) { - final var address = identity.recipient(); - var number = address.number().orElse(null); - if (number != null) { - Profile profile = null; - try { - profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address)); - } catch (IOException | UnregisteredRecipientException ignored) { - } - if (profile != null && profile.getDisplayName().equals(name)) { - numbers.add(number); - } - } - } - return numbers; + return m.getRecipients(false, Optional.empty(), Set.of(), Optional.of(name)) + .stream() + .map(r -> r.getAddress().getLegacyIdentifier()) + .toList(); } @Override -- 2.51.0 From 2ecddba37509c1328980a2d59d1e96ca2cabab53 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 May 2022 11:57:18 +0200 Subject: [PATCH 10/16] Update json-rpc client --- client/Cargo.lock | 218 +++++++++++++++++++++++++----------------- client/src/cli.rs | 22 ++++- client/src/jsonrpc.rs | 22 ++++- client/src/main.rs | 21 +++- 4 files changed, 188 insertions(+), 95 deletions(-) diff --git a/client/Cargo.lock b/client/Cargo.lock index 77afe1dd..855d74bc 100644 --- a/client/Cargo.lock +++ b/client/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.54" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a99269dff3bc004caa411f38845c20303f1e393ca2bd6581576fa3a7f59577d" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "atty" @@ -63,16 +63,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "3.1.0" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f1fea81f183005ced9e59cdb01737ef2423956dac5a6d731b06b2ecfaa3467" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" dependencies = [ "atty", "bitflags", "clap_derive", + "clap_lex", "indexmap", "lazy_static", - "os_str_bytes", "strsim", "termcolor", "textwrap", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.0" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd1122e63869df2cb309f449da1ad54a7c6dfeb7c7e6ccd8e0825d9eb93bb72" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" dependencies = [ "heck", "proc-macro-error", @@ -91,6 +91,15 @@ dependencies = [ "syn", ] +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -221,7 +230,7 @@ checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -260,9 +269,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", "hashbrown", @@ -279,9 +288,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jsonrpc-client-transports" @@ -407,63 +416,45 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.119" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mio" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] @@ -476,14 +467,17 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca" + [[package]] name = "os_str_bytes" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" [[package]] name = "parity-tokio-ipc" @@ -526,9 +520,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -577,18 +571,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -636,18 +630,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -671,9 +665,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "scopeguard" @@ -683,24 +677,24 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -709,9 +703,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "itoa", "ryu", @@ -737,9 +731,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" @@ -765,41 +759,42 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" -version = "0.14.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tokio" -version = "1.17.0" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ "bytes", "libc", "memchr", "mio", "num_cpus", + "once_cell", "pin-project-lite", "socket2", "tokio-macros", @@ -830,9 +825,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -844,9 +839,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -861,10 +856,10 @@ dependencies = [ ] [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-ident" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "version_check" @@ -878,6 +873,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -908,3 +909,46 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/client/src/cli.rs b/client/src/cli.rs index a8cee165..01b59fed 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -49,6 +49,10 @@ pub enum CliCommands { #[clap(short = 'g', long)] group_id: Vec, }, + DeleteLocalAccountData { + #[clap(long = "ignore-registered")] + ignore_registered: Option, + }, GetUserStatus { recipient: Vec, }, @@ -61,11 +65,21 @@ pub enum CliCommands { name: String, }, ListAccounts, - ListContacts, + ListContacts { + recipient: Vec, + #[clap(short = 'a', long = "all-recipients")] + all_recipients: bool, + #[clap(long, parse(try_from_str))] + blocked: Option, + #[clap(long)] + name: Option, + }, ListDevices, ListGroups { #[clap(short = 'd', long)] detailed: bool, + #[clap(short = 'g', long = "group-id")] + group_id: Vec, }, ListIdentities { #[clap(short = 'n', long)] @@ -225,13 +239,13 @@ pub enum CliCommands { #[clap(long = "read-receipts", parse(try_from_str))] read_receipts: Option, - #[clap(long = "unidentified-delivery-indicators")] + #[clap(long = "unidentified-delivery-indicators", parse(try_from_str))] unidentified_delivery_indicators: Option, - #[clap(long = "typing-indicators")] + #[clap(long = "typing-indicators", parse(try_from_str))] typing_indicators: Option, - #[clap(long = "link-previews")] + #[clap(long = "link-previews", parse(try_from_str))] link_previews: Option, }, UpdateContact { diff --git a/client/src/jsonrpc.rs b/client/src/jsonrpc.rs index d4ed4084..f91be25e 100644 --- a/client/src/jsonrpc.rs +++ b/client/src/jsonrpc.rs @@ -20,6 +20,13 @@ pub trait Rpc { #[allow(non_snake_case)] groupIds: Vec, ) -> Result; + #[rpc(name = "deleteLocalAccountData", params = "named")] + fn delete_local_account_data( + &self, + account: Option, + #[allow(non_snake_case)] ignoreRegistered: Option, + ) -> Result; + #[rpc(name = "getUserStatus", params = "named")] fn get_user_status(&self, account: Option, recipients: Vec) -> Result; @@ -37,13 +44,24 @@ pub trait Rpc { fn list_accounts(&self) -> Result; #[rpc(name = "listContacts", params = "named")] - fn list_contacts(&self, account: Option) -> Result; + fn list_contacts( + &self, + account: Option, + recipients: Vec, + #[allow(non_snake_case)] allRecipients: bool, + blocked: Option, + name: Option, + ) -> Result; #[rpc(name = "listDevices", params = "named")] fn list_devices(&self, account: Option) -> Result; #[rpc(name = "listGroups", params = "named")] - fn list_groups(&self, account: Option) -> Result; + fn list_groups( + &self, + account: Option, + #[allow(non_snake_case)] groupIds: Vec, + ) -> Result; #[rpc(name = "listIdentities", params = "named")] fn list_identities(&self, account: Option, number: Option) -> Result; diff --git a/client/src/main.rs b/client/src/main.rs index 6266d224..622be806 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -41,6 +41,11 @@ async fn main() -> Result<(), anyhow::Error> { recipient, group_id, } => client.block(cli.account, recipient, group_id).await, + cli::CliCommands::DeleteLocalAccountData { ignore_registered } => { + client + .delete_local_account_data(cli.account, ignore_registered) + .await + } cli::CliCommands::GetUserStatus { recipient } => { client.get_user_status(cli.account, recipient).await } @@ -55,9 +60,21 @@ async fn main() -> Result<(), anyhow::Error> { client.finish_link(url, name).await } cli::CliCommands::ListAccounts => client.list_accounts().await, - cli::CliCommands::ListContacts => client.list_contacts(cli.account).await, + cli::CliCommands::ListContacts { + recipient, + all_recipients, + blocked, + name, + } => { + client + .list_contacts(cli.account, recipient, all_recipients, blocked, name) + .await + } cli::CliCommands::ListDevices => client.list_devices(cli.account).await, - cli::CliCommands::ListGroups { detailed: _ } => client.list_groups(cli.account).await, + cli::CliCommands::ListGroups { + detailed: _, + group_id, + } => client.list_groups(cli.account, group_id).await, cli::CliCommands::ListIdentities { number } => { client.list_identities(cli.account, number).await } -- 2.51.0 From b18991b9fb3a68b8a3f4bd16a160ead90b4990a9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 May 2022 09:00:42 +0200 Subject: [PATCH 11/16] Update documentation --- graalvm-config-dir/resource-config.json | 9 +++++++++ man/signal-cli.1.adoc | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/graalvm-config-dir/resource-config.json b/graalvm-config-dir/resource-config.json index 542c8f31..1de22f76 100644 --- a/graalvm-config-dir/resource-config.json +++ b/graalvm-config-dir/resource-config.json @@ -46,6 +46,9 @@ { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BM\\E" }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BO\\E" + }, { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BR\\E" }, @@ -67,6 +70,12 @@ { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CN\\E" }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CO\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CR\\E" + }, { "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E" }, diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 4fdd684a..7d1aa6f1 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -418,7 +418,20 @@ Filter the group list by one or more group IDs. === listContacts -Show a list of known contacts with names. +Show a list of known contacts with names and profiles. +When a specific recipient is given, its profile will be refreshed. + +RECIPIENT:: +Specify the recipients’ phone number. + +*-a*, *--all-recipients*:: +Include all known recipients, not only contacts. + +*--blocked*:: +Specify if only blocked or unblocked contacts should be shown (default: all contacts) + +*--name*:: +Find contacts with the given contact or profile name. === listIdentities -- 2.51.0 From 7587a603872a337dd6be706854a0658ea131dbbe Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 20:54:15 +0200 Subject: [PATCH 12/16] Implement sendPayment notification command --- .../org/asamk/signal/manager/Manager.java | 4 ++ .../org/asamk/signal/manager/ManagerImpl.java | 14 +++++ man/signal-cli.1.adoc | 13 +++++ .../org/asamk/signal/commands/Commands.java | 1 + .../SendPaymentNotificationCommand.java | 51 +++++++++++++++++++ .../asamk/signal/dbus/DbusManagerImpl.java | 7 +++ 6 files changed, 90 insertions(+) create mode 100644 src/main/java/org/asamk/signal/commands/SendPaymentNotificationCommand.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cc3ce0ce..e568816f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -141,6 +141,10 @@ public interface Manager extends Closeable { Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; + SendMessageResults sendPaymentNotificationMessage( + byte[] receipt, String note, RecipientIdentifier.Single recipient + ) throws IOException; + SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; void deleteRecipient(RecipientIdentifier.Single recipient); diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index f2d080fd..ed7ae86b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -638,6 +638,20 @@ class ManagerImpl implements Manager { return sendMessage(messageBuilder, recipients); } + @Override + public SendMessageResults sendPaymentNotificationMessage( + byte[] receipt, String note, RecipientIdentifier.Single recipient + ) throws IOException { + final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note); + final var payment = new SignalServiceDataMessage.Payment(paymentNotification); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withPayment(payment); + try { + return sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } + } + @Override public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 7d1aa6f1..1cebd044 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -256,6 +256,19 @@ Specify the mentions of the original message (same format as `--mention`). *-e*, *--end-session*:: Clear session state and send end session message. +=== sendPaymentNotification + +Send a payment notification. + +RECIPIENT:: +Specify the recipient’s phone number. + +*--receipt* RECEIPT:: +The base64 encoded receipt blob. + +*--note* NOTE:: +Specify a note for the payment notification. + === sendReaction Send reaction to a previously received or sent message. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 15ab1724..1c4251da 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -34,6 +34,7 @@ public class Commands { addCommand(new RemoteDeleteCommand()); addCommand(new SendCommand()); addCommand(new SendContactsCommand()); + addCommand(new SendPaymentNotificationCommand()); addCommand(new SendReactionCommand()); addCommand(new SendReceiptCommand()); addCommand(new SendSyncRequestCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendPaymentNotificationCommand.java b/src/main/java/org/asamk/signal/commands/SendPaymentNotificationCommand.java new file mode 100644 index 00000000..77181e3e --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendPaymentNotificationCommand.java @@ -0,0 +1,51 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.output.OutputWriter; +import org.asamk.signal.util.CommandUtil; + +import java.io.IOException; +import java.util.Base64; + +import static org.asamk.signal.util.SendMessageResultUtils.outputResult; + +public class SendPaymentNotificationCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "sendPaymentNotification"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send a payment notification."); + subparser.addArgument("recipient").help("Specify the recipient's phone number."); + subparser.addArgument("--receipt").required(true).help("The base64 encoded receipt blob."); + subparser.addArgument("--note").help("Specify a note for the payment notification."); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var recipientString = ns.getString("recipient"); + final var recipientIdentifier = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber()); + + final var receiptString = ns.getString("receipt"); + final var receipt = Base64.getDecoder().decode(receiptString); + final var note = ns.getString("note"); + + try { + final var results = m.sendPaymentNotificationMessage(receipt, note, recipientIdentifier); + outputResult(outputWriter, results); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")", e); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 11800be2..6bb93677 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -388,6 +388,13 @@ public class DbusManagerImpl implements Manager { groupId)); } + @Override + public SendMessageResults sendPaymentNotificationMessage( + final byte[] receipt, final String note, final RecipientIdentifier.Single recipient + ) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); -- 2.51.0 From 3666531f8bfe179bdaf5eaa20ffc2ed16e0fcf9b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 May 2022 09:29:58 +0200 Subject: [PATCH 13/16] Refactor manager update profile method --- graalvm-config-dir/reflect-config.json | 8 ++ .../org/asamk/signal/manager/Manager.java | 12 +- .../org/asamk/signal/manager/ManagerImpl.java | 14 ++- .../manager/RegistrationManagerImpl.java | 3 +- .../signal/manager/api/UpdateProfile.java | 108 ++++++++++++++++++ .../signal/commands/UpdateProfileCommand.java | 15 ++- .../asamk/signal/dbus/DbusManagerImpl.java | 21 ++-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 14 ++- 8 files changed, 160 insertions(+), 35 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 69b19703..f9deabc6 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -2754,6 +2754,14 @@ {"name":"stickerId_"} ] }, +{ + "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$StoryContext", + "fields":[ + {"name":"authorUuid_"}, + {"name":"bitField0_"}, + {"name":"sentTimestamp_"} + ] +}, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Envelope", "fields":[ diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index e568816f..4444e57a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; @@ -67,15 +68,10 @@ public interface Manager extends Closeable { void updateConfiguration(Configuration configuration) throws IOException, NotMasterDeviceException; /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), + * Update the user's profile. + * If a field is null, the previous value will be kept. */ - void setProfile( - String givenName, String familyName, String about, String aboutEmoji, Optional avatar - ) throws IOException; + void updateProfile(UpdateProfile updateProfile) throws IOException; void unregister() throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index ed7ae86b..3ca3c109 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -38,6 +38,7 @@ import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; @@ -261,10 +262,15 @@ class ManagerImpl implements Manager { } @Override - public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, Optional avatar - ) throws IOException { - context.getProfileHelper().setProfile(givenName, familyName, about, aboutEmoji, avatar); + public void updateProfile(UpdateProfile updateProfile) throws IOException { + context.getProfileHelper() + .setProfile(updateProfile.getGivenName(), + updateProfile.getFamilyName(), + updateProfile.getAbout(), + updateProfile.getAboutEmoji(), + updateProfile.isDeleteAvatar() + ? Optional.empty() + : updateProfile.getAvatar() == null ? null : Optional.of(updateProfile.getAvatar())); context.getSyncHelper().sendSyncFetchProfileMessage(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java index 3529154a..509b8923 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java @@ -19,6 +19,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.helper.AccountFileUpdater; @@ -139,7 +140,7 @@ class RegistrationManagerImpl implements RegistrationManager { } // Set an initial empty profile so user can be added to groups try { - m.setProfile(null, null, null, null, null); + m.updateProfile(UpdateProfile.newBuilder().build()); } catch (NoClassDefFoundError e) { logger.warn("Failed to set default profile: {}", e.getMessage()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java new file mode 100644 index 00000000..2b47f7d5 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java @@ -0,0 +1,108 @@ +package org.asamk.signal.manager.api; + +import java.io.File; + +public class UpdateProfile { + + private final String givenName; + private final String familyName; + private final String about; + private final String aboutEmoji; + private final File avatar; + private final boolean deleteAvatar; + + private UpdateProfile(final Builder builder) { + givenName = builder.givenName; + familyName = builder.familyName; + about = builder.about; + aboutEmoji = builder.aboutEmoji; + avatar = builder.avatar; + deleteAvatar = builder.deleteAvatar; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final UpdateProfile copy) { + Builder builder = new Builder(); + builder.givenName = copy.getGivenName(); + builder.familyName = copy.getFamilyName(); + builder.about = copy.getAbout(); + builder.aboutEmoji = copy.getAboutEmoji(); + builder.avatar = copy.getAvatar(); + builder.deleteAvatar = copy.isDeleteAvatar(); + return builder; + } + + public String getGivenName() { + return givenName; + } + + public String getFamilyName() { + return familyName; + } + + public String getAbout() { + return about; + } + + public String getAboutEmoji() { + return aboutEmoji; + } + + public File getAvatar() { + return avatar; + } + + public boolean isDeleteAvatar() { + return deleteAvatar; + } + + public static final class Builder { + + private String givenName; + private String familyName; + private String about; + private String aboutEmoji; + private File avatar; + private boolean deleteAvatar; + + 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 withAvatar(final File val) { + avatar = val; + return this; + } + + public Builder withDeleteAvatar(final boolean val) { + deleteAvatar = val; + return this; + } + + public UpdateProfile build() { + return new UpdateProfile(this); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index e60edf23..1edb6df9 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -7,11 +7,11 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.output.OutputWriter; import java.io.File; import java.io.IOException; -import java.util.Optional; public class UpdateProfileCommand implements JsonRpcLocalCommand { @@ -44,12 +44,17 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { var avatarPath = ns.getString("avatar"); boolean removeAvatar = Boolean.TRUE.equals(ns.getBoolean("remove-avatar")); - Optional avatarFile = removeAvatar - ? Optional.empty() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); + File avatarFile = removeAvatar || avatarPath == null ? null : new File(avatarPath); try { - m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); + m.updateProfile(UpdateProfile.newBuilder() + .withGivenName(givenName) + .withFamilyName(familyName) + .withAbout(about) + .withAboutEmoji(aboutEmoji) + .withAvatar(avatarFile) + .withDeleteAvatar(removeAvatar) + .build()); } catch (IOException e) { throw new IOErrorException("Update profile error: " + e.getMessage(), e); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 6bb93677..320a8533 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -22,6 +22,7 @@ import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; @@ -138,19 +139,13 @@ public class DbusManagerImpl implements Manager { } @Override - public void setProfile( - final String givenName, - final String familyName, - final String about, - final String aboutEmoji, - final Optional avatar - ) throws IOException { - signal.updateProfile(emptyIfNull(givenName), - emptyIfNull(familyName), - emptyIfNull(about), - emptyIfNull(aboutEmoji), - avatar == null ? "" : avatar.map(File::getPath).orElse(""), - avatar != null && avatar.isEmpty()); + public void updateProfile(UpdateProfile updateProfile) throws IOException { + signal.updateProfile(emptyIfNull(updateProfile.getGivenName()), + emptyIfNull(updateProfile.getFamilyName()), + emptyIfNull(updateProfile.getAbout()), + emptyIfNull(updateProfile.getAboutEmoji()), + updateProfile.getAvatar() == null ? "" : updateProfile.getAvatar().getPath(), + updateProfile.isDeleteAvatar()); } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index f4a9cda8..ec7174b2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; +import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UserStatus; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; @@ -662,10 +663,15 @@ public class DbusSignalImpl implements Signal { about = nullIfEmpty(about); aboutEmoji = nullIfEmpty(aboutEmoji); avatarPath = nullIfEmpty(avatarPath); - Optional avatarFile = removeAvatar - ? Optional.empty() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); - m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); + File avatarFile = removeAvatar || avatarPath == null ? null : new File(avatarPath); + m.updateProfile(UpdateProfile.newBuilder() + .withGivenName(givenName) + .withFamilyName(familyName) + .withAbout(about) + .withAboutEmoji(aboutEmoji) + .withAvatar(avatarFile) + .withDeleteAvatar(removeAvatar) + .build()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } -- 2.51.0 From bf75d9b4e0385d8b6b86faef8860caa06368c447 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 May 2022 10:08:40 +0200 Subject: [PATCH 14/16] Decrypt and verify the profile payment address --- graalvm-config-dir/reflect-config.json | 17 +++++- .../signal/manager/helper/ProfileHelper.java | 14 ++--- .../manager/storage/recipients/Profile.java | 20 +++---- .../storage/recipients/RecipientStore.java | 10 ++-- .../signal/manager/util/PaymentUtils.java | 52 +++++++++++++++++++ .../signal/manager/util/ProfileUtils.java | 39 +++++++++++++- .../signal/commands/ListContactsCommand.java | 13 ++--- 7 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index f9deabc6..6d5df0d0 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -538,7 +538,7 @@ {"name":"familyName","parameterTypes":[] }, {"name":"givenName","parameterTypes":[] }, {"name":"lastUpdateTimestamp","parameterTypes":[] }, - {"name":"paymentAddress","parameterTypes":[] } + {"name":"mobileCoinAddress","parameterTypes":[] } ] }, { @@ -2846,6 +2846,21 @@ {"name":"padding_"} ] }, +{ + "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$PaymentAddress", + "fields":[ + {"name":"addressCase_"}, + {"name":"address_"} + ] +}, +{ + "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$PaymentAddress$MobileCoinAddress", + "fields":[ + {"name":"address_"}, + {"name":"bitField0_"}, + {"name":"signature_"} + ] +}, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Preview", "fields":[ diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 8a64c8b7..1a876d7e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -1,7 +1,5 @@ package org.asamk.signal.manager.helper; -import com.google.protobuf.InvalidProtocolBufferException; - import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -13,6 +11,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.PaymentUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.protocol.IdentityKey; @@ -29,7 +28,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.services.ProfileService; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import java.io.File; import java.io.IOException; @@ -185,13 +183,9 @@ public final class ProfileHelper { final var avatarUploadParams = streamDetails != null ? AvatarUploadParams.forAvatar(streamDetails) : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); - final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> { - try { - return SignalServiceProtos.PaymentAddress.parseFrom(data); - } catch (InvalidProtocolBufferException e) { - return null; - } - }); + final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress()) + .map(address -> PaymentUtils.signPaymentsAddress(address, + account.getAciIdentityKeyPair().getPrivateKey())); logger.debug("Uploading new profile"); final var avatarPath = dependencies.getAccountManager() .setVersionedProfile(account.getAci(), 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 index 3f14f645..909337b7 100644 --- 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 @@ -20,7 +20,7 @@ public class Profile { private final String avatarUrlPath; - private final byte[] paymentAddress; + private final byte[] mobileCoinAddress; private final UnidentifiedAccessMode unidentifiedAccessMode; @@ -33,7 +33,7 @@ public class Profile { final String about, final String aboutEmoji, final String avatarUrlPath, - final byte[] paymentAddress, + final byte[] mobileCoinAddress, final UnidentifiedAccessMode unidentifiedAccessMode, final Set capabilities ) { @@ -43,7 +43,7 @@ public class Profile { this.about = about; this.aboutEmoji = aboutEmoji; this.avatarUrlPath = avatarUrlPath; - this.paymentAddress = paymentAddress; + this.mobileCoinAddress = mobileCoinAddress; this.unidentifiedAccessMode = unidentifiedAccessMode; this.capabilities = capabilities; } @@ -55,7 +55,7 @@ public class Profile { about = builder.about; aboutEmoji = builder.aboutEmoji; avatarUrlPath = builder.avatarUrlPath; - paymentAddress = builder.paymentAddress; + mobileCoinAddress = builder.mobileCoinAddress; unidentifiedAccessMode = builder.unidentifiedAccessMode; capabilities = builder.capabilities; } @@ -72,7 +72,7 @@ public class Profile { builder.about = copy.getAbout(); builder.aboutEmoji = copy.getAboutEmoji(); builder.avatarUrlPath = copy.getAvatarUrlPath(); - builder.paymentAddress = copy.getPaymentAddress(); + builder.mobileCoinAddress = copy.getMobileCoinAddress(); builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode(); builder.capabilities = copy.getCapabilities(); return builder; @@ -124,8 +124,8 @@ public class Profile { return avatarUrlPath; } - public byte[] getPaymentAddress() { - return paymentAddress; + public byte[] getMobileCoinAddress() { + return mobileCoinAddress; } public UnidentifiedAccessMode getUnidentifiedAccessMode() { @@ -200,7 +200,7 @@ public class Profile { private String about; private String aboutEmoji; private String avatarUrlPath; - private byte[] paymentAddress; + private byte[] mobileCoinAddress; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private Set capabilities = Collections.emptySet(); private long lastUpdateTimestamp = 0; @@ -252,8 +252,8 @@ public class Profile { return this; } - public Builder withPaymentAddress(final byte[] val) { - paymentAddress = val; + public Builder withMobileCoinAddress(final byte[] val) { + mobileCoinAddress = val; return this; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 850b6270..0297f6d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -105,9 +105,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile r.profile.about, r.profile.aboutEmoji, r.profile.avatarUrlPath, - r.profile.paymentAddress == null + r.profile.mobileCoinAddress == null ? null - : Base64.getDecoder().decode(r.profile.paymentAddress), + : Base64.getDecoder().decode(r.profile.mobileCoinAddress), Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode), r.profile.capabilities.stream() .map(Profile.Capability::valueOfOrNull) @@ -592,9 +592,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile recipientProfile.getAbout(), recipientProfile.getAboutEmoji(), recipientProfile.getAvatarUrlPath(), - recipientProfile.getPaymentAddress() == null + recipientProfile.getMobileCoinAddress() == null ? null - : base64.encodeToString(recipientProfile.getPaymentAddress()), + : base64.encodeToString(recipientProfile.getMobileCoinAddress()), recipientProfile.getUnidentifiedAccessMode().name(), recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet())); return new Storage.Recipient(pair.getKey().id(), @@ -651,7 +651,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile String about, String aboutEmoji, String avatarUrlPath, - String paymentAddress, + String mobileCoinAddress, String unidentifiedAccessMode, Set capabilities ) {} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java new file mode 100644 index 00000000..6c34d9c9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java @@ -0,0 +1,52 @@ +package org.asamk.signal.manager.util; + +import com.google.protobuf.ByteString; + +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.IdentityKeyPair; +import org.signal.libsignal.protocol.ecc.ECPrivateKey; +import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +public class PaymentUtils { + + private PaymentUtils() { + } + + /** + * Signs the supplied address bytes with the {@link IdentityKeyPair}'s private key and returns a proto that includes it, and it's signature. + */ + public static SignalServiceProtos.PaymentAddress signPaymentsAddress( + byte[] publicAddressBytes, ECPrivateKey privateKey + ) { + byte[] signature = privateKey.calculateSignature(publicAddressBytes); + + return SignalServiceProtos.PaymentAddress.newBuilder() + .setMobileCoinAddress(SignalServiceProtos.PaymentAddress.MobileCoinAddress.newBuilder() + .setAddress(ByteString.copyFrom(publicAddressBytes)) + .setSignature(ByteString.copyFrom(signature))) + .build(); + } + + /** + * Verifies that the payments address is signed with the supplied {@link IdentityKey}. + *

+ * Returns the validated bytes if so, otherwise returns null. + */ + public static byte[] verifyPaymentsAddress( + SignalServiceProtos.PaymentAddress paymentAddress, ECPublicKey publicKey + ) { + if (!paymentAddress.hasMobileCoinAddress()) { + return null; + } + + byte[] bytes = paymentAddress.getMobileCoinAddress().getAddress().toByteArray(); + byte[] signature = paymentAddress.getMobileCoinAddress().getSignature().toByteArray(); + + if (signature.length != 64 || !publicKey.verifySignature(bytes, signature)) { + return null; + } + + return bytes; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 53a9cdb6..e202f70a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -1,14 +1,21 @@ package org.asamk.signal.manager.util; +import com.google.protobuf.InvalidProtocolBufferException; + import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.storage.recipients.Profile; +import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.protocol.InvalidKeyException; +import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; +import java.io.IOException; import java.util.Base64; import java.util.HashSet; @@ -20,6 +27,12 @@ public class ProfileUtils { final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { var profileCipher = new ProfileCipher(profileKey); + IdentityKey identityKey = null; + try { + identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()), 0); + } catch (InvalidKeyException ignored) { + } + try { var name = decrypt(encryptedProfile.getName(), profileCipher); var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher)); @@ -32,7 +45,11 @@ public class ProfileUtils { about, aboutEmoji, encryptedProfile.getAvatar(), - encryptedProfile.getPaymentAddress(), + identityKey == null || encryptedProfile.getPaymentAddress() == null + ? null + : decryptAndVerifyMobileCoinAddress(encryptedProfile.getPaymentAddress(), + profileCipher, + identityKey.getPublicKey()), getUnidentifiedAccessMode(encryptedProfile, profileCipher), getCapabilities(encryptedProfile)); } catch (InvalidCiphertextException e) { @@ -88,6 +105,26 @@ public class ProfileUtils { } } + private static byte[] decryptAndVerifyMobileCoinAddress( + final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey + ) throws InvalidCiphertextException { + byte[] decrypted; + try { + decrypted = profileCipher.decryptWithLength(encryptedPaymentAddress); + } catch (IOException e) { + return null; + } + + SignalServiceProtos.PaymentAddress paymentAddress; + try { + paymentAddress = SignalServiceProtos.PaymentAddress.parseFrom(decrypted); + } catch (InvalidProtocolBufferException e) { + return null; + } + + return PaymentUtils.verifyPaymentsAddress(paymentAddress, publicKey); + } + private static Pair splitName(String name) { if (name == null) { return new Pair<>(null, null); diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 0bfc7ab9..8fc6f2d1 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -81,10 +81,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand { r.getProfile().getFamilyName(), r.getProfile().getAbout(), r.getProfile().getAboutEmoji(), - r.getProfile().getPaymentAddress() == null + r.getProfile().getMobileCoinAddress() == null ? null : Base64.getEncoder() - .encodeToString(r.getProfile().getPaymentAddress()))); + .encodeToString(r.getProfile().getMobileCoinAddress()))); }).toList(); writer.write(jsonContacts); @@ -92,12 +92,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { } private record JsonContact( - String number, - String uuid, - String name, - boolean isBlocked, - int messageExpirationTime, - JsonProfile profile + String number, String uuid, String name, boolean isBlocked, int messageExpirationTime, JsonProfile profile ) { private record JsonProfile( @@ -106,7 +101,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { String familyName, String about, String aboutEmoji, - String paymentAddress + String mobileCoinAddress ) {} } } -- 2.51.0 From 34c0968f5e4db6b01b9c5cfd0d9556632e0667ba Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 May 2022 10:42:56 +0200 Subject: [PATCH 15/16] Add mobile-coin-address to updateProfile command --- .../org/asamk/signal/manager/ManagerImpl.java | 3 ++- .../asamk/signal/manager/api/UpdateProfile.java | 13 +++++++++++++ .../signal/manager/helper/ProfileHelper.java | 17 +++++++++++++---- .../signal/manager/helper/StorageHelper.java | 1 + man/signal-cli.1.adoc | 3 +++ .../signal/commands/UpdateProfileCommand.java | 9 ++++++++- 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 3ca3c109..96196f8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -270,7 +270,8 @@ class ManagerImpl implements Manager { updateProfile.getAboutEmoji(), updateProfile.isDeleteAvatar() ? Optional.empty() - : updateProfile.getAvatar() == null ? null : Optional.of(updateProfile.getAvatar())); + : updateProfile.getAvatar() == null ? null : Optional.of(updateProfile.getAvatar()), + updateProfile.getMobileCoinAddress()); context.getSyncHelper().sendSyncFetchProfileMessage(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java index 2b47f7d5..d5de308e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/UpdateProfile.java @@ -10,6 +10,7 @@ public class UpdateProfile { private final String aboutEmoji; private final File avatar; private final boolean deleteAvatar; + private final byte[] mobileCoinAddress; private UpdateProfile(final Builder builder) { givenName = builder.givenName; @@ -18,6 +19,7 @@ public class UpdateProfile { aboutEmoji = builder.aboutEmoji; avatar = builder.avatar; deleteAvatar = builder.deleteAvatar; + mobileCoinAddress = builder.mobileCoinAddress; } public static Builder newBuilder() { @@ -32,6 +34,7 @@ public class UpdateProfile { builder.aboutEmoji = copy.getAboutEmoji(); builder.avatar = copy.getAvatar(); builder.deleteAvatar = copy.isDeleteAvatar(); + builder.mobileCoinAddress = copy.getMobileCoinAddress(); return builder; } @@ -59,6 +62,10 @@ public class UpdateProfile { return deleteAvatar; } + public byte[] getMobileCoinAddress() { + return mobileCoinAddress; + } + public static final class Builder { private String givenName; @@ -67,6 +74,7 @@ public class UpdateProfile { private String aboutEmoji; private File avatar; private boolean deleteAvatar; + private byte[] mobileCoinAddress; private Builder() { } @@ -101,6 +109,11 @@ public class UpdateProfile { return this; } + public Builder withMobileCoinAddress(final byte[] val) { + mobileCoinAddress = val; + return this; + } + public UpdateProfile build() { return new UpdateProfile(this); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 1a876d7e..cc01dc73 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -64,7 +64,7 @@ public final class ProfileHelper { var profileKey = KeyUtils.createProfileKey(); account.setProfileKey(profileKey); context.getAccountHelper().updateAccountAttributes(); - setProfile(true, true, null, null, null, null, null); + setProfile(true, true, null, null, null, null, null, null); // TODO update profile key in storage final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing(); @@ -144,9 +144,14 @@ public final class ProfileHelper { * @param avatar if avatar is null the image from the local avatar store is used (if present), */ public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + String givenName, + final String familyName, + String about, + String aboutEmoji, + Optional avatar, + byte[] mobileCoinAddress ) throws IOException { - setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar); + setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar, mobileCoinAddress); } public void setProfile( @@ -156,7 +161,8 @@ public final class ProfileHelper { final String familyName, String about, String aboutEmoji, - Optional avatar + Optional avatar, + byte[] mobileCoinAddress ) throws IOException { var profile = getSelfProfile(); var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); @@ -172,6 +178,9 @@ public final class ProfileHelper { if (aboutEmoji != null) { builder.withAboutEmoji(aboutEmoji); } + if (mobileCoinAddress != null) { + builder.withMobileCoinAddress(mobileCoinAddress); + } var newProfile = builder.build(); if (uploadProfile) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 469ca02e..88fe84b9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -234,6 +234,7 @@ public class StorageHelper { accountRecord.getFamilyName().orElse(null), null, null, + null, null); } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 1cebd044..456c6f1a 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -493,6 +493,9 @@ Path to the new avatar image file. *--remove-avatar*:: Remove the avatar +*--mobile-coin-address*:: +New MobileCoin address (Base64 encoded public address) + === updateContact Update the info associated to a number on our contact list. diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index 1edb6df9..d8e87430 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -12,6 +12,7 @@ import org.asamk.signal.output.OutputWriter; import java.io.File; import java.io.IOException; +import java.util.Base64; public class UpdateProfileCommand implements JsonRpcLocalCommand { @@ -27,6 +28,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { subparser.addArgument("--family-name").help("New profile family name (optional)"); subparser.addArgument("--about").help("New profile about text"); subparser.addArgument("--about-emoji").help("New profile about emoji"); + subparser.addArgument("--mobile-coin-address").help("New MobileCoin address (Base64 encoded public address)"); final var avatarOptions = subparser.addMutuallyExclusiveGroup(); avatarOptions.addArgument("--avatar").help("Path to new profile avatar"); @@ -41,9 +43,13 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { var familyName = ns.getString("family-name"); var about = ns.getString("about"); var aboutEmoji = ns.getString("about-emoji"); + var mobileCoinAddressString = ns.getString("mobile-coin-address"); + var mobileCoinAddress = mobileCoinAddressString == null + ? null + : Base64.getDecoder().decode(mobileCoinAddressString); + var avatarPath = ns.getString("avatar"); boolean removeAvatar = Boolean.TRUE.equals(ns.getBoolean("remove-avatar")); - File avatarFile = removeAvatar || avatarPath == null ? null : new File(avatarPath); try { @@ -52,6 +58,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { .withFamilyName(familyName) .withAbout(about) .withAboutEmoji(aboutEmoji) + .withMobileCoinAddress(mobileCoinAddress) .withAvatar(avatarFile) .withDeleteAvatar(removeAvatar) .build()); -- 2.51.0 From 5a63b5419fc109c8eede318dba096571161165a4 Mon Sep 17 00:00:00 2001 From: Sebastian Scheibner Date: Sat, 21 May 2022 11:47:46 +0200 Subject: [PATCH 16/16] Update README.md --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e6f74c46..570451c2 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,23 @@ verifying, sending and receiving messages. To be able to link to an existing Sig signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a linked device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For -registering you need a phone number where you can receive SMS or incoming calls. signal-cli is primarily intended to be -used on servers to notify admins of important events. For this use-case, it has a dbus -interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to -send messages from any programming language that has dbus bindings. It also has a JSON-RPC based interface, see -the [documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service) for more information. +registering you need a phone number where you can receive SMS or incoming calls. + +signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a daemon mode with D-BUS +interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) and JSON-PRC interface ([documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service)). For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust. ## Installation -You can [build signal-cli](#building) yourself, or use +You can [build signal-cli](#building) yourself or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and -Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/), as well as -a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) and -an [Alpine aport](https://pkgs.alpinelinux.org/packages?name=signal-cli). +Windows. There's also a [docker image and some Linux packages](https://github.com/AsamK/signal-cli/wiki/Binary-distributions) provided by the community. System requirements: - at least Java Runtime Environment (JRE) 17 - native library: libsignal-client - The native libs are bundled for x86_64 Linux (with recent enough glibc, see #643), Windows and MacOS. For other + The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other systems/architectures see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal) -- 2.51.0