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
	
	 Ehren Kret
						Ehren Kret