X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/778adacb80bae7d6ecc1d70fa87f9217c7bc1c71..82bb4f22f07bae40ff42570d0ada81125adf4392:/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 3124a5b0..c8dd1d45 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -7,17 +7,22 @@ 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; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +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.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -26,20 +31,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.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; @@ -47,8 +45,10 @@ 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; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -65,6 +65,11 @@ public class DbusManagerImpl implements Manager { private final Signal signal; private final DBusConnection connection; + private final Set messageHandlers = new HashSet<>(); + private DBusSigHandler dbusMsgHandler; + private DBusSigHandler dbusRcptHandler; + private DBusSigHandler dbusSyncHandler; + public DbusManagerImpl(final Signal signal, DBusConnection connection) { this.signal = signal; this.connection = connection; @@ -88,7 +93,7 @@ public class DbusManagerImpl implements Manager { 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) ? UuidUtil.UNKNOWN_UUID : null)); + new Pair<>(numbersList.get(i), registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null)); } return result; } @@ -102,12 +107,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(); } @@ -123,7 +128,7 @@ public class DbusManagerImpl implements Manager { emptyIfNull(familyName), emptyIfNull(about), emptyIfNull(aboutEmoji), - avatar == null ? "" : avatar.transform(File::getPath).or(""), + avatar == null ? "" : avatar.map(File::getPath).orElse(""), avatar != null && !avatar.isPresent()); } @@ -139,19 +144,20 @@ 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 public List getLinkedDevices() throws IOException { final var thisDevice = signal.getThisDevice(); - return signal.listDevices().stream().map(devicePath -> { - final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device"); + 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(), (String) device.get("Name").getValue(), (long) device.get("Created").getValue(), (long) device.get("LastSeen").getValue(), - thisDevice.equals(devicePath)); + thisDevice.equals(d.getObjectPath())); }).collect(Collectors.toList()); } @@ -162,12 +168,12 @@ public class DbusManagerImpl implements Manager { } @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 pin) throws IOException, UnauthenticatedResponseException { + public void setRegistrationLockPin(final Optional pin) throws IOException { if (pin.isPresent()) { signal.setPin(pin.get()); } else { @@ -176,14 +182,14 @@ public class DbusManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) { throw new UnsupportedOperationException(); } @Override public List getGroups() { - final var groupIds = signal.getGroupIds(); - return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + final var groups = signal.listGroups(); + return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList()); } @Override @@ -193,7 +199,8 @@ public class DbusManagerImpl implements Manager { if (groupAdmins.size() > 0) { throw new UnsupportedOperationException(); } - signal.quitGroup(groupId.serialize()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.quitGroup(); return new SendGroupMessageResults(0, List.of()); } @@ -206,8 +213,7 @@ public class DbusManagerImpl implements Manager { public Pair createGroup( final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { - final var newGroupId = signal.updateGroup(new byte[0], - emptyIfNull(name), + final var newGroupId = signal.createGroup(emptyIfNull(name), members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), avatarFile == null ? "" : avatarFile.getPath()); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); @@ -215,30 +221,75 @@ public class DbusManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - signal.updateGroup(groupId.serialize(), - emptyIfNull(name), - members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), - avatarFile == null ? "" : avatarFile.getPath()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + if (updateGroup.getName() != null) { + group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName()); + } + if (updateGroup.getDescription() != null) { + group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription()); + } + if (updateGroup.getAvatarFile() != null) { + group.Set("org.asamk.Signal.Group", + "Avatar", + updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath()); + } + if (updateGroup.getExpirationTimer() != null) { + group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer()); + } + if (updateGroup.getAddMemberPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name()); + } + if (updateGroup.getEditDetailsPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name()); + } + if (updateGroup.getIsAnnouncementGroup() != null) { + group.Set("org.asamk.Signal.Group", + "PermissionSendMessage", + updateGroup.getIsAnnouncementGroup() + ? GroupPermission.ONLY_ADMINS.name() + : GroupPermission.EVERY_MEMBER.name()); + } + if (updateGroup.getMembers() != null) { + group.addMembers(updateGroup.getMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveMembers() != null) { + group.removeMembers(updateGroup.getRemoveMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getAdmins() != null) { + group.addAdmins(updateGroup.getAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveAdmins() != null) { + group.removeAdmins(updateGroup.getRemoveAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.isResetGroupLink()) { + group.resetLink(); + } + if (updateGroup.getGroupLinkState() != null) { + switch (updateGroup.getGroupLinkState()) { + 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 joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException { + 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())); } @@ -268,7 +319,7 @@ public class DbusManagerImpl implements Manager { public void sendViewedReceipt( final RecipientIdentifier.Single sender, final List messageIds ) throws IOException, UntrustedIdentityException { - throw new UnsupportedOperationException(); + signal.sendViewedReceipt(sender.getIdentifier(), messageIds); } @Override @@ -276,9 +327,9 @@ public class DbusManagerImpl implements Manager { final Message message, final Set 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 @@ -328,7 +379,7 @@ public class DbusManagerImpl implements Manager { @Override public void setContactName( final RecipientIdentifier.Single recipient, final String name - ) throws NotMasterDeviceException, UnregisteredUserException { + ) throws NotMasterDeviceException { signal.setContactName(recipient.getIdentifier(), name); } @@ -343,7 +394,12 @@ public class DbusManagerImpl implements Manager { public void setGroupBlocked( final GroupId groupId, final boolean blocked ) throws GroupNotFoundException, IOException { - signal.setGroupBlocked(groupId.serialize(), blocked); + setGroupProperty(groupId, "IsBlocked", blocked); + } + + private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) { + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.Set("org.asamk.Signal.Group", propertyName, blocked); } @Override @@ -367,30 +423,69 @@ public class DbusManagerImpl implements Manager { signal.sendSyncRequest(); } + @Override + public void addReceiveHandler(final ReceiveMessageHandler handler) { + synchronized (messageHandlers) { + if (messageHandlers.size() == 0) { + installMessageHandlers(); + } + messageHandlers.add(handler); + } + } + + @Override + public void removeReceiveHandler(final ReceiveMessageHandler handler) { + synchronized (messageHandlers) { + 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 @@ -410,19 +505,41 @@ public class DbusManagerImpl implements Manager { @Override public Group getGroup(final GroupId groupId) { - final var id = groupId.serialize(); - return new Group(groupId, - signal.getGroupName(id), - null, - null, - signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), - Set.of(), - Set.of(), - Set.of(), - signal.isGroupBlocked(id), - 0, - false, - signal.isMember(id)); + final var groupPath = signal.getGroup(groupId.serialize()); + return getGroup(groupPath); + } + + @SuppressWarnings("unchecked") + private Group getGroup(final DBusPath groupPath) { + final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group"); + final var id = (byte[]) group.get("Id").getValue(); + try { + return new Group(GroupId.unknownVersion(id), + (String) group.get("Name").getValue(), + (String) group.get("Description").getValue(), + GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()), + ((List) group.get("Members").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("PendingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("RequestingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("Admins").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()), + GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()), + GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()), + (boolean) group.get("IsMember").getValue(), + (boolean) group.get("IsAdmin").getValue()); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + throw new AssertionError(e); + } } @Override @@ -459,20 +576,15 @@ public class DbusManagerImpl implements Manager { 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) { + messageHandlers.clear(); + uninstallMessageHandlers(); + } } private SendMessageResults handleMessage( @@ -497,7 +609,7 @@ public class DbusManagerImpl implements Manager { 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()); @@ -516,4 +628,178 @@ 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(), + getAttachments(extras), + Optional.empty(), + Optional.empty(), + List.of(), + List.of(), + List.of())), + Optional.empty(), + Optional.empty()); + synchronized (messageHandlers) { + for (final var messageHandler : messageHandlers) { + messageHandler.handleMessage(envelope, null); + } + } + }; + 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()); + synchronized (messageHandlers) { + for (final var messageHandler : messageHandlers) { + messageHandler.handleMessage(envelope, null); + } + } + }; + 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()); + synchronized (messageHandlers) { + for (final var messageHandler : messageHandlers) { + messageHandler.handleMessage(envelope, null); + } + } + }; + connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler); + } catch (DBusException e) { + e.printStackTrace(); + } + } + + private void uninstallMessageHandlers() { + try { + 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(); + } }