Return backup info at `/v1/subscription/configuration`
- Return the free tier media duration and storage allowance for backups - Add openapi annotations - Update default media storage allowance
This commit is contained in:
parent
65b2892de5
commit
10d559bbb5
|
@ -366,6 +366,8 @@ badges:
|
||||||
subscription: # configuration for Stripe subscriptions
|
subscription: # configuration for Stripe subscriptions
|
||||||
badgeExpiration: P30D
|
badgeExpiration: P30D
|
||||||
badgeGracePeriod: P15D
|
badgeGracePeriod: P15D
|
||||||
|
backupExpiration: P30D
|
||||||
|
backupFreeTierMediaDuration: P30D
|
||||||
levels:
|
levels:
|
||||||
500:
|
500:
|
||||||
badge: EXAMPLE
|
badge: EXAMPLE
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
package org.whispersystems.textsecuregcm.backup;
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.dropwizard.util.DataSize;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.micrometer.core.instrument.DistributionSummary;
|
import io.micrometer.core.instrument.DistributionSummary;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
@ -47,8 +48,8 @@ public class BackupManager {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
|
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
|
||||||
|
|
||||||
static final String MESSAGE_BACKUP_NAME = "messageBackup";
|
static final String MESSAGE_BACKUP_NAME = "messageBackup";
|
||||||
static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = 1024L * 1024L * 1024L * 50L;
|
public static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = DataSize.gibibytes(100).toBytes();
|
||||||
static final long MAX_MEDIA_OBJECT_SIZE = 1024L * 1024L * 101L;
|
static final long MAX_MEDIA_OBJECT_SIZE = DataSize.mebibytes(101).toBytes();
|
||||||
|
|
||||||
// If the last media usage recalculation is over MAX_QUOTA_STALENESS, force a recalculation before quota enforcement.
|
// If the last media usage recalculation is over MAX_QUOTA_STALENESS, force a recalculation before quota enforcement.
|
||||||
static final Duration MAX_QUOTA_STALENESS = Duration.ofDays(1);
|
static final Duration MAX_QUOTA_STALENESS = Duration.ofDays(1);
|
||||||
|
|
|
@ -29,6 +29,7 @@ public class SubscriptionConfiguration {
|
||||||
private final Duration badgeExpiration;
|
private final Duration badgeExpiration;
|
||||||
|
|
||||||
private final Duration backupExpiration;
|
private final Duration backupExpiration;
|
||||||
|
private final Duration backupFreeTierMediaDuration;
|
||||||
private final Map<Long, SubscriptionLevelConfiguration.Donation> donationLevels;
|
private final Map<Long, SubscriptionLevelConfiguration.Donation> donationLevels;
|
||||||
private final Map<Long, SubscriptionLevelConfiguration.Backup> backupLevels;
|
private final Map<Long, SubscriptionLevelConfiguration.Backup> backupLevels;
|
||||||
|
|
||||||
|
@ -37,10 +38,12 @@ public class SubscriptionConfiguration {
|
||||||
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
|
@JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
|
||||||
@JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
|
@JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
|
||||||
@JsonProperty("backupExpiration") @Valid Duration backupExpiration,
|
@JsonProperty("backupExpiration") @Valid Duration backupExpiration,
|
||||||
|
@JsonProperty("backupFreeTierMediaDuration") @Valid Duration backupFreeTierMediaDuration,
|
||||||
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Donation> donationLevels,
|
@JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Donation> donationLevels,
|
||||||
@JsonProperty("backupLevels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Backup> backupLevels) {
|
@JsonProperty("backupLevels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Backup> backupLevels) {
|
||||||
this.badgeGracePeriod = badgeGracePeriod;
|
this.badgeGracePeriod = badgeGracePeriod;
|
||||||
this.badgeExpiration = badgeExpiration;
|
this.badgeExpiration = badgeExpiration;
|
||||||
|
this.backupFreeTierMediaDuration = backupFreeTierMediaDuration;
|
||||||
this.donationLevels = donationLevels;
|
this.donationLevels = donationLevels;
|
||||||
this.backupExpiration = backupExpiration;
|
this.backupExpiration = backupExpiration;
|
||||||
this.backupLevels = backupLevels == null ? Collections.emptyMap() : backupLevels;
|
this.backupLevels = backupLevels == null ? Collections.emptyMap() : backupLevels;
|
||||||
|
@ -107,6 +110,10 @@ public class SubscriptionConfiguration {
|
||||||
return subscriptionLevels.values().stream().allMatch(level -> currencies.equals(level.prices().keySet()));
|
return subscriptionLevels.values().stream().allMatch(level -> currencies.equals(level.prices().keySet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Duration getbackupFreeTierMediaDuration() {
|
||||||
|
return backupFreeTierMediaDuration;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isValidBackupLevel(final long receiptLevel) {
|
private static boolean isValidBackupLevel(final long receiptLevel) {
|
||||||
try {
|
try {
|
||||||
BackupLevelUtil.fromReceiptLevel(receiptLevel);
|
BackupLevelUtil.fromReceiptLevel(receiptLevel);
|
||||||
|
@ -115,4 +122,5 @@ public class SubscriptionConfiguration {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ import io.dropwizard.auth.Auth;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.Tag;
|
||||||
import io.micrometer.core.instrument.Tags;
|
import io.micrometer.core.instrument.Tags;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -74,6 +78,7 @@ import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||||
import org.slf4j.Logger;
|
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.backup.BackupManager;
|
||||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||||
import org.whispersystems.textsecuregcm.badges.LevelTranslator;
|
import org.whispersystems.textsecuregcm.badges.LevelTranslator;
|
||||||
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
||||||
|
@ -200,18 +205,18 @@ public class SubscriptionController {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(
|
GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(
|
||||||
final List<Locale> acceptableLanguages) {
|
final List<Locale> acceptableLanguages) {
|
||||||
final Map<String, LevelConfiguration> levels = new HashMap<>();
|
final Map<String, LevelConfiguration> donationLevels = new HashMap<>();
|
||||||
|
|
||||||
subscriptionConfiguration.getDonationLevels().forEach((levelId, levelConfig) -> {
|
subscriptionConfiguration.getDonationLevels().forEach((levelId, levelConfig) -> {
|
||||||
final LevelConfiguration levelConfiguration = new LevelConfiguration(
|
final LevelConfiguration levelConfiguration = new LevelConfiguration(
|
||||||
levelTranslator.translate(acceptableLanguages, levelConfig.badge()),
|
levelTranslator.translate(acceptableLanguages, levelConfig.badge()),
|
||||||
badgeTranslator.translate(acceptableLanguages, levelConfig.badge()));
|
badgeTranslator.translate(acceptableLanguages, levelConfig.badge()));
|
||||||
levels.put(String.valueOf(levelId), levelConfiguration);
|
donationLevels.put(String.valueOf(levelId), levelConfiguration);
|
||||||
});
|
});
|
||||||
|
|
||||||
final Badge boostBadge = badgeTranslator.translate(acceptableLanguages,
|
final Badge boostBadge = badgeTranslator.translate(acceptableLanguages,
|
||||||
oneTimeDonationConfiguration.boost().badge());
|
oneTimeDonationConfiguration.boost().badge());
|
||||||
levels.put(String.valueOf(oneTimeDonationConfiguration.boost().level()),
|
donationLevels.put(String.valueOf(oneTimeDonationConfiguration.boost().level()),
|
||||||
new LevelConfiguration(
|
new LevelConfiguration(
|
||||||
boostBadge.getName(),
|
boostBadge.getName(),
|
||||||
// NB: the one-time badges are PurchasableBadge, which has a `duration` field
|
// NB: the one-time badges are PurchasableBadge, which has a `duration` field
|
||||||
|
@ -220,14 +225,22 @@ public class SubscriptionController {
|
||||||
oneTimeDonationConfiguration.boost().expiration())));
|
oneTimeDonationConfiguration.boost().expiration())));
|
||||||
|
|
||||||
final Badge giftBadge = badgeTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.gift().badge());
|
final Badge giftBadge = badgeTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.gift().badge());
|
||||||
levels.put(String.valueOf(oneTimeDonationConfiguration.gift().level()),
|
donationLevels.put(String.valueOf(oneTimeDonationConfiguration.gift().level()),
|
||||||
new LevelConfiguration(
|
new LevelConfiguration(
|
||||||
giftBadge.getName(),
|
giftBadge.getName(),
|
||||||
new PurchasableBadge(
|
new PurchasableBadge(
|
||||||
giftBadge,
|
giftBadge,
|
||||||
oneTimeDonationConfiguration.gift().expiration())));
|
oneTimeDonationConfiguration.gift().expiration())));
|
||||||
|
|
||||||
return new GetSubscriptionConfigurationResponse(buildCurrencyConfiguration(), levels, oneTimeDonationConfiguration.sepaMaximumEuros());
|
final Map<String, BackupLevelConfiguration> backupLevels = subscriptionConfiguration.getBackupLevels()
|
||||||
|
.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
e -> String.valueOf(e.getKey()),
|
||||||
|
ignored -> new BackupLevelConfiguration(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES)));
|
||||||
|
|
||||||
|
return new GetSubscriptionConfigurationResponse(buildCurrencyConfiguration(), donationLevels,
|
||||||
|
new BackupConfiguration(backupLevels, subscriptionConfiguration.getbackupFreeTierMediaDuration().toDays()),
|
||||||
|
oneTimeDonationConfiguration.sepaMaximumEuros());
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
|
@ -542,48 +555,61 @@ public class SubscriptionController {
|
||||||
== subscriptionConfiguration.getSubscriptionLevel(level2).type();
|
== subscriptionConfiguration.getSubscriptionLevel(level2).type();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Schema(description = """
|
||||||
* Comprehensive configuration for subscriptions and one-time donations
|
Comprehensive configuration for donation subscriptions, backup subscriptions, gift subscriptions, and one-time
|
||||||
*
|
donations pricing information for all levels are included in currencies. All levels that have an associated
|
||||||
* @param currencies map of lower-cased ISO 3 currency codes to minimums and level-specific scalar amounts
|
badge are included in levels. All levels that correspond to a backup payment tier are included in
|
||||||
* @param levels map of numeric level IDs to level-specific configuration
|
backupLevels.""")
|
||||||
*/
|
public record GetSubscriptionConfigurationResponse(
|
||||||
public record GetSubscriptionConfigurationResponse(Map<String, CurrencyConfiguration> currencies,
|
@Schema(description = "A map of lower-cased ISO 3 currency codes to minimums and level-specific scalar amounts")
|
||||||
|
Map<String, CurrencyConfiguration> currencies,
|
||||||
|
@Schema(description = "A map of numeric donation level IDs to level-specific badge configuration")
|
||||||
Map<String, LevelConfiguration> levels,
|
Map<String, LevelConfiguration> levels,
|
||||||
BigDecimal sepaMaximumEuros) {
|
@Schema(description = "Backup specific configuration")
|
||||||
|
BackupConfiguration backup,
|
||||||
|
@Schema(description = "The maximum value of a one-time donation SEPA transaction")
|
||||||
|
BigDecimal sepaMaximumEuros) {}
|
||||||
|
|
||||||
}
|
@Schema(description = "Configuration for a currency - use to present appropriate client interfaces")
|
||||||
|
public record CurrencyConfiguration(
|
||||||
/**
|
@Schema(description = "The minimum amount that may be submitted for a one-time donation in the currency")
|
||||||
* Configuration for a currency - use to present appropriate client interfaces
|
BigDecimal minimum,
|
||||||
*
|
@Schema(description = "A map of numeric one-time donation level IDs to the list of default amounts to be presented")
|
||||||
* @param minimum the minimum amount that may be submitted for a one-time donation in the currency
|
Map<String, List<BigDecimal>> oneTime,
|
||||||
* @param oneTime map of numeric one-time donation level IDs to the list of default amounts to be
|
@Schema(description = "A map of numeric subscription level IDs to the amount charged for that level")
|
||||||
* presented
|
|
||||||
* @param subscription map of numeric subscription level IDs to the amount charged for that level
|
|
||||||
* @param backupSubscription map of numeric backup level IDs to the amount charged for that level
|
|
||||||
* @param supportedPaymentMethods the payment methods that support the given currency
|
|
||||||
*/
|
|
||||||
public record CurrencyConfiguration(BigDecimal minimum, Map<String, List<BigDecimal>> oneTime,
|
|
||||||
Map<String, BigDecimal> subscription,
|
Map<String, BigDecimal> subscription,
|
||||||
|
@Schema(description = "A map of numeric backup level IDs to the amount charged for that level")
|
||||||
Map<String, BigDecimal> backupSubscription,
|
Map<String, BigDecimal> backupSubscription,
|
||||||
List<String> supportedPaymentMethods) {
|
@Schema(description = "The payment methods that support the given currency")
|
||||||
|
List<String> supportedPaymentMethods) {}
|
||||||
|
|
||||||
}
|
@Schema(description = "Configuration for a donation level - use to present appropriate client interfaces")
|
||||||
|
public record LevelConfiguration(
|
||||||
|
@Schema(description = "The localized name for the level")
|
||||||
|
String name,
|
||||||
|
@Schema(description = "The displayable badge associated with the level")
|
||||||
|
Badge badge) {}
|
||||||
|
|
||||||
/**
|
public record BackupConfiguration(
|
||||||
* Configuration for a donation level - use to present appropriate client interfaces
|
@Schema(description = "A map of numeric backup level IDs to level-specific backup configuration")
|
||||||
*
|
Map<String, BackupLevelConfiguration> levels,
|
||||||
* @param name the localized name for the level
|
@Schema(description = "The number of days of media a free tier backup user gets")
|
||||||
* @param badge the displayable badge associated with the level
|
long backupFreeTierMediaDays) {}
|
||||||
*/
|
|
||||||
public record LevelConfiguration(String name, Badge badge) {
|
|
||||||
|
|
||||||
}
|
@Schema(description = "Configuration for a backup level - use to present appropriate client interfaces")
|
||||||
|
public record BackupLevelConfiguration(
|
||||||
|
@Schema(description = "The amount of media storage in bytes that a paying subscriber may store")
|
||||||
|
long storageAllowanceBytes) {}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/configuration")
|
@Path("/configuration")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(
|
||||||
|
summary = "Subscription configuration ",
|
||||||
|
description = """
|
||||||
|
Returns all configuration for badges, donation subscriptions, backup subscriptions, and one-time donation (
|
||||||
|
"boost" and "gift") minimum and suggested amounts.""")
|
||||||
|
@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = GetSubscriptionConfigurationResponse.class)))
|
||||||
public CompletableFuture<Response> getConfiguration(@Context ContainerRequestContext containerRequestContext) {
|
public CompletableFuture<Response> getConfiguration(@Context ContainerRequestContext containerRequestContext) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
||||||
|
|
|
@ -64,6 +64,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
|
||||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||||
import org.whispersystems.textsecuregcm.badges.LevelTranslator;
|
import org.whispersystems.textsecuregcm.badges.LevelTranslator;
|
||||||
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
||||||
|
@ -1017,6 +1018,11 @@ class SubscriptionControllerTest {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertThat(response.backup().levels()).containsOnlyKeys("201").extractingByKey("201").satisfies(configuration -> {
|
||||||
|
assertThat(configuration.storageAllowanceBytes()).isEqualTo(BackupManager.MAX_TOTAL_BACKUP_MEDIA_BYTES);
|
||||||
|
});
|
||||||
|
assertThat(response.backup().backupFreeTierMediaDays()).isEqualTo(30);
|
||||||
|
|
||||||
// check the badge vs purchasable badge fields
|
// check the badge vs purchasable badge fields
|
||||||
// subscription levels are Badge, while one-time levels are PurchasableBadge, which adds `duration`
|
// subscription levels are Badge, while one-time levels are PurchasableBadge, which adds `duration`
|
||||||
Map<String, Object> genericResponse = RESOURCE_EXTENSION.target("/v1/subscription/configuration")
|
Map<String, Object> genericResponse = RESOURCE_EXTENSION.target("/v1/subscription/configuration")
|
||||||
|
@ -1068,6 +1074,7 @@ class SubscriptionControllerTest {
|
||||||
badgeExpiration: P30D
|
badgeExpiration: P30D
|
||||||
badgeGracePeriod: P15D
|
badgeGracePeriod: P15D
|
||||||
backupExpiration: P13D
|
backupExpiration: P13D
|
||||||
|
backupFreeTierMediaDuration: P30D
|
||||||
backupLevels:
|
backupLevels:
|
||||||
201:
|
201:
|
||||||
prices:
|
prices:
|
||||||
|
|
|
@ -361,6 +361,8 @@ badges:
|
||||||
subscription: # configuration for Stripe subscriptions
|
subscription: # configuration for Stripe subscriptions
|
||||||
badgeExpiration: P30D
|
badgeExpiration: P30D
|
||||||
badgeGracePeriod: P15D
|
badgeGracePeriod: P15D
|
||||||
|
backupExpiration: P30D
|
||||||
|
backupFreeTierMediaDuration: P30D
|
||||||
levels:
|
levels:
|
||||||
500:
|
500:
|
||||||
badge: EXAMPLE
|
badge: EXAMPLE
|
||||||
|
|
Loading…
Reference in New Issue