From c0837104cd6038f75c45030a46a7daa4ba69b52a Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Thu, 14 Oct 2021 11:35:18 -0500 Subject: [PATCH] Bring badge configuration into levels information --- .../textsecuregcm/WhisperServerService.java | 6 +- .../textsecuregcm/badges/BadgeTranslator.java | 14 +++ .../ConfiguredProfileBadgeConverter.java | 85 ++++++++++++------- .../controllers/SubscriptionController.java | 35 ++++++-- .../SubscriptionControllerTest.java | 24 ++++-- 5 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/badges/BadgeTranslator.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 6a28dbd68..f5e16ef2f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -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 acceptableLanguages, String badgeId); +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java index 7297ec6d4..a41dc71c8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java @@ -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 acceptableLanguages, final String badgeId) { + final List 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 convert( final List acceptableLanguages, @@ -63,35 +81,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter { } final Instant now = clock.instant(); - - final List 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 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 acceptableLocales = getAcceptableLocales(acceptableLanguages); + final ResourceBundle resourceBundle = getResourceBundle(acceptableLocales); List 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 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 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 getAcceptableLocales(final List acceptableLanguages) { + return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList()); + } + private Badge newBadge( final boolean isSelf, final String id, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index b43331422..4345d9ad2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -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 currencies; @JsonCreator public Level( - @JsonProperty("badgeId") String badgeId, + @JsonProperty("badge") Badge badge, @JsonProperty("currencies") Map currencies) { - this.badgeId = badgeId; + this.badge = badge; this.currencies = currencies; } - public String getBadgeId() { - return badgeId; + public Badge getBadge() { + return badge; } public Map getCurrencies() { @@ -366,11 +374,13 @@ public class SubscriptionController { @Path("/levels") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public CompletableFuture getLevels() { + public CompletableFuture getLevels(@Context ContainerRequestContext containerRequestContext) { return CompletableFuture.supplyAsync(() -> { + List 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 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; diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index ff3aad58b..5ef7abe8b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -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"); });