X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/2ab42ca5471e8fc1e1a31cde954e19564178f114..06e93b84da2718c31111e820cd35a3354f22bae2:/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 4656975e..5421d4ad 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -6,13 +6,14 @@ 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.UntrustedIdentityException; +import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.InactiveGroupLinkException; import org.asamk.signal.manager.api.InvalidDeviceLinkException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; @@ -29,10 +30,13 @@ 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.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.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.DBusSigHandler; +import org.freedesktop.dbus.types.Variant; import java.io.File; import java.io.IOException; @@ -40,6 +44,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -49,6 +54,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * This class implements the Manager interface using the DBus Signal interface, where possible. @@ -59,6 +65,13 @@ public class DbusManagerImpl implements Manager { private final Signal signal; private final DBusConnection connection; + private final Set weakHandlers = new HashSet<>(); + private final Set messageHandlers = new HashSet<>(); + private final List closedListeners = new ArrayList<>(); + private DBusSigHandler dbusMsgHandler; + private DBusSigHandler dbusRcptHandler; + private DBusSigHandler dbusSyncHandler; + public DbusManagerImpl(final Signal signal, DBusConnection connection) { this.signal = signal; this.connection = connection; @@ -96,12 +109,12 @@ public class DbusManagerImpl implements Manager { } @Override - public void updateConfiguration( - final Boolean readReceipts, - final Boolean unidentifiedDeliveryIndicators, - final Boolean typingIndicators, - final Boolean linkPreviews - ) throws IOException { + public Configuration getConfiguration() { + throw new UnsupportedOperationException(); + } + + @Override + public void updateConfiguration(Configuration configuration) throws IOException { throw new UnsupportedOperationException(); } @@ -118,7 +131,7 @@ public class DbusManagerImpl implements Manager { emptyIfNull(about), emptyIfNull(aboutEmoji), avatar == null ? "" : avatar.map(File::getPath).orElse(""), - avatar != null && !avatar.isPresent()); + avatar != null && avatar.isEmpty()); } @Override @@ -133,7 +146,7 @@ public class DbusManagerImpl implements Manager { @Override public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException { - throw new UnsupportedOperationException(); + signal.submitRateLimitChallenge(challenge, captcha); } @Override @@ -284,31 +297,34 @@ public class DbusManagerImpl implements Manager { } @Override - public void sendTypingMessage( + public SendMessageResults sendTypingMessage( final TypingAction action, final Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(), - action == TypingAction.STOP); - } else if (recipient instanceof RecipientIdentifier.Group) { - throw new UnsupportedOperationException(); - } - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, numbers -> { + numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP)); + return 0L; + }, () -> { + signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP); + return 0L; + }, groupId -> { + throw new UnsupportedOperationException(); + }); } @Override - public void sendReadReceipt( + public SendMessageResults sendReadReceipt( final RecipientIdentifier.Single sender, final List messageIds - ) throws IOException, UntrustedIdentityException { + ) { signal.sendReadReceipt(sender.getIdentifier(), messageIds); + return new SendMessageResults(0, Map.of()); } @Override - public void sendViewedReceipt( + public SendMessageResults sendViewedReceipt( final RecipientIdentifier.Single sender, final List messageIds - ) throws IOException, UntrustedIdentityException { - throw new UnsupportedOperationException(); + ) { + signal.sendViewedReceipt(sender.getIdentifier(), messageIds); + return new SendMessageResults(0, Map.of()); } @Override @@ -365,6 +381,16 @@ public class DbusManagerImpl implements Manager { return new SendMessageResults(0, Map.of()); } + @Override + public void deleteRecipient(final RecipientIdentifier.Single recipient) throws IOException { + signal.deleteRecipient(recipient.getIdentifier()); + } + + @Override + public void deleteContact(final RecipientIdentifier.Single recipient) throws IOException { + signal.deleteContact(recipient.getIdentifier()); + } + @Override public void setContactName( final RecipientIdentifier.Single recipient, final String name @@ -413,40 +439,68 @@ public class DbusManagerImpl implements Manager { } @Override - public void addReceiveHandler(final ReceiveMessageHandler handler) { - throw new UnsupportedOperationException(); + public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) { + synchronized (messageHandlers) { + if (isWeakListener) { + weakHandlers.add(handler); + } else { + if (messageHandlers.size() == 0) { + installMessageHandlers(); + } + messageHandlers.add(handler); + } + } } @Override public void removeReceiveHandler(final ReceiveMessageHandler handler) { - throw new UnsupportedOperationException(); + synchronized (messageHandlers) { + weakHandlers.remove(handler); + messageHandlers.remove(handler); + if (messageHandlers.size() == 0) { + uninstallMessageHandlers(); + } + } } @Override public boolean isReceiving() { - throw new UnsupportedOperationException(); + synchronized (messageHandlers) { + return messageHandlers.size() > 0; + } } @Override public void receiveMessages(final ReceiveMessageHandler handler) throws IOException { - throw new UnsupportedOperationException(); + addReceiveHandler(handler); + try { + synchronized (this) { + this.wait(); + } + } catch (InterruptedException ignored) { + } + removeReceiveHandler(handler); } @Override public void receiveMessages( final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler ) throws IOException { - throw new UnsupportedOperationException(); + addReceiveHandler(handler); + try { + Thread.sleep(unit.toMillis(timeout)); + } catch (InterruptedException ignored) { + } + removeReceiveHandler(handler); } @Override public void setIgnoreAttachments(final boolean ignoreAttachments) { - throw new UnsupportedOperationException(); } @Override public boolean hasCaughtUpWithOldMessages() { - throw new UnsupportedOperationException(); + return true; } @Override @@ -454,11 +508,6 @@ public class DbusManagerImpl implements Manager { return signal.isContactBlocked(recipient.getIdentifier()); } - @Override - public File getAttachmentFile(final String attachmentId) { - throw new UnsupportedOperationException(); - } - @Override public void sendContacts() throws IOException { signal.sendContacts(); @@ -547,8 +596,29 @@ public class DbusManagerImpl implements Manager { throw new UnsupportedOperationException(); } + @Override + public void addClosedListener(final Runnable listener) { + synchronized (closedListeners) { + closedListeners.add(listener); + } + } + @Override public void close() throws IOException { + synchronized (this) { + this.notify(); + } + synchronized (messageHandlers) { + if (messageHandlers.size() > 0) { + uninstallMessageHandlers(); + } + weakHandlers.clear(); + messageHandlers.clear(); + } + synchronized (closedListeners) { + closedListeners.forEach(Runnable::run); + closedListeners.clear(); + } } private SendMessageResults handleMessage( @@ -592,4 +662,177 @@ public class DbusManagerImpl implements Manager { throw new AssertionError(e); } } + + private void installMessageHandlers() { + try { + this.dbusMsgHandler = 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.of(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.of(messageReceived.getMessage()), + 0, + false, + false, + false, + false, + Optional.empty(), + Optional.empty(), + Optional.empty(), + getAttachments(extras), + Optional.empty(), + Optional.empty(), + List.of(), + List.of(), + List.of())), + Optional.empty(), + Optional.empty()); + notifyMessageHandlers(envelope); + }; + connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler); + + this.dbusRcptHandler = receiptReceived -> { + final var type = switch (receiptReceived.getReceiptType()) { + case "read" -> MessageEnvelope.Receipt.Type.READ; + case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED; + case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY; + default -> MessageEnvelope.Receipt.Type.UNKNOWN; + }; + final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null, + receiptReceived.getSender())), + 0, + receiptReceived.getTimestamp(), + 0, + 0, + false, + Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(), + type, + List.of(receiptReceived.getTimestamp()))), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + notifyMessageHandlers(envelope); + }; + connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler); + + this.dbusSyncHandler = syncReceived -> { + final var extras = syncReceived.getExtras(); + final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null, + syncReceived.getSource())), + 0, + syncReceived.getTimestamp(), + 0, + 0, + false, + 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(), + syncReceived.getGroupId().length > 0 + ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion( + syncReceived.getGroupId()), false, 0)) + : Optional.empty(), + Optional.empty(), + Optional.of(syncReceived.getMessage()), + 0, + false, + false, + false, + false, + Optional.empty(), + Optional.empty(), + Optional.empty(), + getAttachments(extras), + Optional.empty(), + Optional.empty(), + List.of(), + List.of(), + List.of()))), + Optional.empty(), + List.of(), + List.of(), + 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(); + } + signal.subscribeReceive(); + } + + private void notifyMessageHandlers(final MessageEnvelope envelope) { + synchronized (messageHandlers) { + Stream.concat(messageHandlers.stream(), weakHandlers.stream()) + .forEach(h -> h.handleMessage(envelope, null)); + } + } + + private void uninstallMessageHandlers() { + try { + signal.unsubscribeReceive(); + connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler); + connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler); + connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler); + } catch (DBusException e) { + e.printStackTrace(); + } + } + + private List getAttachments(final Map> extras) { + if (!extras.containsKey("attachments")) { + return List.of(); + } + + final List>> attachments = getValue(extras, "attachments"); + return attachments.stream().map(a -> { + final String file = a.containsKey("file") ? getValue(a, "file") : null; + return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId") + ? Optional.of(getValue(a, "remoteId")) + : Optional.empty(), + file != null ? Optional.of(new File(file)) : Optional.empty(), + Optional.empty(), + getValue(a, "contentType"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + getValue(a, "isVoiceNote"), + getValue(a, "isGif"), + getValue(a, "isBorderless")); + }).collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private T getValue( + final Map> stringVariantMap, final String field + ) { + return (T) stringVariantMap.get(field).getValue(); + } }