X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/382d8d22d0ccad109106b140616638799cc646f6..d356d92b5eb24f7340055f766455b943c274bc50:/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index bde5113a..cd65be51 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -1,56 +1,79 @@ package org.asamk.signal.dbus; import org.asamk.Signal; -import org.asamk.signal.DbusConfig; -import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.StickerPackInvalidException; +import org.asamk.signal.manager.api.AlreadyReceivingException; +import org.asamk.signal.manager.api.AttachmentInvalidException; +import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.Configuration; +import org.asamk.signal.manager.api.Contact; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.GroupId; +import org.asamk.signal.manager.api.GroupInviteLinkUrl; +import org.asamk.signal.manager.api.GroupNotFoundException; +import org.asamk.signal.manager.api.GroupPermission; +import org.asamk.signal.manager.api.GroupSendingNotAllowedException; import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.IdentityVerificationCode; import org.asamk.signal.manager.api.InactiveGroupLinkException; +import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; +import org.asamk.signal.manager.api.InvalidStickerException; +import org.asamk.signal.manager.api.InvalidUsernameException; +import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.MessageEnvelope; +import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; +import org.asamk.signal.manager.api.NotAGroupMemberException; +import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.Pair; +import org.asamk.signal.manager.api.PinLockedException; +import org.asamk.signal.manager.api.RateLimitException; +import org.asamk.signal.manager.api.ReceiveConfig; +import org.asamk.signal.manager.api.Recipient; +import org.asamk.signal.manager.api.RecipientAddress; 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.StickerPack; +import org.asamk.signal.manager.api.StickerPackId; +import org.asamk.signal.manager.api.StickerPackInvalidException; +import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UpdateGroup; -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.GroupPermission; -import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; -import org.asamk.signal.manager.groups.LastGroupAdminException; -import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.recipients.Contact; -import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.api.UpdateProfile; +import org.asamk.signal.manager.api.UserStatus; +import org.asamk.signal.manager.api.UsernameLinkUrl; +import org.asamk.signal.manager.api.UsernameStatus; import org.freedesktop.dbus.DBusMap; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.interfaces.DBusSigHandler; import org.freedesktop.dbus.types.Variant; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -68,13 +91,16 @@ public class DbusManagerImpl implements Manager { private final Set weakHandlers = new HashSet<>(); private final Set messageHandlers = new HashSet<>(); private final List closedListeners = new ArrayList<>(); + private final String busname; private DBusSigHandler dbusMsgHandler; + private DBusSigHandler dbusEditMsgHandler; private DBusSigHandler dbusRcptHandler; private DBusSigHandler dbusSyncHandler; - public DbusManagerImpl(final Signal signal, DBusConnection connection) { + public DbusManagerImpl(final Signal signal, DBusConnection connection, final String busname) { this.signal = signal; this.connection = connection; + this.busname = busname; } @Override @@ -83,67 +109,120 @@ public class DbusManagerImpl implements Manager { } @Override - public void checkAccountState() throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public Map> areUsersRegistered(final Set numbers) throws IOException { + public Map getUserStatus(final Set numbers) throws IOException { final var numbersList = new ArrayList<>(numbers); final var registered = signal.isRegistered(numbersList); - final var result = new HashMap>(); + final var result = new HashMap(); for (var i = 0; i < numbersList.size(); i++) { result.put(numbersList.get(i), - new Pair<>(numbersList.get(i), registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null)); + new UserStatus(numbersList.get(i), + registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null, + false)); } return result; } @Override - public void updateAccountAttributes(final String deviceName) throws IOException { + public Map getUsernameStatus(final Set usernames) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateAccountAttributes( + final String deviceName, + final Boolean unrestrictedUnidentifiedSender, + final Boolean discoverableByNumber, + final Boolean numberSharing + ) throws IOException { if (deviceName != null) { final var devicePath = signal.getThisDevice(); getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName); + } else { + throw new UnsupportedOperationException(); } } @Override public Configuration getConfiguration() { + final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"), + Signal.Configuration.class).GetAll("org.asamk.Signal.Configuration"); + return new Configuration(Optional.of((Boolean) configuration.get("ReadReceipts").getValue()), + Optional.of((Boolean) configuration.get("UnidentifiedDeliveryIndicators").getValue()), + Optional.of((Boolean) configuration.get("TypingIndicators").getValue()), + Optional.of((Boolean) configuration.get("LinkPreviews").getValue())); + } + + @Override + public void updateConfiguration(Configuration newConfiguration) { + final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"), + Signal.Configuration.class); + newConfiguration.readReceipts() + .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "ReadReceipts", v)); + newConfiguration.unidentifiedDeliveryIndicators() + .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", + "UnidentifiedDeliveryIndicators", + v)); + newConfiguration.typingIndicators() + .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "TypingIndicators", v)); + newConfiguration.linkPreviews() + .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "LinkPreviews", v)); + } + + @Override + public void updateProfile(UpdateProfile updateProfile) throws IOException { + signal.updateProfile(emptyIfNull(updateProfile.getGivenName()), + emptyIfNull(updateProfile.getFamilyName()), + emptyIfNull(updateProfile.getAbout()), + emptyIfNull(updateProfile.getAboutEmoji()), + updateProfile.getAvatar() == null ? "" : updateProfile.getAvatar(), + updateProfile.isDeleteAvatar()); + } + + @Override + public String getUsername() { throw new UnsupportedOperationException(); } @Override - public void updateConfiguration(Configuration configuration) throws IOException { + public UsernameLinkUrl getUsernameLink() { throw new UnsupportedOperationException(); } @Override - public void setProfile( - final String givenName, - final String familyName, - final String about, - final String aboutEmoji, - final Optional avatar - ) throws IOException { - signal.updateProfile(emptyIfNull(givenName), - emptyIfNull(familyName), - emptyIfNull(about), - emptyIfNull(aboutEmoji), - avatar == null ? "" : avatar.map(File::getPath).orElse(""), - avatar != null && !avatar.isPresent()); + public void setUsername(final String username) throws IOException, InvalidUsernameException { + throw new UnsupportedOperationException(); } @Override - public void unregister() throws IOException { + public void deleteUsername() throws IOException { throw new UnsupportedOperationException(); } @Override - public void deleteAccount() throws IOException { + public void startChangeNumber( + final String newNumber, final boolean voiceVerification, final String captcha + ) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException { throw new UnsupportedOperationException(); } + @Override + public void finishChangeNumber( + final String newNumber, final String verificationCode, final String pin + ) throws IncorrectPinException, PinLockedException, IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void unregister() throws IOException { + signal.unregister(); + } + + @Override + public void deleteAccount() throws IOException { + signal.deleteAccount(); + } + @Override public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException { signal.submitRateLimitChallenge(challenge, captcha); @@ -155,23 +234,23 @@ public class DbusManagerImpl implements Manager { return signal.listDevices().stream().map(d -> { final var device = getRemoteObject(d.getObjectPath(), Signal.Device.class).GetAll("org.asamk.Signal.Device"); - return new Device((long) device.get("Id").getValue(), + return new Device((Integer) device.get("Id").getValue(), (String) device.get("Name").getValue(), (long) device.get("Created").getValue(), (long) device.get("LastSeen").getValue(), thisDevice.equals(d.getObjectPath())); - }).collect(Collectors.toList()); + }).toList(); } @Override - public void removeLinkedDevices(final long deviceId) throws IOException { + public void removeLinkedDevices(final int deviceId) throws IOException { final var devicePath = signal.getDevice(deviceId); getRemoteObject(devicePath, Signal.Device.class).removeDevice(); } @Override - public void addDeviceLink(final URI linkUri) throws IOException, InvalidDeviceLinkException { - signal.addDevice(linkUri.toString()); + public void addDeviceLink(final DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException { + signal.addDevice(linkUri.createDeviceLinkUri().toString()); } @Override @@ -183,41 +262,45 @@ public class DbusManagerImpl implements Manager { } } - @Override - public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) { - throw new UnsupportedOperationException(); - } - @Override public List getGroups() { final var groups = signal.listGroups(); - return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList()); + return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).toList(); } @Override public SendGroupMessageResults quitGroup( final GroupId groupId, final Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - if (groupAdmins.size() > 0) { + if (!groupAdmins.isEmpty()) { throw new UnsupportedOperationException(); } final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); - group.quitGroup(); + try { + group.quitGroup(); + } catch (Signal.Error.GroupNotFound e) { + throw new GroupNotFoundException(groupId); + } catch (Signal.Error.NotAGroupMember e) { + throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name")); + } catch (Signal.Error.LastGroupAdmin e) { + throw new LastGroupAdminException(groupId, group.Get("org.asamk.Signal.Group", "Name")); + } return new SendGroupMessageResults(0, List.of()); } @Override public void deleteGroup(final GroupId groupId) throws IOException { - throw new UnsupportedOperationException(); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.deleteGroup(); } @Override public Pair createGroup( - final String name, final Set members, final File avatarFile + final String name, final Set members, final String avatarFile ) throws IOException, AttachmentInvalidException { final var newGroupId = signal.createGroup(emptyIfNull(name), - members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), - avatarFile == null ? "" : avatarFile.getPath()); + members.stream().map(RecipientIdentifier.Single::getIdentifier).toList(), + avatarFile == null ? "" : avatarFile); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); } @@ -235,7 +318,7 @@ public class DbusManagerImpl implements Manager { if (updateGroup.getAvatarFile() != null) { group.Set("org.asamk.Signal.Group", "Avatar", - updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath()); + updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile()); } if (updateGroup.getExpirationTimer() != null) { group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer()); @@ -254,28 +337,22 @@ public class DbusManagerImpl implements Manager { : GroupPermission.EVERY_MEMBER.name()); } if (updateGroup.getMembers() != null) { - group.addMembers(updateGroup.getMembers() - .stream() - .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList())); + group.addMembers(updateGroup.getMembers().stream().map(RecipientIdentifier.Single::getIdentifier).toList()); } if (updateGroup.getRemoveMembers() != null) { group.removeMembers(updateGroup.getRemoveMembers() .stream() .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList())); + .toList()); } if (updateGroup.getAdmins() != null) { - group.addAdmins(updateGroup.getAdmins() - .stream() - .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList())); + group.addAdmins(updateGroup.getAdmins().stream().map(RecipientIdentifier.Single::getIdentifier).toList()); } if (updateGroup.getRemoveAdmins() != null) { group.removeAdmins(updateGroup.getRemoveAdmins() .stream() .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList())); + .toList()); } if (updateGroup.isResetGroupLink()) { group.resetLink(); @@ -292,8 +369,13 @@ public class DbusManagerImpl implements Manager { @Override public Pair joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException { - final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl()); - return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + try { + final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl()); + return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + } catch (DBusExecutionException e) { + throw new IOException("Failed to join group: " + e.getMessage() + " (" + e.getClass().getSimpleName() + ")", + e); + } } @Override @@ -307,7 +389,8 @@ public class DbusManagerImpl implements Manager { signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP); return 0L; }, groupId -> { - throw new UnsupportedOperationException(); + signal.sendGroupTyping(groupId, action == TypingAction.STOP); + return 0L; }); } @@ -329,7 +412,7 @@ public class DbusManagerImpl implements Manager { @Override public SendMessageResults sendMessage( - final Message message, final Set recipients + final Message message, final Set recipients, final boolean notifySelf ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { return handleMessage(recipients, numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers), @@ -337,6 +420,13 @@ public class DbusManagerImpl implements Manager { groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId)); } + @Override + public SendMessageResults sendEditMessage( + final Message message, final Set recipients, final long editTargetTimestamp + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException { + throw new UnsupportedOperationException(); + } + @Override public SendMessageResults sendRemoteDeleteMessage( final long targetSentTimestamp, final Set recipients @@ -353,7 +443,8 @@ public class DbusManagerImpl implements Manager { final boolean remove, final RecipientIdentifier.Single targetAuthor, final long targetSentTimestamp, - final Set recipients + final Set recipients, + final boolean isStory ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { return handleMessage(recipients, numbers -> signal.sendMessageReaction(emoji, @@ -373,33 +464,65 @@ public class DbusManagerImpl implements Manager { groupId)); } + @Override + public SendMessageResults sendPaymentNotificationMessage( + final byte[] receipt, final String note, final RecipientIdentifier.Single recipient + ) throws IOException { + final var timestamp = signal.sendPaymentNotification(receipt, note, recipient.getIdentifier()); + return new SendMessageResults(timestamp, Map.of()); + } + @Override public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { - signal.sendEndSessionMessage(recipients.stream() - .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList())); + signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList()); return new SendMessageResults(0, Map.of()); } + @Override + public SendMessageResults sendMessageRequestResponse( + final MessageEnvelope.Sync.MessageRequestResponse.Type type, + final Set recipientIdentifiers + ) { + throw new UnsupportedOperationException(); + } + + public void hideRecipient(final RecipientIdentifier.Single recipient) { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteRecipient(final RecipientIdentifier.Single recipient) { + signal.deleteRecipient(recipient.getIdentifier()); + } + + @Override + public void deleteContact(final RecipientIdentifier.Single recipient) { + signal.deleteContact(recipient.getIdentifier()); + } + @Override public void setContactName( - final RecipientIdentifier.Single recipient, final String name - ) throws NotMasterDeviceException { - signal.setContactName(recipient.getIdentifier(), name); + final RecipientIdentifier.Single recipient, final String givenName, final String familyName + ) throws NotPrimaryDeviceException { + signal.setContactName(recipient.getIdentifier(), givenName); } @Override - public void setContactBlocked( - final RecipientIdentifier.Single recipient, final boolean blocked - ) throws NotMasterDeviceException, IOException { - signal.setContactBlocked(recipient.getIdentifier(), blocked); + public void setContactsBlocked( + final Collection recipients, final boolean blocked + ) throws NotPrimaryDeviceException, IOException { + for (final var recipient : recipients) { + signal.setContactBlocked(recipient.getIdentifier(), blocked); + } } @Override - public void setGroupBlocked( - final GroupId groupId, final boolean blocked + public void setGroupsBlocked( + final Collection groupIds, final boolean blocked ) throws GroupNotFoundException, IOException { - setGroupProperty(groupId, "IsBlocked", blocked); + for (final var groupId : groupIds) { + setGroupProperty(groupId, "IsBlocked", blocked); + } } private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) { @@ -415,14 +538,24 @@ public class DbusManagerImpl implements Manager { } @Override - public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException { + public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException { try { - return new URI(signal.uploadStickerPack(path.getPath())); - } catch (URISyntaxException e) { + return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath()))); + } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) { throw new AssertionError(e); } } + @Override + public void installStickerPack(final StickerPackUrl url) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public List getStickerPacks() { + throw new UnsupportedOperationException(); + } + @Override public void requestAllSyncData() throws IOException { signal.sendSyncRequest(); @@ -434,7 +567,7 @@ public class DbusManagerImpl implements Manager { if (isWeakListener) { weakHandlers.add(handler); } else { - if (messageHandlers.size() == 0) { + if (messageHandlers.isEmpty()) { installMessageHandlers(); } messageHandlers.add(handler); @@ -447,7 +580,7 @@ public class DbusManagerImpl implements Manager { synchronized (messageHandlers) { weakHandlers.remove(handler); messageHandlers.remove(handler); - if (messageHandlers.size() == 0) { + if (messageHandlers.isEmpty()) { uninstallMessageHandlers(); } } @@ -456,41 +589,71 @@ public class DbusManagerImpl implements Manager { @Override public boolean isReceiving() { synchronized (messageHandlers) { - return messageHandlers.size() > 0; + return !messageHandlers.isEmpty(); } } - @Override - public void receiveMessages(final ReceiveMessageHandler handler) throws IOException { - addReceiveHandler(handler); - try { - synchronized (this) { - this.wait(); - } - } catch (InterruptedException ignored) { - } - removeReceiveHandler(handler); - } + private Thread receiveThread; @Override public void receiveMessages( - final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler - ) throws IOException { - addReceiveHandler(handler); - try { - Thread.sleep(unit.toMillis(timeout)); - } catch (InterruptedException ignored) { + Optional timeout, Optional maxMessages, ReceiveMessageHandler handler + ) throws IOException, AlreadyReceivingException { + if (receiveThread != null) { + throw new AlreadyReceivingException("Already receiving message."); + } + receiveThread = Thread.currentThread(); + + final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1)); + final var lastMessage = new AtomicLong(System.currentTimeMillis()); + final var thread = Thread.currentThread(); + + final ReceiveMessageHandler receiveHandler = (envelope, e) -> { + lastMessage.set(System.currentTimeMillis()); + handler.handleMessage(envelope, e); + if (remainingMessages.get() > 0) { + if (remainingMessages.decrementAndGet() <= 0) { + remainingMessages.set(0); + thread.interrupt(); + } + } + }; + addReceiveHandler(receiveHandler); + if (timeout.isPresent()) { + while (remainingMessages.get() != 0) { + try { + final var passedTime = System.currentTimeMillis() - lastMessage.get(); + final var sleepTimeRemaining = timeout.get().toMillis() - passedTime; + if (sleepTimeRemaining < 0) { + break; + } + Thread.sleep(sleepTimeRemaining); + } catch (InterruptedException ignored) { + break; + } + } + } else { + try { + synchronized (this) { + this.wait(); + } + } catch (InterruptedException ignored) { + } } - removeReceiveHandler(handler); + + removeReceiveHandler(receiveHandler); + receiveThread = null; } @Override - public void setIgnoreAttachments(final boolean ignoreAttachments) { + public void stopReceiveMessages() { + if (receiveThread != null) { + receiveThread.interrupt(); + } } @Override - public boolean hasCaughtUpWithOldMessages() { - return true; + public void setReceiveConfig(final ReceiveConfig receiveConfig) { } @Override @@ -504,8 +667,44 @@ public class DbusManagerImpl implements Manager { } @Override - public List> getContacts() { - throw new UnsupportedOperationException(); + public List getRecipients( + final boolean onlyContacts, + final Optional blocked, + final Collection addresses, + final Optional name + ) { + final var numbers = addresses.stream() + .filter(s -> s instanceof RecipientIdentifier.Number) + .map(s -> ((RecipientIdentifier.Number) s).number()) + .collect(Collectors.toSet()); + return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> { + final var contactBlocked = signal.isContactBlocked(n); + if (blocked.isPresent() && blocked.get() != contactBlocked) { + return null; + } + final var contactName = signal.getContactName(n); + if (onlyContacts && contactName.isEmpty()) { + return null; + } + if (name.isPresent() && !name.get().equals(contactName)) { + return null; + } + return Recipient.newBuilder() + .withAddress(new RecipientAddress(null, n)) + .withContact(new Contact(contactName, + null, + null, + null, + 0, + 0, + false, + contactBlocked, + false, + false, + false, + null)) + .build(); + }).filter(Objects::nonNull).toList(); } @Override @@ -540,6 +739,9 @@ public class DbusManagerImpl implements Manager { ((List) group.get("Admins").getValue()).stream() .map(m -> new RecipientAddress(null, m)) .collect(Collectors.toSet()), + ((List) group.get("Banned").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), (boolean) group.get("IsBlocked").getValue(), (int) group.get("MessageExpirationTimer").getValue(), GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()), @@ -563,27 +765,19 @@ public class DbusManagerImpl implements Manager { } @Override - public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean trustIdentityVerifiedSafetyNumber( - final RecipientIdentifier.Single recipient, final String safetyNumber + public boolean trustIdentityVerified( + final RecipientIdentifier.Single recipient, final IdentityVerificationCode verificationCode ) { throw new UnsupportedOperationException(); } @Override - public boolean trustIdentityVerifiedSafetyNumber( - final RecipientIdentifier.Single recipient, final byte[] safetyNumber - ) { + public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) { throw new UnsupportedOperationException(); } @Override - public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) { - throw new UnsupportedOperationException(); + public void addAddressChangedListener(final Runnable listener) { } @Override @@ -594,12 +788,12 @@ public class DbusManagerImpl implements Manager { } @Override - public void close() throws IOException { + public void close() { synchronized (this) { this.notify(); } synchronized (messageHandlers) { - if (messageHandlers.size() > 0) { + if (!messageHandlers.isEmpty()) { uninstallMessageHandlers(); } weakHandlers.clear(); @@ -622,8 +816,8 @@ public class DbusManagerImpl implements Manager { .filter(r -> r instanceof RecipientIdentifier.Single) .map(RecipientIdentifier.Single.class::cast) .map(RecipientIdentifier.Single::getIdentifier) - .collect(Collectors.toList()); - if (singleRecipients.size() > 0) { + .toList(); + if (!singleRecipients.isEmpty()) { timestamp = recipientsHandler.apply(singleRecipients); } @@ -634,7 +828,7 @@ public class DbusManagerImpl implements Manager { .filter(r -> r instanceof RecipientIdentifier.Group) .map(RecipientIdentifier.Group.class::cast) .map(RecipientIdentifier.Group::groupId) - .collect(Collectors.toList()); + .toList(); for (final var groupId : groupRecipients) { timestamp = groupHandler.apply(groupId.serialize()); } @@ -645,9 +839,9 @@ public class DbusManagerImpl implements Manager { return string == null ? "" : string; } - private T getRemoteObject(final DBusPath devicePath, final Class type) { + private T getRemoteObject(final DBusPath path, final Class type) { try { - return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type); + return connection.getRemoteObject(busname, path.getPath(), type); } catch (DBusException e) { throw new AssertionError(e); } @@ -672,25 +866,74 @@ public class DbusManagerImpl implements Manager { messageReceived.getGroupId()), false, 0)) : Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of(messageReceived.getMessage()), 0, false, false, false, false, + false, + Optional.empty(), Optional.empty(), Optional.empty(), getAttachments(extras), Optional.empty(), Optional.empty(), List.of(), + getMentions(extras), List.of(), List.of())), Optional.empty(), + Optional.empty(), + Optional.empty(), Optional.empty()); notifyMessageHandlers(envelope); }; connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler); + this.dbusEditMsgHandler = messageReceived -> { + final var extras = messageReceived.getExtras(); + final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null, + messageReceived.getSender())), + 0, + messageReceived.getTimestamp(), + 0, + 0, + false, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of(new MessageEnvelope.Edit(messageReceived.getTargetSentTimestamp(), + new MessageEnvelope.Data(messageReceived.getTimestamp(), + messageReceived.getGroupId().length > 0 + ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion( + messageReceived.getGroupId()), false, 0)) + : Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of(messageReceived.getMessage()), + 0, + false, + false, + false, + false, + false, + Optional.empty(), + Optional.empty(), + Optional.empty(), + getAttachments(extras), + Optional.empty(), + Optional.empty(), + List.of(), + getMentions(extras), + List.of(), + List.of()))), + Optional.empty(), + Optional.empty(), + Optional.empty()); + notifyMessageHandlers(envelope); + }; + connection.addSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler); this.dbusRcptHandler = receiptReceived -> { final var type = switch (receiptReceived.getReceiptType()) { @@ -712,6 +955,8 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), Optional.empty()); notifyMessageHandlers(envelope); }; @@ -729,32 +974,39 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(), syncReceived.getTimestamp(), syncReceived.getDestination().isEmpty() ? Optional.empty() : Optional.of(new RecipientAddress(null, syncReceived.getDestination())), Set.of(), - new MessageEnvelope.Data(syncReceived.getTimestamp(), + Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(), syncReceived.getGroupId().length > 0 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion( syncReceived.getGroupId()), false, 0)) : Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of(syncReceived.getMessage()), 0, false, false, false, false, + false, + Optional.empty(), Optional.empty(), Optional.empty(), getAttachments(extras), Optional.empty(), Optional.empty(), List.of(), + getMentions(extras), List.of(), - List.of()))), + List.of())), + Optional.empty(), + Optional.empty())), Optional.empty(), List.of(), List.of(), @@ -762,21 +1014,21 @@ public class DbusManagerImpl implements Manager { Optional.empty(), Optional.empty(), Optional.empty())), + Optional.empty(), Optional.empty()); notifyMessageHandlers(envelope); }; connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler); } catch (DBusException e) { - e.printStackTrace(); + throw new RuntimeException(e); } signal.subscribeReceive(); } private void notifyMessageHandlers(final MessageEnvelope envelope) { synchronized (messageHandlers) { - Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> { - h.handleMessage(envelope, null); - }); + Stream.concat(messageHandlers.stream(), weakHandlers.stream()) + .forEach(h -> h.handleMessage(envelope, null)); } } @@ -784,10 +1036,11 @@ public class DbusManagerImpl implements Manager { try { signal.unsubscribeReceive(); connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler); + connection.removeSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler); connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler); connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler); } catch (DBusException e) { - e.printStackTrace(); + throw new RuntimeException(e); } } @@ -815,7 +1068,45 @@ public class DbusManagerImpl implements Manager { getValue(a, "isVoiceNote"), getValue(a, "isGif"), getValue(a, "isBorderless")); - }).collect(Collectors.toList()); + }).toList(); + } + + private List getMentions(final Map> extras) { + if (!extras.containsKey("mentions")) { + return List.of(); + } + + final List>> mentions = getValue(extras, "mentions"); + return mentions.stream() + .map(a -> new MessageEnvelope.Data.Mention(new RecipientAddress(null, getValue(a, "recipient")), + getValue(a, "start"), + getValue(a, "length"))) + .toList(); + } + + @Override + public InputStream retrieveAttachment(final String id) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException { + throw new UnsupportedOperationException(); } @SuppressWarnings("unchecked")