From 7f3ba1978d556bb3d730f6a2edc35420b588864f Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 30 Sep 2021 17:06:13 -0300 Subject: [PATCH] Add RedeemReceiptRequest object and DonationService. --- .../dependencies/ApplicationDependencies.java | 14 ++++++ .../ApplicationDependencyProvider.java | 10 ++++ .../api/services/DonationsService.java | 48 ++++++++++++++++++ .../signalservice/internal/EmptyResponse.java | 8 +++ .../internal/push/PushServiceSocket.java | 9 +++- .../internal/push/RedeemReceiptRequest.java | 49 +++++++++++++++++++ 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/EmptyResponse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index d3c26fff5..d986e890f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -48,6 +48,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.services.DonationsService; import okhttp3.OkHttpClient; @@ -104,6 +105,7 @@ public class ApplicationDependencies { private static volatile GiphyMp4Cache giphyMp4Cache; private static volatile SimpleExoPlayerPool exoPlayerPool; private static volatile AudioManagerCompat audioManagerCompat; + private static volatile DonationsService donationsService; @MainThread public static void init(@NonNull Application application, @NonNull Provider provider) { @@ -590,6 +592,17 @@ public class ApplicationDependencies { return audioManagerCompat; } + public static @NonNull DonationsService getDonationsService() { + if (donationsService == null) { + synchronized (LOCK) { + if (donationsService == null) { + donationsService = provider.provideDonationsService(); + } + } + } + return donationsService; + } + public interface Provider { @NonNull GroupsV2Operations provideGroupsV2Operations(); @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(); @@ -625,5 +638,6 @@ public class ApplicationDependencies { @NonNull GiphyMp4Cache provideGiphyMp4Cache(); @NonNull SimpleExoPlayerPool provideExoPlayerPool(); @NonNull AudioManagerCompat provideAndroidCallAudioManager(); + @NonNull DonationsService provideDonationsService(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 675cecf32..f898f2cf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -68,6 +68,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -301,6 +302,15 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return AudioManagerCompat.create(context); } + @Override + public @NonNull DonationsService provideDonationsService() { + return new DonationsService(provideSignalServiceNetworkAccess().getConfiguration(context), + new DynamicCredentialsProvider(context), + BuildConfig.SIGNAL_AGENT, + provideGroupsV2Operations(), + FeatureFlags.okHttpAutomaticRetry()); + } + private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) { return new WebSocketFactory() { @Override diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java new file mode 100644 index 000000000..ef7396e41 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -0,0 +1,48 @@ +package org.whispersystems.signalservice.api.services; + +import org.signal.zkgroup.receipts.ReceiptCredentialPresentation; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.util.CredentialsProvider; +import org.whispersystems.signalservice.internal.EmptyResponse; +import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.push.PushServiceSocket; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * One-stop shop for Signal service calls related to donations. + */ +public class DonationsService { + private final PushServiceSocket pushServiceSocket; + + public DonationsService( + SignalServiceConfiguration configuration, + CredentialsProvider credentialsProvider, + String signalAgent, + GroupsV2Operations groupsV2Operations, + boolean automaticNetworkRetry + ) { + this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations(), automaticNetworkRetry); + } + + /** + * Allows a user to redeem a given receipt they were given after submitting a donation successfully. + * + * @param receiptCredentialPresentation Receipt + * @param visible Whether the badge will be visible on the user's profile immediately after redemption + * @param primary Whether the badge will be made primary immediately after redemption + */ + public Single> redeemReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) { + return Single.fromCallable(() -> { + try { + pushServiceSocket.redeemDonationReceipt(receiptCredentialPresentation, visible, primary); + return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null); + } catch (Exception e) { + return ServiceResponse.forUnknownError(e); + } + }).subscribeOn(Schedulers.io()); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/EmptyResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/EmptyResponse.java new file mode 100644 index 000000000..6c8ccb191 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/EmptyResponse.java @@ -0,0 +1,8 @@ +package org.whispersystems.signalservice.internal; + +/** + * Indicates that no data is returned from a given service call. + */ +public enum EmptyResponse { + INSTANCE +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index acfcee9de..1463d2685 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -25,6 +25,7 @@ import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext; import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse; import org.signal.zkgroup.profiles.ProfileKeyVersion; +import org.signal.zkgroup.receipts.ReceiptCredentialPresentation; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.logging.Log; @@ -134,7 +135,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Future; @@ -233,6 +233,8 @@ public class PushServiceSocket { private static final String SUBMIT_RATE_LIMIT_CHALLENGE = "/v1/challenge"; private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push"; + private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt"; + private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; @@ -862,6 +864,11 @@ public class PushServiceSocket { makeServiceRequest(SUBMIT_RATE_LIMIT_CHALLENGE, "PUT", payload); } + public void redeemDonationReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) throws IOException { + String payload = JsonUtil.toJson(new RedeemReceiptRequest(Base64.encodeBytesToBytes(receiptCredentialPresentation.serialize()), visible, primary)); + makeServiceRequest(DONATION_REDEEM_RECEIPT, "PUT", payload); + } + public List retrieveDirectory(Set contactTokens) throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java new file mode 100644 index 000000000..fcf36fb1b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemReceiptRequest.java @@ -0,0 +1,49 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.signal.zkgroup.receipts.ReceiptCredentialPresentation; +import org.whispersystems.libsignal.util.guava.Preconditions; + +/** + * POST /v1/donation/redeem-receipt + * + * Request object for redeeming a receipt from a donation transaction. + */ +class RedeemReceiptRequest { + + private final byte[] receiptCredentialPresentation; + private final boolean visible; + private final boolean primary; + + /** + * @param receiptCredentialPresentation base64-encoded no-newlines standard-character-set with-padding of the bytes of a {@link ReceiptCredentialPresentation} object + * @param visible boolean indicating if the new badge should be visible or not on the profile + * @param primary boolean indicating if the new badge should be primary or not on the profile; is always treated as false if `visible` is false + */ + @JsonCreator + RedeemReceiptRequest( + @JsonProperty("receiptCredentialPresentation") byte[] receiptCredentialPresentation, + @JsonProperty("visible") boolean visible, + @JsonProperty("primary") boolean primary) { + + Preconditions.checkArgument(receiptCredentialPresentation.length == ReceiptCredentialPresentation.SIZE); + + this.receiptCredentialPresentation = receiptCredentialPresentation; + this.visible = visible; + this.primary = primary; + } + + public byte[] getReceiptCredentialPresentation() { + return receiptCredentialPresentation; + } + + public boolean isVisible() { + return visible; + } + + public boolean isPrimary() { + return primary; + } +} \ No newline at end of file