diff --git a/build.gradle b/build.gradle index 946d118de..cd53100e3 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ task qa { ':Signal-Android:lintPlayProdRelease', 'Signal-Android:ktlintCheck', ':libsignal-service:test', + ':libsignal-service:ktlintCheck', ':Signal-Android:assemblePlayProdRelease' } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b6a80016e..fc25766bb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2292,6 +2292,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2300,6 +2308,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2308,6 +2324,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2316,6 +2340,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2324,6 +2356,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2332,6 +2372,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2340,6 +2388,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2348,6 +2404,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2356,6 +2420,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2364,6 +2436,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -2372,6 +2452,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + + + + @@ -3424,6 +3512,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3459,6 +3552,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3599,6 +3697,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3619,6 +3722,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3699,6 +3807,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3749,6 +3862,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3784,6 +3902,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3824,6 +3947,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index 2216625f7..5d99b0ef2 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -1,9 +1,11 @@ apply plugin: 'java-library' +apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'java-test-fixtures' apply plugin: 'com.google.protobuf' apply plugin: 'maven-publish' apply plugin: 'signing' apply plugin: 'idea' +apply plugin: 'org.jlleitschuh.gradle.ktlint' sourceCompatibility = 1.8 archivesBaseName = "signal-service-java" @@ -41,6 +43,8 @@ dependencies { api libs.rxjava3.rxjava + implementation libs.kotlin.stdlib.jdk8 + testImplementation testLibs.junit.junit testImplementation testLibs.assertj.core testImplementation testLibs.conscrypt.openjdk.uber diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 6cdda8678..c59f66e2c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -721,27 +721,29 @@ public final class SignalServiceContent { metadata.getSenderDevice()); } - return new SignalServiceDataMessage(metadata.getTimestamp(), - groupInfoV2, - attachments, - content.hasBody() ? content.getBody() : null, - endSession, - content.getExpireTimer(), - expirationUpdate, - content.hasProfileKey() ? content.getProfileKey().toByteArray() : null, - profileKeyUpdate, - quote, - sharedContacts, - previews, - mentions, - sticker, - content.getIsViewOnce(), - reaction, - remoteDelete, - groupCallUpdate, - payment, - storyContext, - giftBadge); + return SignalServiceDataMessage.newBuilder() + .withTimestamp(metadata.getTimestamp()) + .asGroupMessage(groupInfoV2) + .withAttachments(attachments) + .withBody(content.hasBody() ? content.getBody() : null) + .asEndSessionMessage(endSession) + .withExpiration(content.getExpireTimer()) + .asExpirationUpdate(expirationUpdate) + .withProfileKey(content.hasProfileKey() ? content.getProfileKey().toByteArray() : null) + .asProfileKeyUpdate(profileKeyUpdate) + .withQuote(quote) + .withSharedContacts(sharedContacts) + .withPreviews(previews) + .withMentions(mentions) + .withSticker(sticker) + .withViewOnce(content.getIsViewOnce()) + .withReaction(reaction) + .withRemoteDelete(remoteDelete) + .withGroupCallUpdate(groupCallUpdate) + .withPayment(payment) + .withStoryContext(storyContext) + .withGiftBadge(giftBadge) + .build(); } private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java deleted file mode 100644 index 3c7b0273f..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.whispersystems.signalservice.api.messages; - -import org.signal.libsignal.protocol.InvalidMessageException; -import org.signal.libsignal.zkgroup.groups.GroupSecretParams; -import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; -import org.whispersystems.signalservice.api.messages.shared.SharedContact; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.OptionalUtil; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -/** - * Represents a decrypted Signal Service data message. - */ -public class SignalServiceDataMessage { - - private final long timestamp; - private final Optional> attachments; - private final Optional body; - private final Optional group; - private final Optional profileKey; - private final boolean endSession; - private final boolean expirationUpdate; - private final int expiresInSeconds; - private final boolean profileKeyUpdate; - private final Optional quote; - private final Optional> contacts; - private final Optional> previews; - private final Optional> mentions; - private final Optional sticker; - private final boolean viewOnce; - private final Optional reaction; - private final Optional remoteDelete; - private final Optional groupCallUpdate; - private final Optional payment; - private final Optional storyContext; - private final Optional giftBadge; - - /** - * Construct a SignalServiceDataMessage. - * - * @param timestamp The sent timestamp. - * @param groupV2 The group information (or null if none). - * @param attachments The attachments (or null if none). - * @param body The message contents. - * @param endSession Flag indicating whether this message should close a session. - * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. - */ - SignalServiceDataMessage(long timestamp, - SignalServiceGroupV2 groupV2, - List attachments, - String body, - boolean endSession, - int expiresInSeconds, - boolean expirationUpdate, - byte[] profileKey, - boolean profileKeyUpdate, - Quote quote, - List sharedContacts, - List previews, - List mentions, - Sticker sticker, - boolean viewOnce, - Reaction reaction, - RemoteDelete remoteDelete, - GroupCallUpdate groupCallUpdate, - Payment payment, - StoryContext storyContext, - GiftBadge giftBadge) - { - this.group = Optional.ofNullable(groupV2); - this.timestamp = timestamp; - this.body = OptionalUtil.absentIfEmpty(body); - this.endSession = endSession; - this.expiresInSeconds = expiresInSeconds; - this.expirationUpdate = expirationUpdate; - this.profileKey = Optional.ofNullable(profileKey); - this.profileKeyUpdate = profileKeyUpdate; - this.quote = Optional.ofNullable(quote); - this.sticker = Optional.ofNullable(sticker); - this.viewOnce = viewOnce; - this.reaction = Optional.ofNullable(reaction); - this.remoteDelete = Optional.ofNullable(remoteDelete); - this.groupCallUpdate = Optional.ofNullable(groupCallUpdate); - this.payment = Optional.ofNullable(payment); - this.storyContext = Optional.ofNullable(storyContext); - this.giftBadge = Optional.ofNullable(giftBadge); - - if (attachments != null && !attachments.isEmpty()) { - this.attachments = Optional.of(attachments); - } else { - this.attachments = Optional.empty(); - } - - if (sharedContacts != null && !sharedContacts.isEmpty()) { - this.contacts = Optional.of(sharedContacts); - } else { - this.contacts = Optional.empty(); - } - - if (previews != null && !previews.isEmpty()) { - this.previews = Optional.of(previews); - } else { - this.previews = Optional.empty(); - } - - if (mentions != null && !mentions.isEmpty()) { - this.mentions = Optional.of(mentions); - } else { - this.mentions = Optional.empty(); - } - } - - public static Builder newBuilder() { - return new Builder(); - } - - /** - * @return The message timestamp. - */ - public long getTimestamp() { - return timestamp; - } - - /** - * @return The message attachments (if any). - */ - public Optional> getAttachments() { - return attachments; - } - - /** - * @return The message body (if any). - */ - public Optional getBody() { - return body; - } - - /** - * @return The message group context (if any). - */ - public Optional getGroupContext() { - return group; - } - - public boolean isEndSession() { - return endSession; - } - - public boolean isExpirationUpdate() { - return expirationUpdate; - } - - public boolean isActivatePaymentsRequest() { - return getPayment().isPresent() && - getPayment().get().getPaymentActivation().isPresent() && - getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST); - } - - public boolean isPaymentsActivated() { - return getPayment().isPresent() && - getPayment().get().getPaymentActivation().isPresent() && - getPayment().get().getPaymentActivation().get().getType().equals(SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED); - } - - public boolean isProfileKeyUpdate() { - return profileKeyUpdate; - } - - public boolean isGroupV2Message() { - return group.isPresent(); - } - - public boolean isGroupV2Update() { - return group.isPresent() && - group.get().hasSignedGroupChange() && - !hasRenderableContent(); - } - - public boolean isEmptyGroupV2Message() { - return isGroupV2Message() && !isGroupV2Update() && !hasRenderableContent(); - } - - /** Contains some user data that affects the conversation */ - public boolean hasRenderableContent() { - return attachments.isPresent() || - body.isPresent() || - quote.isPresent() || - contacts.isPresent() || - previews.isPresent() || - mentions.isPresent() || - sticker.isPresent() || - reaction.isPresent() || - remoteDelete.isPresent(); - } - - public int getExpiresInSeconds() { - return expiresInSeconds; - } - - public Optional getProfileKey() { - return profileKey; - } - - public Optional getQuote() { - return quote; - } - - public Optional> getSharedContacts() { - return contacts; - } - - public Optional> getPreviews() { - return previews; - } - - public Optional> getMentions() { - return mentions; - } - - public Optional getSticker() { - return sticker; - } - - public boolean isViewOnce() { - return viewOnce; - } - - public Optional getReaction() { - return reaction; - } - - public Optional getRemoteDelete() { - return remoteDelete; - } - - public Optional getGroupCallUpdate() { - return groupCallUpdate; - } - - public Optional getPayment() { - return payment; - } - - public Optional getStoryContext() { - return storyContext; - } - - public Optional getGiftBadge() { - return giftBadge; - } - - public Optional getGroupId() { - byte[] groupId = null; - - if (getGroupContext().isPresent() && getGroupContext().isPresent()) { - SignalServiceGroupV2 gv2 = getGroupContext().get(); - groupId = GroupSecretParams.deriveFromMasterKey(gv2.getMasterKey()) - .getPublicParams() - .getGroupIdentifier() - .serialize(); - } - - return Optional.ofNullable(groupId); - } - - public static class Builder { - - private List attachments = new LinkedList<>(); - private List sharedContacts = new LinkedList<>(); - private List previews = new LinkedList<>(); - private List mentions = new LinkedList<>(); - - private long timestamp; - private SignalServiceGroupV2 groupV2; - private String body; - private boolean endSession; - private int expiresInSeconds; - private boolean expirationUpdate; - private byte[] profileKey; - private boolean profileKeyUpdate; - private Quote quote; - private Sticker sticker; - private boolean viewOnce; - private Reaction reaction; - private RemoteDelete remoteDelete; - private GroupCallUpdate groupCallUpdate; - private Payment payment; - private StoryContext storyContext; - private GiftBadge giftBadge; - - private Builder() {} - - public Builder withTimestamp(long timestamp) { - this.timestamp = timestamp; - return this; - } - - public Builder asGroupMessage(SignalServiceGroupV2 group) { - this.groupV2 = group; - return this; - } - - public Builder withAttachment(SignalServiceAttachment attachment) { - this.attachments.add(attachment); - return this; - } - - public Builder withAttachments(List attachments) { - this.attachments.addAll(attachments); - return this; - } - - public Builder withBody(String body) { - this.body = body; - return this; - } - - public Builder asEndSessionMessage() { - return asEndSessionMessage(true); - } - - public Builder asEndSessionMessage(boolean endSession) { - this.endSession = endSession; - return this; - } - - public Builder asExpirationUpdate() { - return asExpirationUpdate(true); - } - - public Builder asExpirationUpdate(boolean expirationUpdate) { - this.expirationUpdate = expirationUpdate; - return this; - } - - public Builder withExpiration(int expiresInSeconds) { - this.expiresInSeconds = expiresInSeconds; - return this; - } - - public Builder withProfileKey(byte[] profileKey) { - this.profileKey = profileKey; - return this; - } - - public Builder asProfileKeyUpdate(boolean profileKeyUpdate) { - this.profileKeyUpdate = profileKeyUpdate; - return this; - } - - public Builder withQuote(Quote quote) { - this.quote = quote; - return this; - } - - public Builder withSharedContact(SharedContact contact) { - this.sharedContacts.add(contact); - return this; - } - - public Builder withSharedContacts(List contacts) { - this.sharedContacts.addAll(contacts); - return this; - } - - public Builder withPreviews(List previews) { - this.previews.addAll(previews); - return this; - } - - public Builder withMentions(List mentions) { - this.mentions.addAll(mentions); - return this; - } - - public Builder withSticker(Sticker sticker) { - this.sticker = sticker; - return this; - } - - public Builder withViewOnce(boolean viewOnce) { - this.viewOnce = viewOnce; - return this; - } - - public Builder withReaction(Reaction reaction) { - this.reaction = reaction; - return this; - } - - public Builder withRemoteDelete(RemoteDelete remoteDelete) { - this.remoteDelete = remoteDelete; - return this; - } - - public Builder withGroupCallUpdate(GroupCallUpdate groupCallUpdate) { - this.groupCallUpdate = groupCallUpdate; - return this; - } - - public Builder withPayment(Payment payment) { - this.payment = payment; - return this; - } - - public Builder withStoryContext(StoryContext storyContext) { - this.storyContext = storyContext; - return this; - } - - public Builder withGiftBadge(GiftBadge giftBadge) { - this.giftBadge = giftBadge; - return this; - } - - public SignalServiceDataMessage build() { - if (timestamp == 0) timestamp = System.currentTimeMillis(); - return new SignalServiceDataMessage(timestamp, groupV2, attachments, body, endSession, - expiresInSeconds, expirationUpdate, profileKey, - profileKeyUpdate, quote, sharedContacts, previews, - mentions, sticker, viewOnce, reaction, remoteDelete, - groupCallUpdate, - payment, - storyContext, - giftBadge); - } - } - - public static class Quote { - private final long id; - private final ServiceId author; - private final String text; - private final List attachments; - private final List mentions; - private final Type type; - - public Quote(long id, - ServiceId author, - String text, - List attachments, - List mentions, - Type type) - { - this.id = id; - this.author = author; - this.text = text; - this.attachments = attachments; - this.mentions = mentions; - this.type = type; - } - - public long getId() { - return id; - } - - public ServiceId getAuthor() { - return author; - } - - public String getText() { - return text; - } - - public List getAttachments() { - return attachments; - } - - public List getMentions() { - return mentions; - } - - public Type getType() { - return type; - } - - public enum Type { - NORMAL(SignalServiceProtos.DataMessage.Quote.Type.NORMAL), - GIFT_BADGE(SignalServiceProtos.DataMessage.Quote.Type.GIFT_BADGE); - - private final SignalServiceProtos.DataMessage.Quote.Type protoType; - - Type(SignalServiceProtos.DataMessage.Quote.Type protoType) { - this.protoType = protoType; - } - - public SignalServiceProtos.DataMessage.Quote.Type getProtoType() { - return protoType; - } - - public static Type fromProto(SignalServiceProtos.DataMessage.Quote.Type protoType) { - for (final Type value : values()) { - if (value.protoType == protoType) { - return value; - } - } - - return NORMAL; - } - } - - public static class QuotedAttachment { - private final String contentType; - private final String fileName; - private final SignalServiceAttachment thumbnail; - - public QuotedAttachment(String contentType, String fileName, SignalServiceAttachment thumbnail) { - this.contentType = contentType; - this.fileName = fileName; - this.thumbnail = thumbnail; - } - - public String getContentType() { - return contentType; - } - - public String getFileName() { - return fileName; - } - - public SignalServiceAttachment getThumbnail() { - return thumbnail; - } - } - } - - public static class Sticker { - private final byte[] packId; - private final byte[] packKey; - private final int stickerId; - private final String emoji; - private final SignalServiceAttachment attachment; - - public Sticker(byte[] packId, byte[] packKey, int stickerId, String emoji, SignalServiceAttachment attachment) { - this.packId = packId; - this.packKey = packKey; - this.stickerId = stickerId; - this.emoji = emoji; - this.attachment = attachment; - } - - public byte[] getPackId() { - return packId; - } - - public byte[] getPackKey() { - return packKey; - } - - public int getStickerId() { - return stickerId; - } - - public String getEmoji() { - return emoji; - } - - public SignalServiceAttachment getAttachment() { - return attachment; - } - } - - public static class Reaction { - private final String emoji; - private final boolean remove; - private final ServiceId targetAuthor; - private final long targetSentTimestamp; - - public Reaction(String emoji, boolean remove, ServiceId targetAuthor, long targetSentTimestamp) { - this.emoji = emoji; - this.remove = remove; - this.targetAuthor = targetAuthor; - this.targetSentTimestamp = targetSentTimestamp; - } - - public String getEmoji() { - return emoji; - } - - public boolean isRemove() { - return remove; - } - - public ServiceId getTargetAuthor() { - return targetAuthor; - } - - public long getTargetSentTimestamp() { - return targetSentTimestamp; - } - } - - public static class RemoteDelete { - private final long targetSentTimestamp; - - public RemoteDelete(long targetSentTimestamp) { - this.targetSentTimestamp = targetSentTimestamp; - } - - public long getTargetSentTimestamp() { - return targetSentTimestamp; - } - } - - public static class Mention { - private final ServiceId serviceId; - private final int start; - private final int length; - - public Mention(ServiceId serviceId, int start, int length) { - this.serviceId = serviceId; - this.start = start; - this.length = length; - } - - public ServiceId getServiceId() { - return serviceId; - } - - public int getStart() { - return start; - } - - public int getLength() { - return length; - } - } - - public static class GroupCallUpdate { - private final String eraId; - - public GroupCallUpdate(String eraId) { - this.eraId = eraId; - } - - public String getEraId() { - return eraId; - } - } - - public static class PaymentNotification { - - private final byte[] receipt; - private final String note; - - public PaymentNotification(byte[] receipt, String note) { - this.receipt = receipt; - this.note = note; - } - - public byte[] getReceipt() { - return receipt; - } - - public String getNote() { - return note; - } - } - - public static class PaymentActivation { - private final SignalServiceProtos.DataMessage.Payment.Activation.Type type; - - public PaymentActivation(SignalServiceProtos.DataMessage.Payment.Activation.Type type) { - this.type = type; - } - - public SignalServiceProtos.DataMessage.Payment.Activation.Type getType() { - return type; - } - } - - public static class Payment { - private final Optional paymentNotification; - private final Optional paymentActivation; - - public Payment(PaymentNotification paymentNotification, PaymentActivation paymentActivation) { - this.paymentNotification = Optional.ofNullable(paymentNotification); - this.paymentActivation = Optional.ofNullable(paymentActivation); - } - - public Optional getPaymentNotification() { - return paymentNotification; - } - - public Optional getPaymentActivation() { - return paymentActivation; - } - } - - public static class StoryContext { - private final ServiceId authorServiceId; - private final long sentTimestamp; - - public StoryContext(ServiceId authorServiceId, long sentTimestamp) { - this.authorServiceId = authorServiceId; - this.sentTimestamp = sentTimestamp; - } - - public ServiceId getAuthorServiceId() { - return authorServiceId; - } - - public long getSentTimestamp() { - return sentTimestamp; - } - } - - public static class GiftBadge { - private final ReceiptCredentialPresentation receiptCredentialPresentation; - - public GiftBadge(ReceiptCredentialPresentation receiptCredentialPresentation) { - this.receiptCredentialPresentation = receiptCredentialPresentation; - } - - public ReceiptCredentialPresentation getReceiptCredentialPresentation() { - return receiptCredentialPresentation; - } - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt new file mode 100644 index 000000000..46b10ab93 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2014-2016 Open Whisper Systems + * + * Licensed according to the LICENSE file in this repository. + */ +package org.whispersystems.signalservice.api.messages + +import org.signal.libsignal.zkgroup.groups.GroupSecretParams +import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation +import org.whispersystems.signalservice.api.messages.shared.SharedContact +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional +import org.whispersystems.signalservice.api.util.OptionalUtil.emptyIfStringEmpty +import java.util.LinkedList +import java.util.Optional +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Payment as PaymentProto +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Quote as QuoteProto + +/** + * Represents a decrypted Signal Service data message. + * + * @param timestamp The sent timestamp. + * @param groupContext The group information (or null if none). + * @param attachments The attachments (or null if none). + * @param body The message contents. + * @param isEndSession Flag indicating whether this message should close a session. + * @param expiresInSeconds Number of seconds in which the message should disappear after being seen. + */ +class SignalServiceDataMessage private constructor( + val timestamp: Long, + val groupContext: Optional, + val attachments: Optional>, + val body: Optional, + val isEndSession: Boolean, + val expiresInSeconds: Int, + val isExpirationUpdate: Boolean, + val profileKey: Optional, + val isProfileKeyUpdate: Boolean, + val quote: Optional, + val sharedContacts: Optional>, + val previews: Optional>, + val mentions: Optional>, + val sticker: Optional, + val isViewOnce: Boolean, + val reaction: Optional, + val remoteDelete: Optional, + val groupCallUpdate: Optional, + val payment: Optional, + val storyContext: Optional, + val giftBadge: Optional +) { + val isActivatePaymentsRequest: Boolean = payment.map { it.isActivationRequest }.orElse(false) + val isPaymentsActivated: Boolean = payment.map { it.isActivation }.orElse(false) + + val groupId: Optional = groupContext.map { GroupSecretParams.deriveFromMasterKey(it.masterKey).publicParams.groupIdentifier.serialize() } + val isGroupV2Message: Boolean = groupContext.isPresent + + /** Contains some user data that affects the conversation */ + private val hasRenderableContent: Boolean = + this.attachments.isPresent || + this.body.isPresent || + this.quote.isPresent || + this.sharedContacts.isPresent || + this.previews.isPresent || + this.mentions.isPresent || + this.sticker.isPresent || + this.reaction.isPresent || + this.remoteDelete.isPresent + + val isGroupV2Update: Boolean = groupContext.isPresent && groupContext.get().hasSignedGroupChange() && !hasRenderableContent + val isEmptyGroupV2Message: Boolean = isGroupV2Message && !isGroupV2Update && !hasRenderableContent + + class Builder { + private var timestamp: Long = 0 + private var groupV2: SignalServiceGroupV2? = null + private val attachments: MutableList = LinkedList() + private var body: String? = null + private var endSession: Boolean = false + private var expiresInSeconds: Int = 0 + private var expirationUpdate: Boolean = false + private var profileKey: ByteArray? = null + private var profileKeyUpdate: Boolean = false + private var quote: Quote? = null + private val sharedContacts: MutableList = LinkedList() + private val previews: MutableList = LinkedList() + private val mentions: MutableList = LinkedList() + private var sticker: Sticker? = null + private var viewOnce: Boolean = false + private var reaction: Reaction? = null + private var remoteDelete: RemoteDelete? = null + private var groupCallUpdate: GroupCallUpdate? = null + private var payment: Payment? = null + private var storyContext: StoryContext? = null + private var giftBadge: GiftBadge? = null + + fun withTimestamp(timestamp: Long): Builder { + this.timestamp = timestamp + return this + } + + fun asGroupMessage(group: SignalServiceGroupV2?): Builder { + groupV2 = group + return this + } + + fun withAttachment(attachment: SignalServiceAttachment?): Builder { + attachment?.let { attachments.add(attachment) } + return this + } + + fun withAttachments(attachments: List?): Builder { + attachments?.let { this.attachments.addAll(attachments) } + return this + } + + fun withBody(body: String?): Builder { + this.body = body + return this + } + + @JvmOverloads + fun asEndSessionMessage(endSession: Boolean = true): Builder { + this.endSession = endSession + return this + } + + @JvmOverloads + fun asExpirationUpdate(expirationUpdate: Boolean = true): Builder { + this.expirationUpdate = expirationUpdate + return this + } + + fun withExpiration(expiresInSeconds: Int): Builder { + this.expiresInSeconds = expiresInSeconds + return this + } + + fun withProfileKey(profileKey: ByteArray?): Builder { + this.profileKey = profileKey + return this + } + + fun asProfileKeyUpdate(profileKeyUpdate: Boolean): Builder { + this.profileKeyUpdate = profileKeyUpdate + return this + } + + fun withQuote(quote: Quote?): Builder { + this.quote = quote + return this + } + + fun withSharedContact(contact: SharedContact?): Builder { + contact?.let { sharedContacts.add(contact) } + return this + } + + fun withSharedContacts(contacts: List?): Builder { + contacts?.let { sharedContacts.addAll(contacts) } + return this + } + + fun withPreviews(previews: List?): Builder { + previews?.let { this.previews.addAll(previews) } + return this + } + + fun withMentions(mentions: List?): Builder { + mentions?.let { this.mentions.addAll(mentions) } + return this + } + + fun withSticker(sticker: Sticker?): Builder { + this.sticker = sticker + return this + } + + fun withViewOnce(viewOnce: Boolean): Builder { + this.viewOnce = viewOnce + return this + } + + fun withReaction(reaction: Reaction?): Builder { + this.reaction = reaction + return this + } + + fun withRemoteDelete(remoteDelete: RemoteDelete?): Builder { + this.remoteDelete = remoteDelete + return this + } + + fun withGroupCallUpdate(groupCallUpdate: GroupCallUpdate?): Builder { + this.groupCallUpdate = groupCallUpdate + return this + } + + fun withPayment(payment: Payment?): Builder { + this.payment = payment + return this + } + + fun withStoryContext(storyContext: StoryContext?): Builder { + this.storyContext = storyContext + return this + } + + fun withGiftBadge(giftBadge: GiftBadge?): Builder { + this.giftBadge = giftBadge + return this + } + + fun build(): SignalServiceDataMessage { + if (timestamp == 0L) { + timestamp = System.currentTimeMillis() + } + + return SignalServiceDataMessage( + timestamp = timestamp, + groupContext = groupV2.asOptional(), + attachments = attachments.asOptional(), + body = body.emptyIfStringEmpty(), + isEndSession = endSession, + expiresInSeconds = expiresInSeconds, + isExpirationUpdate = expirationUpdate, + profileKey = profileKey.asOptional(), + isProfileKeyUpdate = profileKeyUpdate, + quote = quote.asOptional(), + sharedContacts = sharedContacts.asOptional(), + previews = previews.asOptional(), + mentions = mentions.asOptional(), + sticker = sticker.asOptional(), + isViewOnce = viewOnce, + reaction = reaction.asOptional(), + remoteDelete = remoteDelete.asOptional(), + groupCallUpdate = groupCallUpdate.asOptional(), + payment = payment.asOptional(), + storyContext = storyContext.asOptional(), + giftBadge = giftBadge.asOptional() + ) + } + } + + data class Quote( + val id: Long, + val author: ServiceId, + val text: String, + val attachments: List, + val mentions: List, + val type: Type + ) { + enum class Type(val protoType: QuoteProto.Type) { + NORMAL(QuoteProto.Type.NORMAL), + GIFT_BADGE(QuoteProto.Type.GIFT_BADGE); + + companion object { + @JvmStatic + fun fromProto(protoType: QuoteProto.Type): Type { + return values().firstOrNull { it.protoType == protoType } ?: NORMAL + } + } + } + + data class QuotedAttachment(val contentType: String, val fileName: String, val thumbnail: SignalServiceAttachment) + } + class Sticker(val packId: ByteArray, val packKey: ByteArray, val stickerId: Int, val emoji: String, val attachment: SignalServiceAttachment) + data class Reaction(val emoji: String, val isRemove: Boolean, val targetAuthor: ServiceId, val targetSentTimestamp: Long) + data class RemoteDelete(val targetSentTimestamp: Long) + data class Mention(val serviceId: ServiceId, val start: Int, val length: Int) + data class GroupCallUpdate(val eraId: String) + class PaymentNotification(val receipt: ByteArray, val note: String) + data class PaymentActivation(val type: PaymentProto.Activation.Type) + class Payment(paymentNotification: PaymentNotification?, paymentActivation: PaymentActivation?) { + val paymentNotification: Optional = Optional.ofNullable(paymentNotification) + val paymentActivation: Optional = Optional.ofNullable(paymentActivation) + val isActivationRequest: Boolean = paymentActivation != null && paymentActivation.type == PaymentProto.Activation.Type.REQUEST + val isActivation: Boolean = paymentActivation != null && paymentActivation.type == PaymentProto.Activation.Type.ACTIVATED + } + data class StoryContext(val authorServiceId: ServiceId, val sentTimestamp: Long) + data class GiftBadge(val receiptCredentialPresentation: ReceiptCredentialPresentation) + + companion object { + @JvmStatic + fun newBuilder(): Builder { + return Builder() + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java deleted file mode 100644 index 52b56ae0e..000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.whispersystems.signalservice.api.util; - -import com.google.protobuf.ByteString; - -import java.util.Arrays; -import java.util.Optional; - -public final class OptionalUtil { - - private OptionalUtil() { } - - @SafeVarargs - public static Optional or(Optional... optionals) { - return Arrays.stream(optionals) - .filter(Optional::isPresent) - .findFirst() - .orElse(Optional.empty()); - } - - public static boolean byteArrayEquals(Optional a, Optional b) { - if (a.isPresent() != b.isPresent()) { - return false; - } else if (a.isPresent()) { - return Arrays.equals(a.get(), b.get()); - } else { - return true; - } - } - - public static int byteArrayHashCode(Optional bytes) { - if (bytes.isPresent()) { - return Arrays.hashCode(bytes.get()); - } else { - return 0; - } - } - - public static Optional absentIfEmpty(String value) { - if (value == null || value.length() == 0) { - return Optional.empty(); - } else { - return Optional.of(value); - } - } - - public static Optional absentIfEmpty(ByteString value) { - if (value == null || value.isEmpty()) { - return Optional.empty(); - } else { - return Optional.of(value.toByteArray()); - } - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt new file mode 100644 index 000000000..469d2419f --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/OptionalUtil.kt @@ -0,0 +1,67 @@ +package org.whispersystems.signalservice.api.util + +import com.google.protobuf.ByteString +import java.util.Optional + +object OptionalUtil { + @JvmStatic + @SafeVarargs + fun or(vararg optionals: Optional): Optional { + return optionals.firstOrNull { it.isPresent } ?: Optional.empty() + } + + @JvmStatic + fun byteArrayEquals(a: Optional, b: Optional): Boolean { + return if (a.isPresent != b.isPresent) { + false + } else if (a.isPresent) { + a.get().contentEquals(b.get()) + } else { + true + } + } + + @JvmStatic + fun byteArrayHashCode(bytes: Optional): Int { + return if (bytes.isPresent) { + bytes.get().contentHashCode() + } else { + 0 + } + } + + @JvmStatic + fun absentIfEmpty(value: String?): Optional { + return if (value == null || value.isEmpty()) { + Optional.empty() + } else { + Optional.of(value) + } + } + + @JvmStatic + fun absentIfEmpty(value: ByteString?): Optional { + return if (value == null || value.isEmpty) { + Optional.empty() + } else { + Optional.of(value.toByteArray()) + } + } + + @JvmStatic + fun emptyIfListEmpty(list: List?): Optional> { + return list.asOptional() + } + + fun E?.asOptional(): Optional { + return Optional.ofNullable(this) + } + + fun List?.asOptional(): Optional> { + return Optional.ofNullable(this?.takeIf { it.isNotEmpty() }) + } + + fun String?.emptyIfStringEmpty(): Optional { + return absentIfEmpty(this) + } +}