Return a badge with additional properties when fetching your own profile

This commit is contained in:
Ehren Kret 2021-09-17 13:20:13 -05:00
parent 5c1cde1b28
commit 44bc90e5ab
6 changed files with 138 additions and 34 deletions

View File

@ -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<Badge> convert(
final List<Locale> acceptableLanguages,
final List<AccountBadge> accountBadges) {
final List<AccountBadge> 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);
}
}
}

View File

@ -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<Badge> convert(List<Locale> acceptableLanguages, List<AccountBadge> accountBadges);
List<Badge> convert(List<Locale> acceptableLanguages, List<AccountBadge> accountBadges, boolean isSelf);
}

View File

@ -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<Account> 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> uuid = usernamesManager.get(username);
Optional<UUID> 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<Account> accountProfile = accountsManager.get(uuid.get());
final boolean isSelf = auth.getAccount().getUuid().equals(uuid.get());
Optional<Account> 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<Account> 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

View File

@ -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);
}
}

View File

@ -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<Control> 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(

View File

@ -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(