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.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; 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.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.util.InvalidNumberException; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; 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; public DbusSignalImpl(final Manager m) { this.m = m; } @Override public boolean isRemote() { return false; } @Override public String getObjectPath() { return null; } @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); } 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(message, attachments, recipients); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(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, recipients); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @Override public long sendGroupRemoteDeleteMessage( final long targetSentTimestamp, final byte[] groupId ) { try { final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException 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, targetAuthor, targetSentTimestamp, recipients); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public long sendNoteToSelfMessage( final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { final var results = m.sendSelfMessage(message, attachments); checkSendMessageResult(results.first(), results.second()); return results.first(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public void sendEndSessionMessage(final List recipients) { try { final var results = m.sendEndSessionMessage(recipients); checkSendMessageResults(results.first(), results.second()); } 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(message, attachments, GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException 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.sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } 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()); } } // Since contact names might be empty if not defined, also potentially return // the profile name @Override public String getContactName(final String number) { try { return m.getContactOrProfileName(number); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @Override public void setContactName(final String number, final String name) { try { m.setContactName(number, name); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } } @Override public void setContactBlocked(final String number, final boolean blocked) { try { m.setContactBlocked(number, blocked); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } } @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(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(GroupId.unknownVersion(groupId)); if (group == null) { return ""; } else { return group.getTitle(); } } @Override public List getGroupMembers(final byte[] groupId) { var group = m.getGroup(GroupId.unknownVersion(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 (members.isEmpty()) { members = null; } if (avatar.isEmpty()) { avatar = null; } if (groupId == null) { final var results = m.createGroup(name, members, avatar == null ? null : new File(avatar)); checkSendMessageResults(0, results.second()); return results.first().serialize(); } else { final var results = m.updateGroup(GroupId.unknownVersion(groupId), name, null, members, null, null, null, false, null, null, null, avatar == null ? null : new File(avatar), null); if (results != null) { checkSendMessageResults(results.first(), results.second()); } return groupId; } } catch (IOException e) { 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()); } } @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 = GroupId.unknownVersion(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) { try { final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink); if (linkUrl == null) { throw new Error.Failure("Group link is invalid:"); } m.joinGroup(linkUrl); } 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) { try { return m.isContactBlocked(number); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @Override public boolean isGroupBlocked(final byte[] groupId) { var group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return false; } else { return group.isBlocked(); } } @Override public boolean isMember(final byte[] groupId) { var group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { return false; } else { return group.isMember(m.getSelfRecipientId()); } } }