From 44bc90e5abb47ea30ac749be9e828adf53aa9b77 Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Fri, 17 Sep 2021 13:20:13 -0500 Subject: [PATCH] Return a badge with additional properties when fetching your own profile --- .../ConfiguredProfileBadgeConverter.java | 36 +++++++++-- .../badges/ProfileBadgeConverter.java | 2 +- .../controllers/ProfileController.java | 57 ++++++++++------- .../textsecuregcm/entities/SelfBadge.java | 63 +++++++++++++++++++ .../ConfiguredProfileBadgeConverterTest.java | 12 ++-- .../controllers/ProfileControllerTest.java | 2 +- 6 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java 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 35fa65a28..9862a3f3b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverter.java @@ -6,7 +6,9 @@ package org.whispersystems.textsecuregcm.badges; import com.google.common.annotations.VisibleForTesting; +import java.net.URL; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -20,6 +22,7 @@ import java.util.stream.Collectors; 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 { @@ -54,7 +57,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter { @Override public List convert( final List acceptableLanguages, - final List accountBadges) { + final List accountBadges, + final boolean isSelf) { if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) { return List.of(); } @@ -95,23 +99,45 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter { && knownBadges.containsKey(accountBadge.getId())) .map(accountBadge -> { BadgeConfiguration configuration = knownBadges.get(accountBadge.getId()); - return new Badge( + return newBadge( + isSelf, accountBadge.getId(), configuration.getCategory(), configuration.getImageUrl(), resourceBundle.getString(accountBadge.getId() + "_name"), - resourceBundle.getString(accountBadge.getId() + "_description")); + resourceBundle.getString(accountBadge.getId() + "_description"), + accountBadge.getExpiration(), + accountBadge.isVisible()); }) .collect(Collectors.toCollection(ArrayList::new)); badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> { BadgeConfiguration configuration = knownBadges.get(id); - return new Badge( + return newBadge( + isSelf, id, configuration.getCategory(), configuration.getImageUrl(), resourceBundle.getString(id + "_name"), - resourceBundle.getString(id + "_description")); + resourceBundle.getString(id + "_description"), + now.plus(Duration.ofDays(1)), + true); }).collect(Collectors.toList())); return badges; } + + private Badge newBadge( + final boolean isSelf, + final String id, + final String category, + final URL imageUrl, + final String name, + final String description, + final Instant expiration, + final boolean visible) { + if (isSelf) { + return new SelfBadge(id, category, imageUrl, name, description, expiration, visible); + } else { + return new Badge(id, category, imageUrl, name, description); + } + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java index a47710e99..b36f8946f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/badges/ProfileBadgeConverter.java @@ -16,5 +16,5 @@ public interface ProfileBadgeConverter { * Converts the {@link AccountBadge}s for an account into the objects * that can be returned on a profile fetch. */ - List convert(List acceptableLanguages, List accountBadges); + List convert(List acceptableLanguages, List accountBadges, boolean isSelf); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 720c3a6c5..4b6dfecbe 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -243,8 +243,11 @@ public class ProfileController { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } + boolean isSelf = false; if (requestAccount.isPresent()) { - rateLimiters.getProfileLimiter().validate(requestAccount.get().getUuid()); + UUID authedUuid = requestAccount.get().getUuid(); + rateLimiters.getProfileLimiter().validate(authedUuid); + isSelf = uuid.equals(authedUuid); } Optional accountProfile = accountsManager.get(uuid); @@ -282,7 +285,7 @@ public class ProfileController { UserCapabilities.createForAccount(accountProfile.get()), username.orElse(null), null, - profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges()), + profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf), credential.orElse(null))); } catch (InvalidInputException e) { logger.info("Bad profile request", e); @@ -291,26 +294,28 @@ public class ProfileController { } - @Timed - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/username/{username}") - public Profile getProfileByUsername( - @Auth AuthenticatedAccount auth, - @Context ContainerRequestContext containerRequestContext, - @PathParam("username") String username) - throws RateLimitExceededException { - rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid()); + @Timed + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/username/{username}") + public Profile getProfileByUsername( + @Auth AuthenticatedAccount auth, + @Context ContainerRequestContext containerRequestContext, + @PathParam("username") String username) + throws RateLimitExceededException { + rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid()); - username = username.toLowerCase(); + username = username.toLowerCase(); - Optional uuid = usernamesManager.get(username); + Optional uuid = usernamesManager.get(username); - if (uuid.isEmpty()) { - throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); - } + if (uuid.isEmpty()) { + throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); + } - Optional accountProfile = accountsManager.get(uuid.get()); + final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get()); + + Optional accountProfile = accountsManager.get(uuid.get()); if (accountProfile.isEmpty()) { throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build()); @@ -328,7 +333,10 @@ public class ProfileController { UserCapabilities.createForAccount(accountProfile.get()), username, accountProfile.get().getUuid(), - profileBadgeConverter.convert(getAcceptableLanguagesForRequest(containerRequestContext), accountProfile.get().getBadges()), + profileBadgeConverter.convert( + getAcceptableLanguagesForRequest(containerRequestContext), + accountProfile.get().getBadges(), + isSelf), null); } @@ -382,8 +390,11 @@ public class ProfileController { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } + boolean isSelf = false; if (auth.isPresent()) { - rateLimiters.getProfileLimiter().validate(auth.get().getAccount().getUuid()); + UUID authedUuid = auth.get().getAccount().getUuid(); + rateLimiters.getProfileLimiter().validate(authedUuid); + isSelf = authedUuid.equals(identifier); } Optional accountProfile = accountsManager.get(identifier); @@ -403,11 +414,13 @@ public class ProfileController { UserCapabilities.createForAccount(accountProfile.get()), username.orElse(null), null, - profileBadgeConverter.convert(getAcceptableLanguagesForRequest(containerRequestContext), accountProfile.get().getBadges()), + profileBadgeConverter.convert( + getAcceptableLanguagesForRequest(containerRequestContext), + accountProfile.get().getBadges(), + isSelf), null); } - @Deprecated @Timed @GET diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java new file mode 100644 index 000000000..891d5f815 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/SelfBadge.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.net.URL; +import java.time.Instant; +import java.util.Objects; + +/** + * Extension of the Badge object returned when asking for one's own badges. + */ +public class SelfBadge extends Badge { + private final Instant expiration; + private final boolean visible; + + @JsonCreator + + public SelfBadge( + @JsonProperty("id") final String id, + @JsonProperty("category") final String category, + @JsonProperty("imageUrl") final URL imageUrl, + @JsonProperty("name") final String name, + @JsonProperty("description") final String description, + @JsonProperty("expiration") final Instant expiration, + @JsonProperty("visible") final boolean visible) { + super(id, category, imageUrl, name, description); + this.expiration = expiration; + this.visible = visible; + } + + public Instant getExpiration() { + return expiration; + } + + public boolean isVisible() { + return visible; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + SelfBadge selfBadge = (SelfBadge) o; + return visible == selfBadge.visible && Objects.equals(expiration, selfBadge.expiration); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), expiration, visible); + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java index b58d5a168..52beed908 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/badges/ConfiguredProfileBadgeConverterTest.java @@ -111,7 +111,7 @@ public class ConfiguredProfileBadgeConverterTest { BadgesConfiguration badgesConfiguration = createBadges(1); ConfiguredProfileBadgeConverter badgeConverter = new ConfiguredProfileBadgeConverter(clock, badgesConfiguration, resourceBundleFactory); - assertThat(badgeConverter.convert(List.of(Locale.getDefault()), List.of())).isNotNull().isEmpty(); + assertThat(badgeConverter.convert(List.of(Locale.getDefault()), List.of(), false)).isNotNull().isEmpty(); } @ParameterizedTest @@ -123,11 +123,13 @@ public class ConfiguredProfileBadgeConverterTest { setupResourceBundle(Locale.getDefault()); if (expectedBadge != null) { - assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)))).isNotNull() + assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)), + false)).isNotNull() .hasSize(1) .containsOnly(expectedBadge); } else { - assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)))).isNotNull() + assertThat(badgeConverter.convert(List.of(), List.of(new AccountBadge(name, expiration, visible)), + false)).isNotNull() .isEmpty(); } } @@ -161,7 +163,7 @@ public class ConfiguredProfileBadgeConverterTest { ArgumentCaptor controlArgumentCaptor = setupResourceBundle(enGb); badgeConverter.convert(List.of(enGb, en, esUs), - List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true))); + List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true)), false); Control control = controlArgumentCaptor.getValue(); assertThatNullPointerException().isThrownBy(() -> control.getFormats(null)); @@ -186,7 +188,7 @@ public class ConfiguredProfileBadgeConverterTest { // this should always terminate at the system default locale since the development defined bundle should get // returned at that point anyhow badgeConverter.convert(List.of(enGb, Locale.getDefault(), en, esUs), - List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true))); + List.of(new AccountBadge(idFor(0), Instant.ofEpochSecond(43), true)), false); Control control2 = controlArgumentCaptor.getValue(); assertThat(control2.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, enGb)).isEqualTo( diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java index 162e6db8c..e087aee02 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java @@ -117,7 +117,7 @@ class ProfileControllerTest { profilesManager, usernamesManager, dynamicConfigurationManager, - (acceptableLanguages, accountBadges) -> List.of( + (acceptableLanguages, accountBadges, isSelf) -> List.of( new Badge("TEST", "other", makeURL("https://example.com/badge/test"), "Test Badge", "This badge is in unit tests.") ), new BadgesConfiguration(List.of(