Implement new APIs for Boost badging.

This commit is contained in:
Alex Hart
2021-10-28 09:11:45 -03:00
committed by Greyson Parrelli
parent 48a81da883
commit 186bd9db48
19 changed files with 457 additions and 137 deletions

View File

@@ -6,6 +6,7 @@ import org.signal.zkgroup.receipts.ReceiptCredentialResponse;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
@@ -19,6 +20,9 @@ import org.whispersystems.signalservice.internal.push.DonationIntentResult;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Single;
@@ -73,8 +77,33 @@ public class DonationsService {
* @param currencyCode The currency code for the amount
* @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway.
*/
public Single<ServiceResponse<DonationIntentResult>> createDonationIntentWithAmount(String amount, String currencyCode) {
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createDonationIntentWithAmount(amount, currencyCode), 200));
public Single<ServiceResponse<SubscriptionClientSecret>> createDonationIntentWithAmount(String amount, String currencyCode) {
return createServiceResponse(() -> new Pair<>(pushServiceSocket.createBoostPaymentMethod(currencyCode, Long.parseLong(amount)), 200));
}
/**
* Given a completed payment intent and a receipt credential request produces a receipt credential response.
* Clients should always use the same ReceiptCredentialRequest with the same payment intent id. This request is repeatable so long as the two values are reused.
*
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
* @param receiptCredentialRequest Client-generated request token
*/
public Single<ServiceResponse<ReceiptCredentialResponse>> submitBoostReceiptCredentialRequest(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
return createServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200));
}
/**
* @return The suggested amounts for Signal Boost
*/
public Single<ServiceResponse<Map<String, List<BigDecimal>>>> getBoostAmounts() {
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostAmounts(), 200));
}
/**
* @return The badge configuration for signal boost. Expect for right now only a single level numbered 1.
*/
public Single<ServiceResponse<SignalServiceProfile.Badge>> getBoostBadge() {
return createServiceResponse(() -> new Pair<>(pushServiceSocket.getBoostLevels().getLevels().get("1").getBadge(), 200));
}
/**

View File

@@ -8,6 +8,8 @@ import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
import java.util.concurrent.ExecutionException;
import io.reactivex.rxjava3.core.Single;
/**
* Encapsulates a parsed APi response regardless of where it came from (WebSocket or REST). Not only
* includes the success result but also any application errors encountered (404s, parsing, etc.) or
@@ -68,6 +70,18 @@ public final class ServiceResponse<Result> {
return executionError;
}
public Single<Result> flattenResult() {
if (result.isPresent()) {
return Single.just(result.get());
} else if (applicationError.isPresent()) {
return Single.error(applicationError.get());
} else if (executionError.isPresent()) {
return Single.error(executionError.get());
} else {
return Single.error(new AssertionError("Should never get here."));
}
}
public static <T> ServiceResponse<T> forResult(T result, WebsocketResponse response) {
return new ServiceResponse<>(result, response);
}

View File

@@ -0,0 +1,19 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.zkgroup.receipts.ReceiptCredentialRequest;
import org.whispersystems.util.Base64;
class BoostReceiptCredentialRequestJson {
@JsonProperty("paymentIntentId")
private final String paymentIntentId;
@JsonProperty("receiptCredentialRequest")
private final String receiptCredentialRequest;
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
this.paymentIntentId = paymentIntentId;
this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize());
}
}

View File

@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
@@ -133,6 +134,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@@ -242,7 +244,6 @@ 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_INTENT = "/v1/donation/authorize-apple-pay";
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
@@ -251,6 +252,10 @@ public class PushServiceSocket {
private static final String CREATE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
@@ -870,15 +875,42 @@ public class PushServiceSocket {
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
}
/**
* @return The PaymentIntent id
*/
public DonationIntentResult createDonationIntentWithAmount(String amount, String currencyCode) throws IOException {
String payload = JsonUtil.toJson(new DonationIntentPayload(Long.parseLong(amount), currencyCode.toLowerCase(Locale.ROOT)));
String result = makeServiceRequest(DONATION_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, DonationIntentResult.class);
public SubscriptionClientSecret createBoostPaymentMethod(String currencyCode, long amount) throws IOException {
String payload = JsonUtil.toJson(new DonationIntentPayload(amount, currencyCode));
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, SubscriptionClientSecret.class);
}
public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {};
return JsonUtil.fromJsonResponse(result, typeRef);
}
public SubscriptionLevels getBoostLevels() throws IOException {
String result = makeServiceRequestWithoutAuthentication(BOOST_BADGES, "GET", null);
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
}
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest));
String response = makeServiceRequestWithoutAuthentication(
BOOST_RECEIPT_CREDENTIALS,
"POST",
payload,
(code, body) -> {
if (code == 204) throw new NonSuccessfulResponseCodeException(204);
});
ReceiptCredentialResponseJson responseJson = JsonUtil.fromJson(response, ReceiptCredentialResponseJson.class);
if (responseJson.getReceiptCredentialResponse() != null) {
return responseJson.getReceiptCredentialResponse();
} else {
throw new MalformedResponseException("Unable to parse response");
}
}
public SubscriptionLevels getSubscriptionLevels() throws IOException {
String result = makeServiceRequestWithoutAuthentication(SUBSCRIPTION_LEVELS, "GET", null);
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);

View File

@@ -10,6 +10,7 @@ package org.whispersystems.signalservice.internal.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
@@ -52,6 +53,22 @@ public class JsonUtil {
return objectMapper.readValue(json, clazz);
}
public static <T> T fromJson(String json, TypeReference<T> typeRef)
throws IOException
{
return objectMapper.readValue(json, typeRef);
}
public static <T> T fromJsonResponse(String json, TypeReference<T> typeRef)
throws MalformedResponseException
{
try {
return JsonUtil.fromJson(json, typeRef);
} catch (IOException e) {
throw new MalformedResponseException("Unable to parse entity", e);
}
}
public static <T> T fromJsonResponse(String body, Class<T> clazz)
throws MalformedResponseException {
try {