Return a badge with additional properties when fetching your own profile
This commit is contained in:
parent
5c1cde1b28
commit
44bc90e5ab
|
@ -6,7 +6,9 @@
|
||||||
package org.whispersystems.textsecuregcm.badges;
|
package org.whispersystems.textsecuregcm.badges;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.net.URL;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -20,6 +22,7 @@ import java.util.stream.Collectors;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.SelfBadge;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||||
|
|
||||||
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
||||||
|
@ -54,7 +57,8 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
||||||
@Override
|
@Override
|
||||||
public List<Badge> convert(
|
public List<Badge> convert(
|
||||||
final List<Locale> acceptableLanguages,
|
final List<Locale> acceptableLanguages,
|
||||||
final List<AccountBadge> accountBadges) {
|
final List<AccountBadge> accountBadges,
|
||||||
|
final boolean isSelf) {
|
||||||
if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) {
|
if (accountBadges.isEmpty() && badgeIdsEnabledForAll.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
@ -95,23 +99,45 @@ public class ConfiguredProfileBadgeConverter implements ProfileBadgeConverter {
|
||||||
&& knownBadges.containsKey(accountBadge.getId()))
|
&& knownBadges.containsKey(accountBadge.getId()))
|
||||||
.map(accountBadge -> {
|
.map(accountBadge -> {
|
||||||
BadgeConfiguration configuration = knownBadges.get(accountBadge.getId());
|
BadgeConfiguration configuration = knownBadges.get(accountBadge.getId());
|
||||||
return new Badge(
|
return newBadge(
|
||||||
|
isSelf,
|
||||||
accountBadge.getId(),
|
accountBadge.getId(),
|
||||||
configuration.getCategory(),
|
configuration.getCategory(),
|
||||||
configuration.getImageUrl(),
|
configuration.getImageUrl(),
|
||||||
resourceBundle.getString(accountBadge.getId() + "_name"),
|
resourceBundle.getString(accountBadge.getId() + "_name"),
|
||||||
resourceBundle.getString(accountBadge.getId() + "_description"));
|
resourceBundle.getString(accountBadge.getId() + "_description"),
|
||||||
|
accountBadge.getExpiration(),
|
||||||
|
accountBadge.isVisible());
|
||||||
})
|
})
|
||||||
.collect(Collectors.toCollection(ArrayList::new));
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> {
|
badges.addAll(badgeIdsEnabledForAll.stream().filter(knownBadges::containsKey).map(id -> {
|
||||||
BadgeConfiguration configuration = knownBadges.get(id);
|
BadgeConfiguration configuration = knownBadges.get(id);
|
||||||
return new Badge(
|
return newBadge(
|
||||||
|
isSelf,
|
||||||
id,
|
id,
|
||||||
configuration.getCategory(),
|
configuration.getCategory(),
|
||||||
configuration.getImageUrl(),
|
configuration.getImageUrl(),
|
||||||
resourceBundle.getString(id + "_name"),
|
resourceBundle.getString(id + "_name"),
|
||||||
resourceBundle.getString(id + "_description"));
|
resourceBundle.getString(id + "_description"),
|
||||||
|
now.plus(Duration.ofDays(1)),
|
||||||
|
true);
|
||||||
}).collect(Collectors.toList()));
|
}).collect(Collectors.toList()));
|
||||||
return badges;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,5 @@ public interface ProfileBadgeConverter {
|
||||||
* Converts the {@link AccountBadge}s for an account into the objects
|
* Converts the {@link AccountBadge}s for an account into the objects
|
||||||
* that can be returned on a profile fetch.
|
* 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,8 +243,11 @@ public class ProfileController {
|
||||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isSelf = false;
|
||||||
if (requestAccount.isPresent()) {
|
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);
|
Optional<Account> accountProfile = accountsManager.get(uuid);
|
||||||
|
@ -282,7 +285,7 @@ public class ProfileController {
|
||||||
UserCapabilities.createForAccount(accountProfile.get()),
|
UserCapabilities.createForAccount(accountProfile.get()),
|
||||||
username.orElse(null),
|
username.orElse(null),
|
||||||
null,
|
null,
|
||||||
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges()),
|
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
|
||||||
credential.orElse(null)));
|
credential.orElse(null)));
|
||||||
} catch (InvalidInputException e) {
|
} catch (InvalidInputException e) {
|
||||||
logger.info("Bad profile request", e);
|
logger.info("Bad profile request", e);
|
||||||
|
@ -291,26 +294,28 @@ public class ProfileController {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/username/{username}")
|
@Path("/username/{username}")
|
||||||
public Profile getProfileByUsername(
|
public Profile getProfileByUsername(
|
||||||
@Auth AuthenticatedAccount auth,
|
@Auth AuthenticatedAccount auth,
|
||||||
@Context ContainerRequestContext containerRequestContext,
|
@Context ContainerRequestContext containerRequestContext,
|
||||||
@PathParam("username") String username)
|
@PathParam("username") String username)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
rateLimiters.getUsernameLookupLimiter().validate(auth.getAccount().getUuid());
|
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()) {
|
if (uuid.isEmpty()) {
|
||||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
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()) {
|
if (accountProfile.isEmpty()) {
|
||||||
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
|
||||||
|
@ -328,7 +333,10 @@ public class ProfileController {
|
||||||
UserCapabilities.createForAccount(accountProfile.get()),
|
UserCapabilities.createForAccount(accountProfile.get()),
|
||||||
username,
|
username,
|
||||||
accountProfile.get().getUuid(),
|
accountProfile.get().getUuid(),
|
||||||
profileBadgeConverter.convert(getAcceptableLanguagesForRequest(containerRequestContext), accountProfile.get().getBadges()),
|
profileBadgeConverter.convert(
|
||||||
|
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||||
|
accountProfile.get().getBadges(),
|
||||||
|
isSelf),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,8 +390,11 @@ public class ProfileController {
|
||||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isSelf = false;
|
||||||
if (auth.isPresent()) {
|
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);
|
Optional<Account> accountProfile = accountsManager.get(identifier);
|
||||||
|
@ -403,11 +414,13 @@ public class ProfileController {
|
||||||
UserCapabilities.createForAccount(accountProfile.get()),
|
UserCapabilities.createForAccount(accountProfile.get()),
|
||||||
username.orElse(null),
|
username.orElse(null),
|
||||||
null,
|
null,
|
||||||
profileBadgeConverter.convert(getAcceptableLanguagesForRequest(containerRequestContext), accountProfile.get().getBadges()),
|
profileBadgeConverter.convert(
|
||||||
|
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||||
|
accountProfile.get().getBadges(),
|
||||||
|
isSelf),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@Timed
|
@Timed
|
||||||
@GET
|
@GET
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,7 +111,7 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||||
BadgesConfiguration badgesConfiguration = createBadges(1);
|
BadgesConfiguration badgesConfiguration = createBadges(1);
|
||||||
ConfiguredProfileBadgeConverter badgeConverter = new ConfiguredProfileBadgeConverter(clock, badgesConfiguration,
|
ConfiguredProfileBadgeConverter badgeConverter = new ConfiguredProfileBadgeConverter(clock, badgesConfiguration,
|
||||||
resourceBundleFactory);
|
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
|
@ParameterizedTest
|
||||||
|
@ -123,11 +123,13 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||||
setupResourceBundle(Locale.getDefault());
|
setupResourceBundle(Locale.getDefault());
|
||||||
|
|
||||||
if (expectedBadge != null) {
|
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)
|
.hasSize(1)
|
||||||
.containsOnly(expectedBadge);
|
.containsOnly(expectedBadge);
|
||||||
} else {
|
} 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();
|
.isEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,7 +163,7 @@ public class ConfiguredProfileBadgeConverterTest {
|
||||||
|
|
||||||
ArgumentCaptor<Control> controlArgumentCaptor = setupResourceBundle(enGb);
|
ArgumentCaptor<Control> controlArgumentCaptor = setupResourceBundle(enGb);
|
||||||
badgeConverter.convert(List.of(enGb, en, esUs),
|
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();
|
Control control = controlArgumentCaptor.getValue();
|
||||||
|
|
||||||
assertThatNullPointerException().isThrownBy(() -> control.getFormats(null));
|
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
|
// this should always terminate at the system default locale since the development defined bundle should get
|
||||||
// returned at that point anyhow
|
// returned at that point anyhow
|
||||||
badgeConverter.convert(List.of(enGb, Locale.getDefault(), en, esUs),
|
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();
|
Control control2 = controlArgumentCaptor.getValue();
|
||||||
|
|
||||||
assertThat(control2.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, enGb)).isEqualTo(
|
assertThat(control2.getFallbackLocale(ConfiguredProfileBadgeConverter.BASE_NAME, enGb)).isEqualTo(
|
||||||
|
|
|
@ -117,7 +117,7 @@ class ProfileControllerTest {
|
||||||
profilesManager,
|
profilesManager,
|
||||||
usernamesManager,
|
usernamesManager,
|
||||||
dynamicConfigurationManager,
|
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 Badge("TEST", "other", makeURL("https://example.com/badge/test"), "Test Badge", "This badge is in unit tests.")
|
||||||
),
|
),
|
||||||
new BadgesConfiguration(List.of(
|
new BadgesConfiguration(List.of(
|
||||||
|
|
Loading…
Reference in New Issue