From: AsamK Date: Wed, 25 Aug 2021 10:22:53 +0000 (+0200) Subject: Add RecipientIdentifier as external Manager interface X-Git-Tag: v0.9.0~46 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/467a48bac508b56f84dce7ee0b81a22fd0d32161?ds=sidebyside Add RecipientIdentifier as external Manager interface --- 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 ae3f6aed..92dcf4d6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -18,6 +18,9 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; @@ -156,6 +159,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -354,9 +358,6 @@ public class Manager implements Closeable { .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); - // Store numbers as recipients so we have the number/uuid association - contactDetails.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); final var uuid = contactDetails.get(number); @@ -697,31 +698,9 @@ public class Manager implements Closeable { return account.getGroupStore().getGroups(); } - public Pair> sendGroupMessage( - Message message, GroupId groupId - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = createMessageBuilder(); - applyMessage(messageBuilder, message); - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId - ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendQuitGroupMessage( - GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { + public SendGroupMessageResults sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV1) { return quitGroupV1((GroupInfoV1) group); @@ -737,19 +716,19 @@ public class Manager implements Closeable { } } - private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupInfoV1.getGroupId().serialize()) .build(); - var messageBuilder = createMessageBuilder().asGroupMessage(group); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); - return sendHelper.sendGroupMessage(messageBuilder.build(), + return sendGroupMessage(messageBuilder, groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private Pair> quitGroupV2( + private SendGroupMessageResults quitGroupV2( final GroupInfoV2 groupInfoV2, final Set newAdmins ) throws LastGroupAdminException, IOException { final var currentAdmins = groupInfoV2.getAdminMembers(); @@ -764,9 +743,10 @@ public class Manager implements Closeable { } final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); - return sendHelper.sendGroupMessage(messageBuilder.build(), + + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + return sendGroupMessage(messageBuilder, groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } @@ -775,13 +755,13 @@ public class Manager implements Closeable { avatarStore.deleteGroupAvatar(groupId); } - public Pair> createGroup( - String name, List members, File avatarFile - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); } - private Pair> createGroup( + private Pair createGroupInternal( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { final var selfRecipientId = account.getSelfRecipientId(); @@ -794,13 +774,12 @@ public class Manager implements Closeable { members == null ? Set.of() : members, avatarFile); - SignalServiceDataMessage.Builder messageBuilder; if (gv2Pair == null) { // Failed to create v2 group, creating v1 group instead var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); gv1.addMembers(List.of(selfRecipientId)); final var result = updateGroupV1(gv1, name, members, avatarFile); - return new Pair<>(gv1.getGroupId(), result.second()); + return new Pair<>(gv1.getGroupId(), result); } final var gv2 = gv2Pair.first(); @@ -811,22 +790,23 @@ public class Manager implements Closeable { avatarStore.storeGroupAvatar(gv2.getGroupId(), outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); } - messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + account.getGroupStore().updateGroup(gv2); - final var result = sendHelper.sendGroupMessage(messageBuilder.build(), - gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result.second()); + final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + + final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result); } - public Pair> updateGroup( + public SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, - List members, - List removeMembers, - List admins, - List removeAdmins, + Set members, + Set removeMembers, + Set admins, + Set removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, GroupPermission addMemberPermission, @@ -834,8 +814,8 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return updateGroup(groupId, + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + return updateGroupInternal(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -851,7 +831,7 @@ public class Manager implements Closeable { isAnnouncementGroup); } - private Pair> updateGroup( + private SendGroupMessageResults updateGroupInternal( final GroupId groupId, final String name, final String description, @@ -913,16 +893,15 @@ public class Manager implements Closeable { return result; } - private Pair> updateGroupV1( + private SendGroupMessageResults updateGroupV1( final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { updateGroupV1Details(gv1, name, members, avatarFile); - var messageBuilder = getGroupUpdateMessageBuilder(gv1); account.getGroupStore().updateGroup(gv1); - return sendHelper.sendGroupMessage(messageBuilder.build(), - gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private void updateGroupV1Details( @@ -963,7 +942,7 @@ public class Manager implements Closeable { } } - private Pair> updateGroupV2( + private SendGroupMessageResults updateGroupV2( final GroupInfoV2 group, final String name, final String description, @@ -979,7 +958,7 @@ public class Manager implements Closeable { final Integer expirationTimer, final Boolean isAnnouncementGroup ) throws IOException { - Pair> result = null; + SendGroupMessageResults result = null; if (group.isPendingMember(account.getSelfRecipientId())) { var groupGroupChangePair = groupV2Helper.acceptInvite(group); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); @@ -1080,7 +1059,7 @@ public class Manager implements Closeable { return result; } - public Pair> joinGroup( + public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -1094,25 +1073,74 @@ public class Manager implements Closeable { if (group.getGroup() == null) { // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), List.of()); + return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); } final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - return new Pair<>(group.getGroupId(), result.second()); + return new Pair<>(group.getGroupId(), result); } - private Pair> sendUpdateGroupV2Message( + private SendGroupMessageResults sendUpdateGroupV2Message( GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange ) throws IOException { final var selfRecipientId = account.getSelfRecipientId(); final var members = group.getMembersIncludingPendingWithout(selfRecipientId); group.setGroup(newDecryptedGroup, this::resolveRecipient); members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); + account.getGroupStore().updateGroup(group); final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendHelper.sendGroupMessage(messageBuilder.build(), members); + return sendGroupMessage(messageBuilder, members); + } + + public SendMessageResults sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + final var result = sendHelper.sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { + final var result = sendHelper.sendSelfMessage(messageBuilder); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + results.put(recipient, result); + } + } + return new SendMessageResults(timestamp, results); + } + + public void sendTypingMessage( + SignalServiceTypingMessage.Action action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + final var timestamp = System.currentTimeMillis(); + for (var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + sendHelper.sendTypingMessage(message, recipientId); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); + } + } + } + + private SendGroupMessageResults sendGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Set members + ) throws IOException { + final var timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); + return new SendGroupMessageResults(timestamp, results); } private static int currentTimeDays() { @@ -1138,7 +1166,7 @@ public class Manager implements Closeable { } } - Pair> sendGroupInfoMessage( + SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; @@ -1156,7 +1184,7 @@ public class Manager implements Closeable { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); + return sendGroupMessage(messageBuilder, Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1177,45 +1205,49 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) .withSignedGroupChange(signedGroupChange); - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); } - Pair> sendGroupInfoRequest( + SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); + return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); } public void sendReadReceipt( - String sender, List messageIds - ) throws IOException, UntrustedIdentityException, InvalidNumberException { + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, System.currentTimeMillis()); - sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } public void sendViewedReceipt( - String sender, List messageIds - ) throws IOException, UntrustedIdentityException, InvalidNumberException { + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, System.currentTimeMillis()); - sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } void sendDeliveryReceipt( @@ -1228,12 +1260,12 @@ public class Manager implements Closeable { sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } - public Pair> sendMessage( - Message message, List recipients - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = createMessageBuilder(); + public SendMessageResults sendMessage( + Message message, Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + return sendMessage(messageBuilder, recipients); } private void applyMessage( @@ -1258,48 +1290,41 @@ public class Manager implements Closeable { } } - public Pair sendSelfMessage(final Message message) throws IOException, AttachmentInvalidException { - final var messageBuilder = createMessageBuilder(); - applyMessage(messageBuilder, message); - return sendHelper.sendSelfMessage(messageBuilder); - } - - public Pair> sendRemoteDeleteMessage( - long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); - } - - public Pair> sendGroupRemoteDeleteMessage( - long targetSentTimestamp, GroupId groupId + public SendMessageResults sendRemoteDeleteMessage( + long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + return sendMessage(messageBuilder, recipients); } - public Pair> sendMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); + public SendMessageResults sendMessageReaction( + String emoji, + boolean remove, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + return sendMessage(messageBuilder, recipients); } - public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - var messageBuilder = createMessageBuilder().asEndSessionMessage(); + public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { + var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - final var recipientIds = getRecipientIds(recipients); try { - return sendHelper.sendMessage(messageBuilder, recipientIds); + return sendMessage(messageBuilder, + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new AssertionError(e); } finally { - for (var recipientId : recipientIds) { + for (var recipient : recipients) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); handleEndSession(recipientId); } } @@ -1312,23 +1337,25 @@ public class Manager implements Closeable { } } - public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = canonicalizeAndResolveRecipient(number); + final var recipientId = resolveRecipient(recipient); var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); account.getContactStore().storeContact(recipientId, builder.withName(name).build()); } public void setContactBlocked( - String number, boolean blocked - ) throws InvalidNumberException, NotMasterDeviceException { + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); + setContactBlocked(resolveRecipient(recipient), blocked); } private void setContactBlocked(RecipientId recipientId, boolean blocked) { @@ -1357,20 +1384,20 @@ public class Manager implements Closeable { .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } - private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); - sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); - } - /** * Change the expiration timer for a contact */ public void setExpirationTimer( - String number, int messageExpirationTimer - ) throws IOException, InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(number); + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); setExpirationTimer(recipientId, messageExpirationTimer); - sendExpirationTimerUpdate(recipientId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException e) { + throw new AssertionError(e); + } } /** @@ -1385,7 +1412,7 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); sendHelper.sendAsGroupMessage(messageBuilder, groupId); } @@ -1395,7 +1422,7 @@ public class Manager implements Closeable { * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ - public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -1414,7 +1441,7 @@ public class Manager implements Closeable { "pack_id=" + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); } catch (URISyntaxException e) { throw new AssertionError(e); } @@ -1484,12 +1511,12 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection numbers) throws InvalidNumberException { - final var signalServiceAddresses = new HashSet(numbers.size()); + private Set getRecipientIds(Collection recipients) { + final var signalServiceAddresses = new HashSet(recipients.size()); final var addressesMissingUuid = new HashSet(); - for (var number : numbers) { - final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number)); + for (var number : recipients) { + final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { @@ -1534,40 +1561,26 @@ public class Manager implements Closeable { } private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; try { - return dependencies.getAccountManager() + registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { throw new IOException(e); } - } - public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, InvalidNumberException { - final var timestamp = System.currentTimeMillis(); - var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); - sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); - } + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - public void sendGroupTypingMessage( - TypingAction action, GroupId groupId - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var timestamp = System.currentTimeMillis(); - final var message = new SignalServiceTypingMessage(action.toSignalService(), - timestamp, - Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); + return registeredUsers; } - private SignalServiceDataMessage.Builder createMessageBuilder() { - final var timestamp = System.currentTimeMillis(); - - var messageBuilder = SignalServiceDataMessage.newBuilder(); - messageBuilder.withTimestamp(timestamp); - return messageBuilder; + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + sendTypingMessage(action.toSignalService(), recipients); } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { @@ -2005,8 +2018,8 @@ public class Manager implements Closeable { return false; } - public boolean isContactBlocked(final String identifier) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(identifier); + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final var recipientId = resolveRecipient(recipient); return isContactBlocked(recipientId); } @@ -2607,8 +2620,8 @@ public class Manager implements Closeable { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(number); + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final var recipientId = resolveRecipient(recipientIdentifier); final var recipient = account.getRecipientStore().getRecipient(recipientId); if (recipient == null) { return null; @@ -2643,19 +2656,19 @@ public class Manager implements Closeable { return account.getIdentityKeyStore().getIdentities(); } - public List getIdentities(String number) throws InvalidNumberException { - final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number)); + public List getIdentities(RecipientIdentifier.Single recipient) { + final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); return identity == null ? List.of() : List.of(identity); } /** * Trust this the identity with this fingerprint * - * @param name username of the identity + * @param recipient username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -2664,11 +2677,11 @@ public class Manager implements Closeable { /** * Trust this the identity with this safety number * - * @param name username of the identity + * @param recipient username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), @@ -2678,11 +2691,11 @@ public class Manager implements Closeable { /** * Trust this the identity with this scannable safety number * - * @param name username of the identity + * @param recipient username of the identity * @param safetyNumber Scannable safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, byte[] safetyNumber) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); @@ -2697,10 +2710,10 @@ public class Manager implements Closeable { /** * Trust all keys of this identity without verification * - * @param name username of the identity + * @param recipient username of the identity */ - public boolean trustIdentityAllKeys(String name) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -2782,12 +2795,6 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveServiceAddress(recipientId); } - private RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { - var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : canonicalizePhoneNumber(identifier); - - return resolveRecipient(canonicalizedNumber); - } - private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { return PhoneNumberFormatter.formatNumber(number, account.getUsername()); } @@ -2798,6 +2805,17 @@ public class Manager implements Closeable { return resolveRecipient(address); } + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { + final SignalServiceAddress address; + if (recipient instanceof RecipientIdentifier.Uuid) { + address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); + } else { + address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + } + + return resolveRecipient(address); + } + public RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java new file mode 100644 index 00000000..cbcf1724 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -0,0 +1,112 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.groups.GroupId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.UUID; + +public abstract class RecipientIdentifier { + + public static class NoteToSelf extends RecipientIdentifier { + + @Override + public boolean equals(final Object obj) { + return obj instanceof NoteToSelf; + } + + @Override + public int hashCode() { + return 5; + } + } + + public abstract static class Single extends RecipientIdentifier { + + public static Single fromString(String identifier, String localNumber) throws InvalidNumberException { + return UuidUtil.isUuid(identifier) + ? new Uuid(UUID.fromString(identifier)) + : new Number(PhoneNumberFormatter.formatNumber(identifier, localNumber)); + } + + public static Single fromAddress(SignalServiceAddress address) { + return address.getUuid().isPresent() + ? new Uuid(address.getUuid().get()) + : new Number(address.getNumber().get()); + } + } + + public static class Uuid extends Single { + + public final UUID uuid; + + public Uuid(final UUID uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Uuid uuid1 = (Uuid) o; + + return uuid.equals(uuid1.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + } + + public static class Number extends Single { + + public final String number; + + public Number(final String number) { + this.number = number; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Number number1 = (Number) o; + + return number.equals(number1.number); + } + + @Override + public int hashCode() { + return number.hashCode(); + } + } + + public static class Group extends RecipientIdentifier { + + public final GroupId groupId; + + public Group(final GroupId groupId) { + this.groupId = groupId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Group group = (Group) o; + + return groupId.equals(group.groupId); + } + + @Override + public int hashCode() { + return groupId.hashCode(); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java b/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java new file mode 100644 index 00000000..d5c9ef91 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java @@ -0,0 +1,26 @@ +package org.asamk.signal.manager.api; + +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +import java.util.List; + +public class SendGroupMessageResults { + + private final long timestamp; + private final List results; + + public SendGroupMessageResults( + final long timestamp, final List results + ) { + this.timestamp = timestamp; + this.results = results; + } + + public long getTimestamp() { + return timestamp; + } + + public List getResults() { + return results; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java b/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java new file mode 100644 index 00000000..ff323919 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java @@ -0,0 +1,27 @@ +package org.asamk.signal.manager.api; + +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +import java.util.List; +import java.util.Map; + +public class SendMessageResults { + + private final long timestamp; + private final Map> results; + + public SendMessageResults( + final long timestamp, final Map> results + ) { + this.timestamp = timestamp; + this.results = results; + } + + public long getTimestamp() { + return timestamp; + } + + public Map> getResults() { + return results; + } +} 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 bc8800cf..b04ff40d 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,7 +11,6 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -67,36 +66,34 @@ public class SendHelper { * Send a single message to one or multiple recipients. * The message is extended with the current expiration timer for each recipient. */ - public Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipientIds + public SendMessageResult sendMessage( + final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId ) throws IOException { - // Send to all individually, so sync messages are sent correctly + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); messageBuilder.withProfileKey(account.getProfileKey().serialize()); - var results = new ArrayList(recipientIds.size()); - long timestamp = 0; - for (var recipientId : recipientIds) { - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - - final var singleMessage = messageBuilder.build(); - timestamp = singleMessage.getTimestamp(); - final var result = sendMessage(singleMessage, recipientId); - handlePossibleIdentityFailure(result); - results.add(result); - } - return new Pair<>(timestamp, results); + final var message = messageBuilder.build(); + final var result = sendMessage(message, recipientId); + handlePossibleIdentityFailure(result); + return result; } /** * Send a group message to the given group * The message is extended with the current expiration timer for the group and the group context. */ - public Pair> sendAsGroupMessage( + public List sendAsGroupMessage( SignalServiceDataMessage.Builder messageBuilder, GroupId groupId ) throws IOException, GroupNotFoundException, NotAGroupMemberException { final var g = getGroupForSending(groupId); + return sendAsGroupMessage(messageBuilder, g); + } + + private List sendAsGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g + ) throws IOException { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTime()); @@ -108,7 +105,7 @@ public class SendHelper { * Send a complete group message to the given recipients (should be current/old/new members) * This method should only be used for create/update/quit group messages. */ - public Pair> sendGroupMessage( + public List sendGroupMessage( final SignalServiceDataMessage message, final Set recipientIds ) throws IOException { List result = sendGroupMessageInternal(message, recipientIds); @@ -117,7 +114,7 @@ public class SendHelper { handlePossibleIdentityFailure(r); } - return new Pair<>(message.getTimestamp(), result); + return result; } public void sendReceiptMessage( @@ -146,7 +143,7 @@ public class SendHelper { } } - public Pair sendSelfMessage( + public SendMessageResult sendSelfMessage( SignalServiceDataMessage.Builder messageBuilder ) throws IOException { final var recipientId = account.getSelfRecipientId(); @@ -155,8 +152,7 @@ public class SendHelper { messageBuilder.withExpiration(expirationTime); var message = messageBuilder.build(); - final var result = sendSelfMessage(message); - return new Pair<>(message.getTimestamp(), result); + return sendSelfMessage(message); } public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException { @@ -170,18 +166,16 @@ public class SendHelper { } public void sendTypingMessage( - SignalServiceTypingMessage message, Set recipientIds + SignalServiceTypingMessage message, RecipientId recipientId ) throws IOException, UntrustedIdentityException { var messageSender = dependencies.getMessageSender(); - for (var recipientId : recipientIds) { - final var address = addressResolver.resolveSignalServiceAddress(recipientId); - try { - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } catch (UnregisteredUserException e) { - final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); - final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); - messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); - } + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); } } diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index e7a21b88..cd101929 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -31,7 +31,7 @@ public interface Signal extends DBusInterface { long sendGroupRemoteDeleteMessage( long targetSentTimestamp, byte[] groupId - ) throws Error.Failure, Error.GroupNotFound; + ) throws Error.Failure, Error.GroupNotFound, Error.InvalidGroupId; long sendMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, String recipient @@ -49,11 +49,11 @@ public interface Signal extends DBusInterface { long sendGroupMessage( String message, List attachments, byte[] groupId - ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid; + ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.InvalidGroupId; long sendGroupMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId - ) throws Error.GroupNotFound, Error.Failure, Error.InvalidNumber; + ) throws Error.GroupNotFound, Error.Failure, Error.InvalidNumber, Error.InvalidGroupId; String getContactName(String number) throws Error.InvalidNumber; @@ -61,17 +61,17 @@ public interface Signal extends DBusInterface { void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; - void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound; + void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; List getGroupIds(); - String getGroupName(byte[] groupId); + String getGroupName(byte[] groupId) throws Error.InvalidGroupId; - List getGroupMembers(byte[] groupId); + List getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; byte[] updateGroup( byte[] groupId, String name, List members, String avatar - ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound; + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; boolean isRegistered(); @@ -85,15 +85,15 @@ public interface Signal extends DBusInterface { List getContactNumber(final String name) throws Error.Failure; - void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure; + void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; boolean isContactBlocked(final String number) throws Error.InvalidNumber; - boolean isGroupBlocked(final byte[] groupId); + boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; - boolean isMember(final byte[] groupId); + boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; - void joinGroup(final String groupLink) throws Error.Failure; + byte[] joinGroup(final String groupLink) throws Error.Failure; class MessageReceived extends DBusSignal { @@ -235,6 +235,13 @@ public interface Signal extends DBusInterface { } } + class InvalidGroupId extends DBusExecutionException { + + public InvalidGroupId(final String message) { + super(message); + } + } + class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 323b6edf..0d8f312f 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,6 +1,7 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; @@ -18,7 +19,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.ArrayList; import java.util.Base64; @@ -450,7 +450,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final PlainTextWriter writer, final SignalServiceDataMessage.Reaction reaction ) { writer.println("Emoji: {}", reaction.getEmoji()); - writer.println("Target author: {}", formatContact(m.resolveSignalServiceAddress(reaction.getTargetAuthor()))); + writer.println("Target author: {}", formatContact(reaction.getTargetAuthor())); writer.println("Target timestamp: {}", DateUtils.formatTimestamp(reaction.getTargetSentTimestamp())); writer.println("Is remove: {}", reaction.isRemove()); } @@ -459,7 +459,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final PlainTextWriter writer, final SignalServiceDataMessage.Quote quote ) { writer.println("Id: {}", quote.getId()); - writer.println("Author: {}", getLegacyIdentifier(m.resolveSignalServiceAddress(quote.getAuthor()))); + writer.println("Author: {}", formatContact(quote.getAuthor())); writer.println("Text: {}", quote.getText()); if (quote.getMentions() != null && quote.getMentions().size() > 0) { writer.println("Mentions:"); @@ -678,12 +678,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } private String formatContact(SignalServiceAddress address) { + address = m.resolveSignalServiceAddress(address); final var number = getLegacyIdentifier(address); - String name = null; - try { - name = m.getContactOrProfileName(number); - } catch (InvalidNumberException ignored) { - } + final var name = m.getContactOrProfileName(RecipientIdentifier.Single.fromAddress(address)); if (name == null || name.isEmpty()) { return number; } else { diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 0710a7e5..7326c398 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -8,12 +8,10 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; public class BlockCommand implements JsonRpcLocalCommand { @@ -35,23 +33,22 @@ public class BlockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : ns.getList("contact")) { + final var contacts = ns.getList("contact"); + for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { try { - m.setContactBlocked(contactNumber, true); - } catch (InvalidNumberException e) { - logger.warn("Invalid number {}: {}", contactNumber, e.getMessage()); + m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } } - if (ns.getList("group-id") != null) { - for (var groupIdString : ns.getList("group-id")) { + final var groupIdStrings = ns.getList("group-id"); + if (groupIdStrings != null) { + for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { - var groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, true); - } catch (GroupIdFormatException | GroupNotFoundException e) { - logger.warn("Invalid group id {}: {}", groupIdString, e.getMessage()); + } catch (GroupNotFoundException e) { + logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 543d9cc7..8d651592 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -70,7 +70,7 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { writer.println("Joined group \"{}\"", newGroupId.toBase64()); } } - handleSendMessageResults(results.second()); + handleSendMessageResults(results.second().getResults()); } catch (GroupPatchNotAcceptedException e) { throw new UserErrorException("Failed to join group, maybe already a member"); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 49ca2546..c859996e 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,15 +7,14 @@ import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; -import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Base64; import java.util.List; @@ -58,11 +57,7 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { if (number == null) { identities = m.getIdentities(); } else { - try { - identities = m.getIdentities(number); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } + identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername())); } if (outputWriter instanceof PlainTextWriter) { diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c5c7472c..c39f298d 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -11,20 +11,15 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import static org.asamk.signal.util.ErrorUtils.handleSendMessageResults; @@ -53,22 +48,16 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final GroupId groupId; - try { - groupId = Util.decodeGroupId(ns.getString("group-id")); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + final var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - var groupAdmins = ns.getList("admin"); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername()); try { try { - final var results = m.sendQuitGroupMessage(groupId, - groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); - final var timestamp = results.first(); + final var results = m.sendQuitGroupMessage(groupId, groupAdmins); + final var timestamp = results.getTimestamp(); outputResult(outputWriter, timestamp); - handleSendMessageResults(results.second()); + handleSendMessageResults(results.getResults()); } catch (NotAGroupMemberException e) { logger.info("User is not a group member"); } @@ -80,8 +69,6 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { throw new IOErrorException("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse admin number: " + e.getMessage()); } catch (LastGroupAdminException e) { throw new UserErrorException("You need to specify a new admin with --admin: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 99100407..f033e0b1 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -1,5 +1,6 @@ package org.asamk.signal.commands; +import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -10,14 +11,15 @@ import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import java.util.List; +import java.io.IOException; import java.util.Map; public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { @@ -34,40 +36,62 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { .required(true) .type(long.class) .help("Specify the timestamp of the message to delete."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("--note-to-self").action(Arguments.storeTrue()); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final long targetTimestamp = ns.getLong("target-timestamp"); + + try { + final var results = m.sendRemoteDeleteMessage(targetTimestamp, recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } } @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); + final var recipients = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if (noRecipients && noGroups) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } final long targetTimestamp = ns.getLong("target-timestamp"); - byte[] groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } - try { - long timestamp; - if (groupId != null) { - timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId); + long timestamp = 0; + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); + for (final var groupId : groupIds) { + timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize()); + } } else { timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients); } @@ -83,13 +107,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index dbb02248..a1e4c296 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -12,11 +12,15 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; +import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; @@ -26,6 +30,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class SendCommand implements DbusCommand, JsonRpcLocalCommand { @@ -40,9 +45,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help("Send a message to another user or group."); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); - final var mutuallyExclusiveGroup = subparser.addMutuallyExclusiveGroup(); - mutuallyExclusiveGroup.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); - mutuallyExclusiveGroup.addArgument("--note-to-self") + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("--note-to-self") .help("Send the message to self without notification.") .action(Arguments.storeTrue()); @@ -53,20 +57,77 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { .action(Arguments.storeTrue()); } + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final var isEndSession = ns.getBoolean("end-session"); + if (isEndSession) { + final var singleRecipients = recipientIdentifiers.stream() + .filter(r -> r instanceof RecipientIdentifier.Single) + .map(RecipientIdentifier.Single.class::cast) + .collect(Collectors.toSet()); + if (singleRecipients.isEmpty()) { + throw new UserErrorException("No recipients given"); + } + + try { + m.sendEndSessionMessage(singleRecipients); + return; + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } + } + + var messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); + } catch (IOException e) { + throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); + } + } + + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = List.of(); + } + + try { + var results = m.sendMessage(new Message(messageText, attachments), recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (AttachmentInvalidException | IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } + } + @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); + final var recipients = ns.getList("recipient"); final var isEndSession = ns.getBoolean("end-session"); - final var groupIdString = ns.getString("group-id"); + final var groupIdStrings = ns.getList("group-id"); final var isNoteToSelf = ns.getBoolean("note-to-self"); final var noRecipients = recipients == null || recipients.isEmpty(); - if ((noRecipients && isEndSession) || (noRecipients && groupIdString == null && !isNoteToSelf)) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } if (!noRecipients && isNoteToSelf) { @@ -99,16 +160,14 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { attachments = List.of(); } - if (groupIdString != null) { - byte[] groupId; - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); try { - var timestamp = signal.sendGroupMessage(messageText, attachments, groupId); + long timestamp = 0; + for (final var groupId : groupIds) { + timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize()); + } outputResult(outputWriter, timestamp); return; } catch (DBusExecutionException e) { @@ -149,11 +208,4 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { writer.write(Map.of("timestamp", timestamp)); } } - - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index fedded42..c8e339a1 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -11,14 +11,15 @@ import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import java.util.List; +import java.io.IOException; import java.util.Map; public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { @@ -31,8 +32,11 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Send reaction to a previously received or sent message."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("--note-to-self") + .help("Send the reaction to self without notification.") + .action(Arguments.storeTrue()); subparser.addArgument("-e", "--emoji") .required(true) .help("Specify the emoji, should be a single unicode grapheme cluster."); @@ -46,39 +50,71 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { subparser.addArgument("-r", "--remove").help("Remove a reaction.").action(Arguments.storeTrue()); } + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final var emoji = ns.getString("emoji"); + final var isRemove = ns.getBoolean("remove"); + final var targetAuthor = ns.getString("target-author"); + final var targetTimestamp = ns.getLong("target-timestamp"); + + try { + final var results = m.sendMessageReaction(emoji, + isRemove, + CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + targetTimestamp, + recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } + } + @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); + final var recipients = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if (noRecipients && noGroups) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } final var emoji = ns.getString("emoji"); - final boolean isRemove = ns.getBoolean("remove"); + final var isRemove = ns.getBoolean("remove"); final var targetAuthor = ns.getString("target-author"); - final long targetTimestamp = ns.getLong("target-timestamp"); - - byte[] groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } + final var targetTimestamp = ns.getLong("target-timestamp"); try { - long timestamp; - if (groupId != null) { - timestamp = signal.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); + long timestamp = 0; + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); + for (final var groupId : groupIds) { + timestamp = signal.sendGroupMessageReaction(emoji, + isRemove, + targetAuthor, + targetTimestamp, + groupId.serialize()); + } } else { timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients); } @@ -94,13 +130,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 74f48112..afdbd4f8 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -7,8 +7,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -34,7 +34,8 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var recipient = ns.getString("recipient"); + final var recipientString = ns.getString("recipient"); + final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); final var targetTimestamps = ns.getList("target-timestamp"); final var type = ns.getString("type"); @@ -49,8 +50,6 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { } } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index d5a68604..14139885 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -8,14 +8,12 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.HashSet; @@ -31,7 +29,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help( "Send typing message to trigger a typing indicator for the recipient. Indicator will be shown for 15seconds unless a typing STOP message is sent first."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); subparser.addArgument("-s", "--stop").help("Send a typing STOP message.").action(Arguments.storeTrue()); } @@ -40,40 +38,29 @@ public class SendTypingCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START; - final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { - throw new UserErrorException("No recipients given"); + final var recipientIdentifiers = new HashSet(); + if (recipientStrings != null) { + final var localNumber = m.getUsername(); + recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); } - if (!noRecipients && groupIdString != null) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); + if (groupIdStrings != null) { + recipientIdentifiers.addAll(CommandUtil.getGroupIdentifiers(groupIdStrings)); } - final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START; - - GroupId groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + if (recipientIdentifiers.isEmpty()) { + throw new UserErrorException("No recipients given"); } try { - if (groupId != null) { - m.sendGroupTypingMessage(action, groupId); - } else { - m.sendTypingMessage(action, new HashSet<>(recipients)); - } + m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 65487ba7..22dcc5d8 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -8,8 +8,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Base64; import java.util.Locale; @@ -37,14 +37,10 @@ public class TrustCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var number = ns.getString("number"); + var recipentString = ns.getString("number"); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); if (ns.getBoolean("trust-all-known-keys")) { - boolean res; - try { - res = m.trustIdentityAllKeys(number); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityAllKeys(recipient); if (!res) { throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct."); } @@ -64,23 +60,13 @@ public class TrustCommand implements JsonRpcLocalCommand { throw new UserErrorException( "Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); } - boolean res; - try { - res = m.trustIdentityVerified(number, fingerprintBytes); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerified(recipient, fingerprintBytes); if (!res) { throw new UserErrorException( "Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); } } else if (safetyNumber.length() == 60) { - boolean res; - try { - res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerifiedSafetyNumber(recipient, safetyNumber); if (!res) { throw new UserErrorException( "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); @@ -93,12 +79,7 @@ public class TrustCommand implements JsonRpcLocalCommand { throw new UserErrorException( "Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); } - boolean res; - try { - res = m.trustIdentityVerifiedSafetyNumber(number, scannableSafetyNumber); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerifiedSafetyNumber(recipient, scannableSafetyNumber); if (!res) { throw new UserErrorException( "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 830147bc..c5b9d1ca 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -8,12 +8,10 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; public class UnblockCommand implements JsonRpcLocalCommand { @@ -35,26 +33,20 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : ns.getList("contact")) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("contact"), m.getUsername())) { try { m.setContactBlocked(contactNumber, false); - } catch (InvalidNumberException e) { - logger.warn("Invalid number: {}", contactNumber); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } } - if (ns.getList("group-id") != null) { - for (var groupIdString : ns.getList("group-id")) { - try { - var groupId = Util.decodeGroupId(groupIdString); - m.setGroupBlocked(groupId, false); - } catch (GroupIdFormatException e) { - logger.warn("Invalid group id: {}", groupIdString); - } catch (GroupNotFoundException e) { - logger.warn("Unknown group id: {}", groupIdString); - } + final var groupIdStrings = ns.getList("group-id"); + for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { + try { + m.setGroupBlocked(groupId, false); + } catch (GroupNotFoundException e) { + logger.warn("Unknown group id: {}", groupId); } } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 462ba8d2..2b7d5b4b 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -9,7 +9,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.asamk.signal.util.CommandUtil; import java.io.IOException; @@ -32,20 +32,19 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var number = ns.getString("number"); + var recipientString = ns.getString("number"); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); try { var expiration = ns.getInt("expiration"); if (expiration != null) { - m.setExpirationTimer(number, expiration); + m.setExpirationTimer(recipient, expiration); } var name = ns.getString("name"); if (name != null) { - m.setContactName(number, name); + m.setContactName(recipient, name); } - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid contact number: " + e.getMessage()); } catch (IOException e) { throw new IOErrorException("Update contact error: " + e.getMessage()); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 00da38d4..fc2cfbc0 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -14,17 +14,15 @@ import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; import java.io.IOException; @@ -114,22 +112,17 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - GroupId groupId = null; final var groupIdString = ns.getString("group-id"); - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } + var groupId = CommandUtil.getGroupId(groupIdString); + + final var localNumber = m.getUsername(); var groupName = ns.getString("name"); var groupDescription = ns.getString("description"); - var groupMembers = ns.getList("member"); - var groupRemoveMembers = ns.getList("remove-member"); - var groupAdmins = ns.getList("admin"); - var groupRemoveAdmins = ns.getList("remove-admin"); + var groupMembers = CommandUtil.getSingleRecipientIdentifiers(ns.getList("member"), localNumber); + var groupRemoveMembers = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-member"), localNumber); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), localNumber); + var groupRemoveAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-admin"), localNumber); var groupAvatar = ns.getString("avatar"); var groupResetLink = ns.getBoolean("reset-link"); var groupLinkState = getGroupLinkState(ns.getString("link")); @@ -140,12 +133,14 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { try { boolean isNewGroup = false; + Long timestamp = null; if (groupId == null) { isNewGroup = true; var results = m.createGroup(groupName, groupMembers, groupAvatar == null ? null : new File(groupAvatar)); - ErrorUtils.handleSendMessageResults(results.second()); + timestamp = results.second().getTimestamp(); + ErrorUtils.handleSendMessageResults(results.second().getResults()); groupId = results.first(); groupName = null; groupMembers = null; @@ -168,20 +163,15 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { groupSendMessagesPermission == null ? null : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); - Long timestamp = null; if (results != null) { - timestamp = results.first(); - ErrorUtils.handleSendMessageResults(results.second()); + timestamp = results.getTimestamp(); + ErrorUtils.handleSendMessageResults(results.getResults()); } outputResult(outputWriter, timestamp, isNewGroup ? groupId : null); } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (GroupNotFoundException e) { - logger.warn("Unknown group id: {}", groupIdString); - } catch (NotAGroupMemberException e) { - logger.warn("You're not a group member"); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse member number: " + e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); } @@ -191,17 +181,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - byte[] groupId = null; - if (ns.getString("group-id") != null) { - try { - groupId = Util.decodeGroupId(ns.getString("group-id")).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } - if (groupId == null) { - groupId = new byte[0]; - } + var groupId = CommandUtil.getGroupId(ns.getString("group-id")); var groupName = ns.getString("name"); if (groupName == null) { @@ -219,8 +199,11 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } try { - var newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); - if (groupId.length != newGroupId.length) { + var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(), + groupName, + groupMembers, + groupAvatar); + if (groupId == null) { outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId)); } } catch (Signal.Error.AttachmentInvalid e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab2bcda0..c5be7501 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -24,7 +25,10 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -59,57 +63,22 @@ public class DbusSignalImpl implements Signal { return sendMessage(message, attachments, recipients); } - private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { - var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); - - if (error == null) { - return; - } - - final var message = timestamp + "\nFailed to send message:\n" + error + '\n'; - - if (result.getIdentityFailure() != null) { - throw new Error.UntrustedIdentity(message); - } else { - throw new Error.Failure(message); - } - } - - private static void checkSendMessageResults( - long timestamp, List results - ) throws DBusExecutionException { - if (results.size() == 1) { - checkSendMessageResult(timestamp, results.get(0)); - return; - } - - var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); - if (errors.size() == 0) { - return; - } - - var message = new StringBuilder(); - message.append(timestamp).append('\n'); - message.append("Failed to send (some) messages:\n"); - for (var error : errors) { - message.append(error).append('\n'); - } - - throw new Error.Failure(message.toString()); - } - @Override public long sendMessage(final String message, final List attachments, final List recipients) { try { - final var results = m.sendMessage(new Message(message, attachments), recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + final var results = m.sendMessage(new Message(message, attachments), + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -127,13 +96,16 @@ public class DbusSignalImpl implements Signal { final long targetSentTimestamp, final List recipients ) { try { - final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -142,9 +114,10 @@ public class DbusSignalImpl implements Signal { final long targetSentTimestamp, final byte[] groupId ) { try { - final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { @@ -174,13 +147,19 @@ public class DbusSignalImpl implements Signal { final List recipients ) { try { - final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + final var results = m.sendMessageReaction(emoji, + remove, + getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + targetSentTimestamp, + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -189,34 +168,36 @@ public class DbusSignalImpl implements Signal { final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendSelfMessage(new Message(message, attachments)); - checkSendMessageResult(results.first(), results.second()); - return results.first(); + final var results = m.sendMessage(new Message(message, attachments), + Set.of(new RecipientIdentifier.NoteToSelf())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(recipients); - checkSendMessageResults(results.first(), results.second()); + final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } } @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - var results = m.sendGroupMessage(new Message(message, attachments), GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + var results = m.sendMessage(new Message(message, attachments), + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { @@ -235,17 +216,15 @@ public class DbusSignalImpl implements Signal { final byte[] groupId ) { try { - final var results = m.sendGroupMessageReaction(emoji, + final var results = m.sendMessageReaction(emoji, remove, - targetAuthor, + getSingleRecipientIdentifier(targetAuthor, m.getUsername()), targetSentTimestamp, - GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -255,19 +234,13 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - try { - return m.getContactOrProfileName(number); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); - } + return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public void setContactName(final String number, final String name) { try { - m.setContactName(number, name); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } @@ -276,9 +249,7 @@ public class DbusSignalImpl implements Signal { @Override public void setContactBlocked(final String number, final boolean blocked) { try { - m.setContactBlocked(number, blocked); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } @@ -287,7 +258,7 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); + m.setGroupBlocked(getGroupId(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -305,7 +276,7 @@ public class DbusSignalImpl implements Signal { @Override public String getGroupName(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return ""; } else { @@ -315,7 +286,7 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return List.of(); } else { @@ -336,21 +307,19 @@ public class DbusSignalImpl implements Signal { if (name.isEmpty()) { name = null; } - if (members.isEmpty()) { - members = null; - } if (avatar.isEmpty()) { avatar = null; } + final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); if (groupId == null) { - final var results = m.createGroup(name, members, avatar == null ? null : new File(avatar)); - checkSendMessageResults(0, results.second()); + final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); + checkSendMessageResults(results.second().getTimestamp(), results.second().getResults()); return results.first().serialize(); } else { - final var results = m.updateGroup(GroupId.unknownVersion(groupId), + final var results = m.updateGroup(getGroupId(groupId), name, null, - members, + memberIdentifiers, null, null, null, @@ -362,7 +331,7 @@ public class DbusSignalImpl implements Signal { null, null); if (results != null) { - checkSendMessageResults(results.first(), results.second()); + checkSendMessageResults(results.getTimestamp(), results.getResults()); } return groupId; } @@ -370,8 +339,6 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } @@ -450,26 +417,25 @@ public class DbusSignalImpl implements Signal { @Override public void quitGroup(final byte[] groupId) { - var group = GroupId.unknownVersion(groupId); + var group = getGroupId(groupId); try { m.sendQuitGroupMessage(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } } @Override - public void joinGroup(final String groupLink) { + public byte[] joinGroup(final String groupLink) { try { final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink); if (linkUrl == null) { throw new Error.Failure("Group link is invalid:"); } - m.joinGroup(linkUrl); + final var result = m.joinGroup(linkUrl); + return result.first().serialize(); } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) { throw new Error.Failure("Group link is invalid: " + e.getMessage()); } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { @@ -481,16 +447,12 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - try { - return m.isContactBlocked(number); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); - } + return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public boolean isGroupBlocked(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { @@ -500,11 +462,102 @@ public class DbusSignalImpl implements Signal { @Override public boolean isMember(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { return group.isMember(m.getSelfRecipientId()); } } + + private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { + var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); + + if (error == null) { + return; + } + + final var message = timestamp + "\nFailed to send message:\n" + error + '\n'; + + if (result.getIdentityFailure() != null) { + throw new Error.UntrustedIdentity(message); + } else { + throw new Error.Failure(message); + } + } + + private static void checkSendMessageResults( + long timestamp, Map> results + ) throws DBusExecutionException { + final var sendMessageResults = results.values().stream().findFirst(); + if (results.size() == 1 && sendMessageResults.get().size() == 1) { + checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get()); + return; + } + + var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); + if (errors.size() == 0) { + return; + } + + var message = new StringBuilder(); + message.append(timestamp).append('\n'); + message.append("Failed to send (some) messages:\n"); + for (var error : errors) { + message.append(error).append('\n'); + } + + throw new Error.Failure(message.toString()); + } + + private static void checkSendMessageResults( + long timestamp, Collection results + ) throws DBusExecutionException { + if (results.size() == 1) { + checkSendMessageResult(timestamp, results.stream().findFirst().get()); + return; + } + + var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); + if (errors.size() == 0) { + return; + } + + var message = new StringBuilder(); + message.append(timestamp).append('\n'); + message.append("Failed to send (some) messages:\n"); + for (var error : errors) { + message.append(error).append('\n'); + } + + throw new Error.Failure(message.toString()); + } + + private static Set getSingleRecipientIdentifiers( + final Collection recipientStrings, final String localNumber + ) throws DBusExecutionException { + final var identifiers = new HashSet(); + for (var recipientString : recipientStrings) { + identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber)); + } + return identifiers; + } + + private static RecipientIdentifier.Single getSingleRecipientIdentifier( + final String recipientString, final String localNumber + ) throws DBusExecutionException { + try { + return RecipientIdentifier.Single.fromString(recipientString, localNumber); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException { + try { + return GroupId.unknownVersion(groupId); + } catch (Throwable e) { + throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); + } + } } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index e53b5ca5..814952aa 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; @@ -94,7 +95,7 @@ public class JsonMessageEnvelope { } String name; try { - name = m.getContactOrProfileName(this.source); + name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername())); } catch (InvalidNumberException | NullPointerException e) { name = null; } diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java new file mode 100644 index 00000000..83674876 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -0,0 +1,99 @@ +package org.asamk.signal.util; + +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class CommandUtil { + + private CommandUtil() { + } + + public static Set getRecipientIdentifiers( + final Manager m, + final boolean isNoteToSelf, + final List recipientStrings, + final List groupIdStrings + ) throws UserErrorException { + final var recipientIdentifiers = new HashSet(); + if (isNoteToSelf) { + recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf()); + } + if (recipientStrings != null) { + final var localNumber = m.getUsername(); + recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); + } + if (groupIdStrings != null) { + recipientIdentifiers.addAll(CommandUtil.getGroupIdentifiers(groupIdStrings)); + } + + if (recipientIdentifiers.isEmpty()) { + throw new UserErrorException("No recipients given"); + } + return recipientIdentifiers; + } + + public static Set getGroupIdentifiers(Collection groupIdStrings) throws UserErrorException { + if (groupIdStrings == null) { + return Set.of(); + } + final var groupIds = new HashSet(); + for (final var groupIdString : groupIdStrings) { + groupIds.add(new RecipientIdentifier.Group(getGroupId(groupIdString))); + } + return groupIds; + } + + public static Set getGroupIds(Collection groupIdStrings) throws UserErrorException { + if (groupIdStrings == null) { + return Set.of(); + } + final var groupIds = new HashSet(); + for (final var groupIdString : groupIdStrings) { + groupIds.add(getGroupId(groupIdString)); + } + return groupIds; + } + + public static GroupId getGroupId(String groupId) throws UserErrorException { + if (groupId == null) { + return null; + } + try { + return GroupId.fromBase64(groupId); + } catch (GroupIdFormatException e) { + throw new UserErrorException("Invalid group id: " + e.getMessage()); + } + } + + public static Set getSingleRecipientIdentifiers( + final Collection recipientStrings, final String localNumber + ) throws UserErrorException { + if (recipientStrings == null) { + return Set.of(); + } + final var identifiers = new HashSet(); + for (var recipientString : recipientStrings) { + identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber)); + } + return identifiers; + } + + public static RecipientIdentifier.Single getSingleRecipientIdentifier( + final String recipientString, final String localNumber + ) throws UserErrorException { + try { + return RecipientIdentifier.Single.fromString(recipientString, localNumber); + } catch (InvalidNumberException e) { + throw new UserErrorException("Invalid phone number '" + recipientString + "': " + e.getMessage()); + } + } +} diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 4fd88819..8a3de142 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -2,13 +2,16 @@ package org.asamk.signal.util; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -21,13 +24,27 @@ public class ErrorUtils { } public static void handleSendMessageResults( - List results + Map> mapResults + ) throws CommandException { + List errors = getErrorMessagesFromSendMessageResults(mapResults); + handleSendMessageResultErrors(errors); + } + + public static void handleSendMessageResults( + Collection results ) throws CommandException { var errors = getErrorMessagesFromSendMessageResults(results); handleSendMessageResultErrors(errors); } - public static List getErrorMessagesFromSendMessageResults(List results) { + public static List getErrorMessagesFromSendMessageResults(final Map> mapResults) { + return mapResults.values() + .stream() + .flatMap(results -> getErrorMessagesFromSendMessageResults(results).stream()) + .collect(Collectors.toList()); + } + + public static List getErrorMessagesFromSendMessageResults(Collection results) { var errors = new ArrayList(); for (var result : results) { var error = getErrorMessageFromSendMessageResult(result); diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 0afe0910..01a79dd1 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -61,10 +59,6 @@ public class Util { return f.toString(); } - public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { - return GroupId.fromBase64(groupId); - } - public static String getLegacyIdentifier(final SignalServiceAddress address) { return address.getNumber().or(() -> address.getUuid().get().toString()); }