package org.asamk.signal.dbus; import org.asamk.Signal; import org.asamk.SignalControl; import org.asamk.SignalControl.Error; import org.asamk.signal.BaseConfig; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.PlainTextWriterImpl; import org.asamk.signal.commands.GetUserStatusCommand; import org.asamk.signal.commands.UpdateGroupCommand; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; 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.manager.AttachmentInvalidException; import org.asamk.signal.manager.AvatarStore; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.api.Device; 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.GroupInviteLinkUrl; 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.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Base64; import java.util.HashSet; import java.util.List; import java.util.Locale; 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; private final static Logger logger = LoggerFactory.getLogger(DbusSignalImpl.class); 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 void updateAccount() { try { m.updateAccountAttributes(); } catch (IOException | Error.Failure e) { throw new Error.Failure("UpdateAccount error: " + e.getMessage()); } } @Override public List listIdentity(String number) { List identities; IdentityInfo theirId; try { identities = m.getIdentities(number); } catch (InvalidNumberException e) { throw new Error.InvalidNumber("Invalid number: " + e.getMessage()); } List results = new ArrayList(); if (identities.isEmpty()) {return results;} theirId = identities.get(0); final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId()); var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey())); results.add(theirId.getTrustLevel().toString()); results.add(theirId.getDateAdded().toString()); results.add(Hex.toString(theirId.getFingerprint())); results.add(digits); return results; } @Override public List listDevices() { List devices; List results = new ArrayList(); try { devices = m.getLinkedDevices(); } catch (IOException | Error.Failure e) { throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); } for (var d : devices) { var name = d.getName(); if (name == null) {name = "null";} results.add(name); } return results; } @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 { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, group.getGroupId()); 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 ) { 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 { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } var results = m.sendGroupMessage(message, attachments, group.getGroupId()); 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 sendGroupMessage(final String message, final List attachments, final String base64GroupId) { try { byte[] groupId = GroupId.fromBase64(base64GroupId).serialize(); return sendGroupMessage(message, attachments, groupId); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + e.getMessage()); } } @Override public long sendGroupMessageReaction( final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final byte[] groupId ) { try { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } final var results = m.sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, group.getGroupId()); 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()); } } @Override public long sendGroupMessageReaction( final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final String base64GroupId ) { try { byte[] groupId = GroupId.fromBase64(base64GroupId).serialize(); return sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + e.getMessage()); } } @Override public void sendContacts() { try { m.sendContacts(); } catch (UntrustedIdentityException e) { throw new Error.UntrustedIdentity("SendContacts error: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure("SendContacts error: " + e.getMessage()); } } @Override public void sendSyncRequest() { try { m.requestAllSyncData(); } catch (IOException e) { throw new Error.Failure("Request sync data error: " + e.getMessage()); } } @Override public void trust(String number, String safetyNumber){ if (safetyNumber != null) { safetyNumber = safetyNumber.replaceAll(" ", ""); if (safetyNumber.length() == 66) { byte[] fingerprintBytes; try { fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT)); } catch (Exception e) { throw new Error.Failure( "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 Error.Failure("Failed to parse recipient: " + e.getMessage()); } if (!res) { throw new Error.Failure( "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 Error.InvalidNumber("Failed to parse recipient: " + e.getMessage()); } if (!res) { throw new Error.Failure( "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); } } else { throw new Error.Failure( "Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); } } else { throw new Error.Failure( "You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER"); } } @Override public void sendTyping(boolean typingAction, String base64GroupId, Listrecipients) { final var noRecipients = recipients == null || recipients.isEmpty(); final var noGroup = base64GroupId == null || base64GroupId.isEmpty(); if (noRecipients && noGroup) { throw new Error.Failure("No recipients given"); } if (!noRecipients && !noGroup) { throw new Error.Failure("You cannot specify recipients by phone number and groups at the same time"); } final TypingAction action = typingAction ? TypingAction.START : TypingAction.STOP; GroupId groupId = null; if (!noGroup) { try { groupId = Util.decodeGroupId(base64GroupId); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id: " + e.getMessage()); } } try { if (groupId != null) { m.sendGroupTypingMessage(action, groupId); } else { m.sendTypingMessage(action, new HashSet(recipients)); } } catch (UntrustedIdentityException e) { throw new Error.UntrustedIdentity("Failed to send message: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.Failure("Failed to send to group: " + e.getMessage()); } catch (InvalidNumberException e) { throw new Error.Failure("Invalid number: " + e.getMessage()); } } @Override public void sendTyping(boolean typingAction, byte[] groupId, Listrecipients) { String base64GroupId = Base64.getEncoder().encodeToString(groupId); sendTyping(typingAction, base64GroupId, recipients); } // 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 setExpirationTimer(final String number, final int expiration) { try { m.setExpirationTimer(number, expiration); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @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) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } try { m.setGroupBlocked(group.getGroupId(), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } } @Override public void setGroupBlocked(final String base64GroupId, final boolean blocked) { try { byte[] groupId = GroupId.fromBase64(base64GroupId).serialize(); setGroupBlocked(groupId, blocked); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + 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) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getTitle(); } } @Override public List getGroupMembers(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } // To get the group Ids in base 64 format, either use the getBaseGroupIds() method, or // the getGroupIds(dummy) form, where dummy represents any string @Override public List getBase64GroupIds() { var groups = m.getGroups(); var ids = new ArrayList(groups.size()); for (var group : groups) { ids.add(group.getGroupId().toBase64()); } return ids; } @Override public List getGroupIds(String dummy) { var groups = m.getGroups(); var ids = new ArrayList(groups.size()); for (var group : groups) { ids.add(group.getGroupId().toBase64()); } return ids; } @Override public String getGroupName(final String base64GroupId) { byte[] groupId = null; GroupInfo group = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure(e.getMessage()); } try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getTitle(); } } @Override public List getGroupMembers(final String base64GroupId) { byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + e.getMessage()); } GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } 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 { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } final var results = m.updateGroup(group.getGroupId(), 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 String updateGroup(String base64GroupId, String name, List members, String avatar) { byte[] groupId = null; if (!base64GroupId.isEmpty()) { try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + e.getMessage()); } } groupId = updateGroup(groupId, name, members, avatar); return Base64.getEncoder().encodeToString(groupId); } @Override public byte[] updateGroup( byte[] groupId, String name, String description, List addMembers, List removeMembers, List addAdmins, List removeAdmins, boolean resetGroupLink, String groupLinkState, String addMemberPermission, String editDetailsPermission, String avatar, Integer expirationTimer ) { try { File avatarFile = null; if (name.isEmpty()) { name = null; } if (description.isEmpty()) { description= null; } if (addMembers.isEmpty()) { addMembers = null; } if (removeMembers.isEmpty()) { removeMembers = null; } if (addAdmins.isEmpty()) { addAdmins = null; } if (removeAdmins.isEmpty()) { removeAdmins = null; } if (groupLinkState.isEmpty()) { groupLinkState = null; } if (addMemberPermission.isEmpty()) { addMemberPermission = null; } if (editDetailsPermission.isEmpty()) { editDetailsPermission = null; } if (avatar.isEmpty()) { avatarFile = null; } else { avatarFile = new File(avatar); //check if we are sending an empty file. If so, this tells Signal // to delete the avatar, and we will delete it from the local AvatarStore long fileSize = avatarFile.length(); if (fileSize == 0) { try { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError ae) { throw new Error.Failure(ae.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } AvatarStore.deleteGroupAvatar(group.getGroupId()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } } if (groupId == null) { final var results = m.createGroup(name, addMembers, avatar == null ? null : new File(avatar)); checkSendMessageResults(0, results.second()); return results.first().serialize(); } else { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } final var results = m.updateGroup(group.getGroupId(), name, description, addMembers, removeMembers, addAdmins, removeAdmins, resetGroupLink, groupLinkState == null ? null : UpdateGroupCommand.getGroupLinkState(groupLinkState), addMemberPermission == null ? null : UpdateGroupCommand.getGroupPermission(addMemberPermission), editDetailsPermission == null ? null : UpdateGroupCommand.getGroupPermission(editDetailsPermission), avatarFile, expirationTimer ); if (results != null) { checkSendMessageResults(results.first(), results.second()); } return groupId; } } catch (UserErrorException | 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 String updateGroup( String base64GroupId, String name, String description, List addMembers, List removeMembers, List addAdmins, List removeAdmins, boolean resetGroupLink, String groupLinkState, String addMemberPermission, String editDetailsPermission, String avatar, Integer expirationTimer ) { byte[] groupId = null; if (!base64GroupId.isEmpty()) { try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Invalid group id format: " + e.getMessage()); } } groupId = updateGroup( groupId, name, description, addMembers, removeMembers, addAdmins, removeAdmins, resetGroupLink, groupLinkState, addMemberPermission, editDetailsPermission, avatar, expirationTimer ); return Base64.getEncoder().encodeToString(groupId); } @Override public String getGroupInviteUri(byte[] groupId) { GroupInfo group = null; GroupInviteLinkUrl groupInviteUri = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } groupInviteUri = group.getGroupInviteLink(); if (groupInviteUri == null) { return ""; } return groupInviteUri.getUrl(); } @Override public String getGroupInviteUri(String base64GroupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.fromBase64(base64GroupId)); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } GroupInviteLinkUrl groupInviteUri = group.getGroupInviteLink(); return groupInviteUri.getUrl(); } @Override public List getGroupPendingMembers(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getPendingMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public List getGroupPendingMembers(final String base64GroupId) { byte [] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getPendingMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public List getGroupRequestingMembers(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getRequestingMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public List getGroupRequestingMembers(final String base64GroupId) { byte [] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getRequestingMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public List getGroupAdminMembers(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getAdminMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public List getGroupAdminMembers(final String base64GroupId) { byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("base64GroupId" + " is not a valid Base 64 string " + e.getMessage()); } GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getAdminMembers() .stream() .map(m::resolveSignalServiceAddress) .map(Util::getLegacyIdentifier) .collect(Collectors.toList()); } } @Override public boolean isRegistered(String number) { if (number.isEmpty()) { return false; } try { Map registered; List numbers = new ArrayList(); numbers.add(number); registered = m.areUsersRegistered(new HashSet(numbers)); return registered.get(number); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @Override public List isRegistered(List numbers) { List results = new ArrayList (); if (numbers.isEmpty()) { return results; } try { Map registered; registered = m.areUsersRegistered(new HashSet(numbers)); for (String number : numbers) { results.add(registered.get(number)); } return results; } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (InvalidNumberException e) { throw new Error.InvalidNumber(e.getMessage()); } } @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()); } } @Override public void updateProfile( final String givenName, final String familyName, 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(givenName, familyName, about, aboutEmoji, avatarFile); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } } @Override public void removePin() { try { m.setRegistrationLockPin(Optional.absent()); } catch (UnauthenticatedResponseException e) { throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure("Remove pin error: " + e.getMessage()); } } @Override public void setPin(String registrationLockPin) { try { m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (UnauthenticatedResponseException e) { throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage()); } catch (IOException e) { throw new Error.Failure("Set pin error: " + e.getMessage()); } } // Provide option to query a version string in order to react on potential // future interface changes @Override public String version() { return DbusSignalControlImpl.version(); } @Override public String link() { return DbusSignalControlImpl.link(); } @Override public String link(String newDeviceName) { return DbusSignalControlImpl.link(newDeviceName); } @Override public void addDevice(String uri) { try { m.addDeviceLink(new URI(uri)); } catch (IOException | InvalidKeyException e) { throw new Error.Failure(e.getClass().getSimpleName() + "Add device link failed. " + e.getMessage()); } catch (URISyntaxException e) { throw new Error.Failure(e.getClass().getSimpleName() + "Device link uri has invalid format: " + e.getMessage()); } } @Override public void removeDevice(int deviceId) { try { m.removeLinkedDevices(deviceId); } catch (IOException e) { throw new Error.Failure(e.getClass().getSimpleName() + "Error while removing device: " + e.getMessage()); } } @Override public void register( String number, boolean voiceVerification ) { DbusSignalControlImpl.register(number, voiceVerification); } @Override public void registerWithCaptcha( String number, boolean voiceVerification, String captcha ) { DbusSignalControlImpl.registerWithCaptcha(number, voiceVerification, captcha); } @Override public void unregister() { try { m.unregister(); logger.info("Unregister succeeded, exiting.\n"); System.exit(0); } catch (IOException e) { throw new Error.Failure(e.getClass().getSimpleName() + "Unregister error: " + e.getMessage()); } } @Override public void verify(String number, String verificationCode) { DbusSignalControlImpl.verify(number, verificationCode); } @Override public void verifyWithPin(String number, String verificationCode, String pin) { DbusSignalControlImpl.verifyWithPin(number, verificationCode, pin); } // 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) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } try { m.sendQuitGroupMessage(group.getGroupId(), 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 quitGroup(final String base64GroupId) { byte [] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } try { m.sendQuitGroupMessage(group.getGroupId(), 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) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.isBlocked(); } } @Override public boolean isGroupBlocked(final String base64GroupId) { GroupInfo group = null; byte [] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.isBlocked(); } } @Override public boolean isMember(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.isMember(m.getSelfRecipientId()); } } @Override public boolean isMember(final String base64GroupId) { GroupInfo group = null; byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.isMember(m.getSelfRecipientId()); } } @Override public List isMember(final byte[] groupId, Listmembers, boolean setMemberStatus) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { throw new Error.GroupNotFound("Error finding group"); } if (setMemberStatus) { var results = m.updateGroup(group.getGroupId(), null, null, members, null, null, null, false, null, null, null, null, null); } else { var results = m.updateGroup(group.getGroupId(), null, null, null, members, null, null, false, null, null, null, null, null); } return getGroupMembers(groupId); } catch (NotAGroupMemberException | InvalidNumberException | AssertionError | IOException | AttachmentInvalidException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound("Group not found."); } } @Override public List isMember(final String base64GroupId, List members, boolean setMemberStatus) { GroupInfo group = null; byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); return isMember(groupId, members, setMemberStatus); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } } @Override public boolean isAdmin(final byte[] groupId) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getAdminMembers().contains(m.getSelfRecipientId()); } } @Override public boolean isAdmin(final String base64GroupId) { GroupInfo group = null; byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } try { group = m.getGroup(GroupId.unknownVersion(groupId)); } catch (AssertionError e) { throw new Error.Failure(e.getMessage()); } if (group == null) { throw new Error.GroupNotFound("Error finding group"); } else { return group.getAdminMembers().contains(m.getSelfRecipientId()); } } @Override public List isAdmin(final byte[] groupId, Listadmins, boolean setAdminStatus) { GroupInfo group = null; try { group = m.getGroup(GroupId.unknownVersion(groupId)); if (group == null) { throw new Error.GroupNotFound("Error finding group"); } if (setAdminStatus) { var results = m.updateGroup(group.getGroupId(), null, null, null, null, admins, null, false, null, null, null, null, null); } else { var results = m.updateGroup(group.getGroupId(), null, null, null, null, null, admins, false, null, null, null, null, null); } return getGroupAdminMembers(groupId); } catch (NotAGroupMemberException | InvalidNumberException | AssertionError | IOException | AttachmentInvalidException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound("Group not found."); } } @Override public List isAdmin(final String base64GroupId, List admins, boolean setAdminStatus) { GroupInfo group = null; byte[] groupId = null; try { groupId = GroupId.fromBase64(base64GroupId).serialize(); return isAdmin(groupId, admins, setAdminStatus); } catch (GroupIdFormatException e) { throw new Error.Failure("Incorrect format: " + e.getMessage()); } } @Override public void uploadStickerPack(String stickerPackPath) { File path = new File(stickerPackPath); try { var url = m.uploadStickerPack(path); } catch (IOException e) { throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage()); } catch (StickerPackInvalidException e) { throw new Error.Failure("Invalid sticker pack: " + e.getMessage()); } } }