package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.signal.BaseConfig; 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; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; 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; import java.util.stream.Stream; import static org.asamk.signal.util.Util.getLegacyIdentifier; public class DbusSignalImpl implements Signal { private final Manager m; private final String objectPath; public DbusSignalImpl(final Manager m, final String objectPath) { this.m = m; this.objectPath = objectPath; } @Override public boolean isRemote() { return false; } @Override public String getObjectPath() { return objectPath; } @Override public long sendMessage(final String message, final List attachments, final String recipient) { var recipients = new ArrayList(1); recipients.add(recipient); return sendMessage(message, attachments, recipients); } @Override public long sendMessage(final String message, final List attachments, final List recipients) { try { 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public long sendRemoteDeleteMessage( final long targetSentTimestamp, final String recipient ) { var recipients = new ArrayList(1); recipients.add(recipient); return sendRemoteDeleteMessage(targetSentTimestamp, recipients); } @Override public long sendRemoteDeleteMessage( final long targetSentTimestamp, final List recipients ) { try { 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 (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public long sendGroupRemoteDeleteMessage( final long targetSentTimestamp, final byte[] groupId ) { try { 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public long sendMessageReaction( final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final String recipient ) { var recipients = new ArrayList(1); recipients.add(recipient); return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients); } @Override public long sendMessageReaction( final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final List recipients ) { try { 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public long sendNoteToSelfMessage( final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public void sendEndSessionMessage(final List recipients) { try { final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); checkSendMessageResults(results.getTimestamp(), results.getResults()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } } @Override public long sendGroupMessageReaction( final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final byte[] groupId ) { try { final var results = m.sendMessageReaction(emoji, remove, getSingleRecipientIdentifier(targetAuthor, m.getUsername()), 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 | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } // Since contact names might be empty if not defined, also potentially return // the profile name @Override public String getContactName(final String number) { return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public void setContactName(final String number, final String name) { try { m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (UnregisteredUserException e) { throw new Error.Failure("Contact is not registered."); } } @Override public void setContactBlocked(final String number, final boolean blocked) { try { m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { m.setGroupBlocked(getGroupId(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public List getGroupIds() { var groups = m.getGroups(); var ids = new ArrayList(groups.size()); for (var group : groups) { ids.add(group.getGroupId().serialize()); } return ids; } @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { return ""; } else { return group.getTitle(); } } @Override public List getGroupMembers(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { return List.of(); } else { return group.getMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { if (groupId.length == 0) { groupId = null; } if (name.isEmpty()) { name = null; } if (avatar.isEmpty()) { avatar = null; } final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); if (groupId == null) { 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(getGroupId(groupId), name, null, memberIdentifiers, null, null, null, false, null, null, null, avatar == null ? null : new File(avatar), null, null); if (results != null) { checkSendMessageResults(results.getTimestamp(), results.getResults()); } return groupId; } } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } } @Override public boolean isRegistered() { return true; } @Override public void updateProfile( final String name, final String about, final String aboutEmoji, String avatarPath, final boolean removeAvatar ) { try { if (avatarPath.isEmpty()) { avatarPath = null; } Optional avatarFile = removeAvatar ? Optional.absent() : avatarPath == null ? null : Optional.of(new File(avatarPath)); m.setProfile(name, null, about, aboutEmoji, avatarFile); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } // Provide option to query a version string in order to react on potential // future interface changes @Override public String version() { return BaseConfig.PROJECT_VERSION; } // Create a unique list of Numbers from Identities and Contacts to really get // all numbers the system knows @Override public List listNumbers() { return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId), m.getContacts().stream().map(Pair::first)) .map(m::resolveSignalServiceAddress) .map(a -> a.getNumber().orNull()) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); } @Override public List getContactNumber(final String name) { // Contact names have precedence. var numbers = new ArrayList(); var contacts = m.getContacts(); for (var c : contacts) { if (name.equals(c.second().getName())) { numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first()))); } } // Try profiles if no contact name was found for (var identity : m.getIdentities()) { final var recipientId = identity.getRecipientId(); final var address = m.resolveSignalServiceAddress(recipientId); var number = address.getNumber().orNull(); if (number != null) { var profile = m.getRecipientProfile(recipientId); if (profile != null && profile.getDisplayName().equals(name)) { numbers.add(number); } } } return numbers; } @Override public void quitGroup(final byte[] groupId) { var group = getGroupId(groupId); try { m.quitGroup(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); } } @Override public byte[] joinGroup(final String groupLink) { try { final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink); if (linkUrl == null) { throw new Error.Failure("Group link is invalid:"); } 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) { throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public boolean isContactBlocked(final String number) { return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public boolean isGroupBlocked(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { return group.isBlocked(); } } @Override public boolean isMember(final byte[] 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()); } } }