Add endpoint for fetching boost amounts

This commit is contained in:
Ehren Kret 2021-10-21 13:56:35 -05:00
parent 3b764bed7a
commit 07cd69ab34
6 changed files with 80 additions and 14 deletions

View File

@ -20,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration; import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration; import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration; import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration; import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration; import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
@ -319,6 +320,11 @@ public class WhisperServerConfiguration extends Configuration {
// TODO: Mark as @NotNull when enabled for production. // TODO: Mark as @NotNull when enabled for production.
private SubscriptionConfiguration subscription; private SubscriptionConfiguration subscription;
@Valid
@JsonProperty
// TODO: Mark as @NotNull when enabled for production.
private BoostConfiguration boost;
@Valid @Valid
@NotNull @NotNull
@JsonProperty @JsonProperty
@ -552,6 +558,10 @@ public class WhisperServerConfiguration extends Configuration {
return subscription; return subscription;
} }
public BoostConfiguration getBoost() {
return boost;
}
public ReportMessageConfiguration getReportMessageConfiguration() { public ReportMessageConfiguration getReportMessageConfiguration() {
return reportMessage; return reportMessage;
} }

View File

@ -640,9 +640,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(),
config.getCdnConfiguration().getBucket()) config.getCdnConfiguration().getBucket())
); );
if (config.getSubscription() != null) { if (config.getSubscription() != null && config.getBoost() != null) {
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), subscriptionManager, commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getBoost(),
stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter)); subscriptionManager, stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter));
} }
for (Object controller : commonControllers) { for (Object controller : commonControllers) {

View File

@ -0,0 +1,34 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class BoostConfiguration {
private final Map<String, List<BigDecimal>> currencies;
@JsonCreator
public BoostConfiguration(
@JsonProperty("currencies") final Map<String, List<BigDecimal>> currencies) {
this.currencies = currencies;
}
@Valid
@NotNull
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
return currencies;
}
}

View File

@ -29,6 +29,7 @@ import java.util.Map.Entry;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.crypto.Mac; import javax.crypto.Mac;
@ -62,6 +63,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.badges.BadgeTranslator; import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
@ -78,7 +80,8 @@ public class SubscriptionController {
private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class); private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class);
private final Clock clock; private final Clock clock;
private final SubscriptionConfiguration config; private final SubscriptionConfiguration subscriptionConfiguration;
private final BoostConfiguration boostConfiguration;
private final SubscriptionManager subscriptionManager; private final SubscriptionManager subscriptionManager;
private final StripeManager stripeManager; private final StripeManager stripeManager;
private final ServerZkReceiptOperations zkReceiptOperations; private final ServerZkReceiptOperations zkReceiptOperations;
@ -87,14 +90,16 @@ public class SubscriptionController {
public SubscriptionController( public SubscriptionController(
@Nonnull Clock clock, @Nonnull Clock clock,
@Nonnull SubscriptionConfiguration config, @Nonnull SubscriptionConfiguration subscriptionConfiguration,
@Nonnull final BoostConfiguration boostConfiguration,
@Nonnull SubscriptionManager subscriptionManager, @Nonnull SubscriptionManager subscriptionManager,
@Nonnull StripeManager stripeManager, @Nonnull StripeManager stripeManager,
@Nonnull ServerZkReceiptOperations zkReceiptOperations, @Nonnull ServerZkReceiptOperations zkReceiptOperations,
@Nonnull IssuedReceiptsManager issuedReceiptsManager, @Nonnull IssuedReceiptsManager issuedReceiptsManager,
@Nonnull BadgeTranslator badgeTranslator) { @Nonnull BadgeTranslator badgeTranslator) {
this.clock = Objects.requireNonNull(clock); this.clock = Objects.requireNonNull(clock);
this.config = Objects.requireNonNull(config); this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration);
this.boostConfiguration = Objects.requireNonNull(boostConfiguration);
this.subscriptionManager = Objects.requireNonNull(subscriptionManager); this.subscriptionManager = Objects.requireNonNull(subscriptionManager);
this.stripeManager = Objects.requireNonNull(stripeManager); this.stripeManager = Objects.requireNonNull(stripeManager);
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations); this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
@ -288,7 +293,7 @@ public class SubscriptionController {
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
.thenApply(this::requireRecordFromGetResult) .thenApply(this::requireRecordFromGetResult)
.thenCompose(record -> { .thenCompose(record -> {
SubscriptionLevelConfiguration levelConfiguration = config.getLevels().get(level); SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level);
if (levelConfiguration == null) { if (levelConfiguration == null) {
throw new BadRequestException(Response.status(Status.BAD_REQUEST) throw new BadRequestException(Response.status(Status.BAD_REQUEST)
.entity(new SetSubscriptionLevelErrorResponse(List.of( .entity(new SetSubscriptionLevelErrorResponse(List.of(
@ -307,12 +312,14 @@ public class SubscriptionController {
} }
if (record.subscriptionId == null) { if (record.subscriptionId == null) {
long lastSubscriptionCreatedAt = record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0; long lastSubscriptionCreatedAt =
record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0;
// we don't have one yet so create it and then record the subscription id // we don't have one yet so create it and then record the subscription id
// //
// this relies on stripe's idempotency key to avoid creating more than one subscription if the client // this relies on stripe's idempotency key to avoid creating more than one subscription if the client
// retries this request // retries this request
return stripeManager.createSubscription(record.customerId, priceConfiguration.getId(), level, lastSubscriptionCreatedAt) return stripeManager.createSubscription(record.customerId, priceConfiguration.getId(), level,
lastSubscriptionCreatedAt)
.thenCompose(subscription -> subscriptionManager.subscriptionCreated( .thenCompose(subscription -> subscriptionManager.subscriptionCreated(
requestData.subscriberUser, subscription.getId(), requestData.now, level) requestData.subscriberUser, subscription.getId(), requestData.now, level)
.thenApply(unused -> subscription)); .thenApply(unused -> subscription));
@ -381,7 +388,7 @@ public class SubscriptionController {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext); List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
GetLevelsResponse getLevelsResponse = new GetLevelsResponse( GetLevelsResponse getLevelsResponse = new GetLevelsResponse(
config.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey, subscriptionConfiguration.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey,
entry -> new GetLevelsResponse.Level( entry -> new GetLevelsResponse.Level(
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()), badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
entry.getValue().getPrices().entrySet().stream().collect( entry.getValue().getPrices().entrySet().stream().collect(
@ -391,6 +398,17 @@ public class SubscriptionController {
}); });
} }
@Timed
@GET
@Path("/boost/amounts")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> getBoostAmounts() {
return CompletableFuture.supplyAsync(() -> Response.ok(
boostConfiguration.getCurrencies().entrySet().stream().collect(
Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), Function.identity()))).build());
}
public static class GetSubscriptionInformationResponse { public static class GetSubscriptionInformationResponse {
public static class Subscription { public static class Subscription {
@ -619,7 +637,7 @@ public class SubscriptionController {
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt( return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()) Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
.plus(config.getBadgeGracePeriod()) .plus(subscriptionConfiguration.getBadgeGracePeriod())
.truncatedTo(ChronoUnit.DAYS) .truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS), .plus(1, ChronoUnit.DAYS),
stripeManager.getLevelForProduct(product), stripeManager.getLevelForProduct(product),

View File

@ -6,9 +6,11 @@
package org.whispersystems.textsecuregcm.util; package org.whispersystems.textsecuregcm.util;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -17,7 +19,7 @@ import java.lang.annotation.Target;
import javax.validation.Constraint; import javax.validation.Constraint;
import javax.validation.Payload; import javax.validation.Payload;
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Target({ FIELD, METHOD, CONSTRUCTOR, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME) @Retention(RUNTIME)
@Constraint(validatedBy = { @Constraint(validatedBy = {
ExactlySizeValidatorForString.class, ExactlySizeValidatorForString.class,

View File

@ -29,6 +29,7 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
import org.whispersystems.textsecuregcm.badges.BadgeTranslator; import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
@ -45,14 +46,15 @@ class SubscriptionControllerTest {
private static final Clock CLOCK = mock(Clock.class); private static final Clock CLOCK = mock(Clock.class);
private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = mock(SubscriptionConfiguration.class); private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = mock(SubscriptionConfiguration.class);
private static final BoostConfiguration BOOST_CONFIG = mock(BoostConfiguration.class);
private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class); private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class);
private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class);
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class); private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class); private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class); private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController( private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, CLOCK, SUBSCRIPTION_CONFIG, BOOST_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS,
BADGE_TRANSLATOR); ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR);
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
.addProvider(AuthHelper.getAuthFilter()) .addProvider(AuthHelper.getAuthFilter())