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;
import org.asamk.signal.manager.api.SendMessageResults;
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.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.util.Pair;
-import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
-import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
-import org.whispersystems.signalservice.api.util.UuidUtil;
-import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
+import org.freedesktop.dbus.interfaces.DBusSigHandler;
+import org.freedesktop.dbus.types.Variant;
import java.io.File;
import java.io.IOException;
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;
import java.util.Set;
import java.util.UUID;
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.
private final Signal signal;
private final DBusConnection connection;
+ private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
+ private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
+ private DBusSigHandler<Signal.MessageReceivedV2> dbusMsgHandler;
+ private DBusSigHandler<Signal.ReceiptReceivedV2> dbusRcptHandler;
+ private DBusSigHandler<Signal.SyncMessageReceivedV2> dbusSyncHandler;
+
public DbusManagerImpl(final Signal signal, DBusConnection connection) {
this.signal = signal;
this.connection = connection;
final var result = new HashMap<String, Pair<String, UUID>>();
for (var i = 0; i < numbersList.size(); i++) {
result.put(numbersList.get(i),
- new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null));
+ new Pair<>(numbersList.get(i), registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null));
}
return result;
}
}
@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();
}
emptyIfNull(familyName),
emptyIfNull(about),
emptyIfNull(aboutEmoji),
- avatar == null ? "" : avatar.transform(File::getPath).or(""),
+ avatar == null ? "" : avatar.map(File::getPath).orElse(""),
avatar != null && !avatar.isPresent());
}
@Override
public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
- throw new UnsupportedOperationException();
+ signal.submitRateLimitChallenge(challenge, captcha);
}
@Override
}
@Override
- public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
+ public void addDeviceLink(final URI linkUri) throws IOException, InvalidDeviceLinkException {
signal.addDevice(linkUri.toString());
}
@Override
- public void setRegistrationLockPin(final Optional<String> pin) throws IOException, UnauthenticatedResponseException {
+ public void setRegistrationLockPin(final Optional<String> pin) throws IOException {
if (pin.isPresent()) {
signal.setPin(pin.get());
} else {
}
@Override
- public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
+ public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) {
throw new UnsupportedOperationException();
}
}
if (updateGroup.getGroupLinkState() != null) {
switch (updateGroup.getGroupLinkState()) {
- case DISABLED:
- group.disableLink();
- break;
- case ENABLED:
- group.enableLink(false);
- break;
- case ENABLED_WITH_APPROVAL:
- group.enableLink(true);
- break;
+ case DISABLED -> group.disableLink();
+ case ENABLED -> group.enableLink(false);
+ case ENABLED_WITH_APPROVAL -> group.enableLink(true);
}
}
return new SendGroupMessageResults(0, List.of());
}
@Override
- public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
+ public Pair<GroupId, SendGroupMessageResults> 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()));
}
public void sendViewedReceipt(
final RecipientIdentifier.Single sender, final List<Long> messageIds
) throws IOException, UntrustedIdentityException {
- throw new UnsupportedOperationException();
+ signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
}
@Override
final Message message, final Set<RecipientIdentifier> recipients
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(recipients,
- numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
- () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
- groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
+ numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
+ () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
+ groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
}
@Override
@Override
public void setContactName(
final RecipientIdentifier.Single recipient, final String name
- ) throws NotMasterDeviceException, UnregisteredUserException {
+ ) throws NotMasterDeviceException {
signal.setContactName(recipient.getIdentifier(), name);
}
signal.sendSyncRequest();
}
+ @Override
+ 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) {
+ synchronized (messageHandlers) {
+ weakHandlers.remove(handler);
+ messageHandlers.remove(handler);
+ if (messageHandlers.size() == 0) {
+ uninstallMessageHandlers();
+ }
+ }
+ }
+
+ @Override
+ public boolean isReceiving() {
+ synchronized (messageHandlers) {
+ return messageHandlers.size() > 0;
+ }
+ }
+
+ @Override
+ public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
+ addReceiveHandler(handler);
+ try {
+ synchronized (this) {
+ this.wait();
+ }
+ } catch (InterruptedException ignored) {
+ }
+ removeReceiveHandler(handler);
+ }
+
@Override
public void receiveMessages(
- final long timeout,
- final TimeUnit unit,
- final boolean returnOnTimeout,
- final boolean ignoreAttachments,
- final ReceiveMessageHandler handler
+ 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 boolean hasCaughtUpWithOldMessages() {
- throw new UnsupportedOperationException();
+ public void setIgnoreAttachments(final boolean ignoreAttachments) {
}
@Override
- public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
- return signal.isContactBlocked(recipient.getIdentifier());
+ public boolean hasCaughtUpWithOldMessages() {
+ return true;
}
@Override
- public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
- throw new UnsupportedOperationException();
+ public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
+ return signal.isContactBlocked(recipient.getIdentifier());
}
@Override
throw new UnsupportedOperationException();
}
- @Override
- public String computeSafetyNumber(
- final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
- ) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
- return address;
- }
-
@Override
public void close() throws IOException {
+ synchronized (this) {
+ this.notify();
+ }
+ synchronized (messageHandlers) {
+ if (messageHandlers.size() > 0) {
+ uninstallMessageHandlers();
+ }
+ weakHandlers.clear();
+ messageHandlers.clear();
+ }
}
private SendMessageResults handleMessage(
final var groupRecipients = recipients.stream()
.filter(r -> r instanceof RecipientIdentifier.Group)
.map(RecipientIdentifier.Group.class::cast)
- .map(g -> g.groupId)
+ .map(RecipientIdentifier.Group::groupId)
.collect(Collectors.toList());
for (final var groupId : groupRecipients) {
timestamp = groupHandler.apply(groupId.serialize());
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(),
+ 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(),
+ 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<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
+ if (!extras.containsKey("attachments")) {
+ return List.of();
+ }
+
+ final List<DBusMap<String, Variant<?>>> 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> T getValue(
+ final Map<String, Variant<?>> stringVariantMap, final String field
+ ) {
+ return (T) stringVariantMap.get(field).getValue();
+ }
}