Bring badge configuration into levels information
This commit is contained in:
parent
fe21d014f7
commit
c0837104cd
|
@ -72,7 +72,6 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
|
|||
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
|
||||
|
@ -296,7 +295,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
ProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(clock, config.getBadges());
|
||||
ConfiguredProfileBadgeConverter profileBadgeConverter =
|
||||
new ConfiguredProfileBadgeConverter(clock, config.getBadges());
|
||||
|
||||
JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
|
||||
Jdbi accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
|
||||
|
@ -636,7 +636,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
);
|
||||
if (config.getSubscription() != null) {
|
||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), subscriptionManager,
|
||||
stripeManager, zkReceiptOperations, issuedReceiptsManager));
|
||||
stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter));
|
||||
}
|
||||
|
||||
for (Object controller : commonControllers) {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.badges;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
|
||||
public interface BadgeTranslator {
|
||||
Badge translate(List<Locale> acceptableLanguages, String badgeId);
|
||||
}
|
|
@ -18,13 +18,14 @@ import java.util.ResourceBundle;
|
|||
import java.util.ResourceBundle.Control;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.entities.SelfBadge;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
|
||||
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
||||
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter, BadgeTranslator {
|
||||
|
||||
private static final int MAX_LOCALES = 15;
|
||||
@VisibleForTesting
|
||||
|
@ -53,6 +54,23 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
|||
this.resourceBundleFactory = resourceBundleFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Badge translate(final List<Locale> acceptableLanguages, final String badgeId) {
|
||||
final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
|
||||
final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
|
||||
final BadgeConfiguration configuration = knownBadges.get(badgeId);
|
||||
return newBadge(
|
||||
false,
|
||||
configuration.getId(),
|
||||
configuration.getCategory(),
|
||||
resourceBundle.getString(configuration.getId() + "_name"),
|
||||
resourceBundle.getString(configuration.getId() + "_description"),
|
||||
configuration.getSprites(),
|
||||
configuration.getSvgs(),
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Badge> convert(
|
||||
final List<Locale> acceptableLanguages,
|
||||
|
@ -63,35 +81,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
|||
}
|
||||
|
||||
final Instant now = clock.instant();
|
||||
|
||||
final List<Locale> acceptableLocales = acceptableLanguages.stream().limit(MAX_LOCALES).distinct()
|
||||
.collect(Collectors.toList());
|
||||
final Locale desiredLocale = acceptableLocales.isEmpty() ? Locale.getDefault() : acceptableLocales.get(0);
|
||||
|
||||
// define a control with a fallback order as specified in the header
|
||||
ResourceBundle.Control control = new Control() {
|
||||
@Override
|
||||
public List<String> getFormats(final String baseName) {
|
||||
Objects.requireNonNull(baseName);
|
||||
return Control.FORMAT_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getFallbackLocale(final String baseName, final Locale locale) {
|
||||
Objects.requireNonNull(baseName);
|
||||
if (locale.equals(Locale.getDefault())) {
|
||||
return null;
|
||||
}
|
||||
final int localeIndex = acceptableLocales.indexOf(locale);
|
||||
if (localeIndex < 0 || localeIndex >= acceptableLocales.size() - 1) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
// [0, acceptableLocales.size() - 2] is now the possible range for localeIndex
|
||||
return acceptableLocales.get(localeIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
final ResourceBundle resourceBundle = resourceBundleFactory.createBundle(BASE_NAME, desiredLocale, control);
|
||||
final List<Locale> acceptableLocales = getAcceptableLocales(acceptableLanguages);
|
||||
final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales);
|
||||
List<Badge> badges = accountBadges.stream()
|
||||
.filter(accountBadge -> (isSelf || accountBadge.isVisible())
|
||||
&& now.isBefore(accountBadge.getExpiration())
|
||||
|
@ -126,6 +117,40 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
|||
return badges;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private ResourceBundle getResourceBundle(final List<Locale> acceptableLocales) {
|
||||
final Locale desiredLocale = acceptableLocales.isEmpty() ? Locale.getDefault() : acceptableLocales.get(0);
|
||||
// define a control with a fallback order as specified in the header
|
||||
Control control = new Control() {
|
||||
@Override
|
||||
public List<String> getFormats(final String baseName) {
|
||||
Objects.requireNonNull(baseName);
|
||||
return Control.FORMAT_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getFallbackLocale(final String baseName, final Locale locale) {
|
||||
Objects.requireNonNull(baseName);
|
||||
if (locale.equals(Locale.getDefault())) {
|
||||
return null;
|
||||
}
|
||||
final int localeIndex = acceptableLocales.indexOf(locale);
|
||||
if (localeIndex < 0 || localeIndex >= acceptableLocales.size() - 1) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
// [0, acceptableLocales.size() - 2] is now the possible range for localeIndex
|
||||
return acceptableLocales.get(localeIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return resourceBundleFactory.createBundle(BASE_NAME, desiredLocale, control);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
|
||||
return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Badge newBadge(
|
||||
final boolean isSelf,
|
||||
final String id,
|
||||
|
|
|
@ -44,7 +44,10 @@ import javax.ws.rs.POST;
|
|||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
@ -56,9 +59,11 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
|
||||
|
@ -76,6 +81,7 @@ public class SubscriptionController {
|
|||
private final StripeManager stripeManager;
|
||||
private final ServerZkReceiptOperations zkReceiptOperations;
|
||||
private final IssuedReceiptsManager issuedReceiptsManager;
|
||||
private final BadgeTranslator badgeTranslator;
|
||||
|
||||
public SubscriptionController(
|
||||
@Nonnull Clock clock,
|
||||
|
@ -83,13 +89,15 @@ public class SubscriptionController {
|
|||
@Nonnull SubscriptionManager subscriptionManager,
|
||||
@Nonnull StripeManager stripeManager,
|
||||
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
|
||||
@Nonnull IssuedReceiptsManager issuedReceiptsManager) {
|
||||
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
|
||||
@Nonnull BadgeTranslator badgeTranslator) {
|
||||
this.clock = Objects.requireNonNull(clock);
|
||||
this.config = Objects.requireNonNull(config);
|
||||
this.subscriptionManager = Objects.requireNonNull(subscriptionManager);
|
||||
this.stripeManager = Objects.requireNonNull(stripeManager);
|
||||
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
|
||||
this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
|
||||
this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
@ -328,19 +336,19 @@ public class SubscriptionController {
|
|||
|
||||
public static class Level {
|
||||
|
||||
private final String badgeId;
|
||||
private final Badge badge;
|
||||
private final Map<String, BigDecimal> currencies;
|
||||
|
||||
@JsonCreator
|
||||
public Level(
|
||||
@JsonProperty("badgeId") String badgeId,
|
||||
@JsonProperty("badge") Badge badge,
|
||||
@JsonProperty("currencies") Map<String, BigDecimal> currencies) {
|
||||
this.badgeId = badgeId;
|
||||
this.badge = badge;
|
||||
this.currencies = currencies;
|
||||
}
|
||||
|
||||
public String getBadgeId() {
|
||||
return badgeId;
|
||||
public Badge getBadge() {
|
||||
return badge;
|
||||
}
|
||||
|
||||
public Map<String, BigDecimal> getCurrencies() {
|
||||
|
@ -366,11 +374,13 @@ public class SubscriptionController {
|
|||
@Path("/levels")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public CompletableFuture<Response> getLevels() {
|
||||
public CompletableFuture<Response> getLevels(@Context ContainerRequestContext containerRequestContext) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
||||
GetLevelsResponse getLevelsResponse = new GetLevelsResponse(
|
||||
config.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey,
|
||||
entry -> new GetLevelsResponse.Level(entry.getValue().getBadge(),
|
||||
entry -> new GetLevelsResponse.Level(
|
||||
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
|
||||
entry.getValue().getPrices().entrySet().stream().collect(
|
||||
Collectors.toMap(levelEntry -> levelEntry.getKey().toUpperCase(Locale.ROOT),
|
||||
levelEntry -> levelEntry.getValue().getAmount()))))));
|
||||
|
@ -620,6 +630,15 @@ public class SubscriptionController {
|
|||
}
|
||||
}
|
||||
|
||||
private List<Locale> getAcceptableLanguagesForRequest(ContainerRequestContext containerRequestContext) {
|
||||
try {
|
||||
return containerRequestContext.getAcceptableLanguages();
|
||||
} catch (final ProcessingException e) {
|
||||
logger.warn("Could not get acceptable languages", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestData {
|
||||
|
||||
public final byte[] subscriberBytes;
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -15,6 +17,7 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
|||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
|
@ -25,10 +28,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLevelsResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.stripe.StripeManager;
|
||||
|
@ -44,8 +49,10 @@ class SubscriptionControllerTest {
|
|||
private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class);
|
||||
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
|
||||
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
|
||||
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
|
||||
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
|
||||
CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER);
|
||||
CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
|
||||
BADGE_TRANSLATOR);
|
||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
|
@ -58,7 +65,8 @@ class SubscriptionControllerTest {
|
|||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
reset(CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER);
|
||||
reset(CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
|
||||
BADGE_TRANSLATOR);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -68,6 +76,12 @@ class SubscriptionControllerTest {
|
|||
2L, new SubscriptionLevelConfiguration("B2", "P2", Map.of("USD", new SubscriptionPriceConfiguration("R2", BigDecimal.valueOf(200)))),
|
||||
3L, new SubscriptionLevelConfiguration("B3", "P3", Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300))))
|
||||
));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
|
||||
when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), List.of("s", "m", "M", "S")));
|
||||
|
||||
GetLevelsResponse response = RESOURCE_EXTENSION.target("/v1/subscription/levels")
|
||||
.request()
|
||||
|
@ -75,19 +89,19 @@ class SubscriptionControllerTest {
|
|||
|
||||
assertThat(response.getLevels()).containsKeys(1L, 2L, 3L).satisfies(longLevelMap -> {
|
||||
assertThat(longLevelMap).extractingByKey(1L).satisfies(level -> {
|
||||
assertThat(level.getBadgeId()).isEqualTo("B1");
|
||||
assertThat(level.getBadge().getId()).isEqualTo("B1");
|
||||
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
|
||||
assertThat(price).isEqualTo("100");
|
||||
});
|
||||
});
|
||||
assertThat(longLevelMap).extractingByKey(2L).satisfies(level -> {
|
||||
assertThat(level.getBadgeId()).isEqualTo("B2");
|
||||
assertThat(level.getBadge().getId()).isEqualTo("B2");
|
||||
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
|
||||
assertThat(price).isEqualTo("200");
|
||||
});
|
||||
});
|
||||
assertThat(longLevelMap).extractingByKey(3L).satisfies(level -> {
|
||||
assertThat(level.getBadgeId()).isEqualTo("B3");
|
||||
assertThat(level.getBadge().getId()).isEqualTo("B3");
|
||||
assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
|
||||
assertThat(price).isEqualTo("300");
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue