mirror of
https://github.com/signalapp/Signal-Server
synced 2026-10-06 10:47:57 +01:00
Add badges to remote config
This commit is contained in:
1 parent
257dd76b68
commit
8e9253181c
4 files changed
+189
-3
No files matched your search
@@ -110,6 +110,7 @@ import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
||||
import org.whispersystems.textsecuregcm.captcha.CaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
|
||||
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule;
|
||||
@@ -1105,7 +1106,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ProductConfigurationGrpcService(config.getSubscription(), config.getOneTimeDonations(),
|
||||
List.of(stripeManager, braintreeManager), profileBadgeConverter,
|
||||
config.getBackupConfiguration().maxTotalMediaSize()),
|
||||
new RemoteConfigurationGrpcService(remoteConfigsManager))
|
||||
new RemoteConfigurationGrpcService(remoteConfigsManager, profileBadgeConverter,
|
||||
config.getBadges().getBadges().stream()
|
||||
.map(BadgeConfiguration::getId)
|
||||
.toList()))
|
||||
.map(bindableService -> ServerInterceptors.intercept(bindableService,
|
||||
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
|
||||
// depends on the user-agent context so it has to come first here!
|
||||
|
||||
+51
-1
@@ -12,25 +12,49 @@ import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.signal.chat.common.Badge;
|
||||
import org.signal.chat.remoteconfiguration.Badges;
|
||||
import org.signal.chat.remoteconfiguration.Configuration;
|
||||
import org.signal.chat.remoteconfiguration.GetBadgesRequest;
|
||||
import org.signal.chat.remoteconfiguration.GetBadgesResponse;
|
||||
import org.signal.chat.remoteconfiguration.GetConfigurationRequest;
|
||||
import org.signal.chat.remoteconfiguration.GetConfigurationResponse;
|
||||
import org.signal.chat.remoteconfiguration.SimpleRemoteConfigurationGrpc;
|
||||
import org.signal.chat.remoteconfiguration.TaggedBadges;
|
||||
import org.signal.chat.remoteconfiguration.TaggedConfiguration;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
|
||||
public class RemoteConfigurationGrpcService extends SimpleRemoteConfigurationGrpc.RemoteConfigurationImplBase {
|
||||
|
||||
private static final GetConfigurationResponse CONFIGURATION_ETAG_MATCHED =
|
||||
GetConfigurationResponse.newBuilder().setEtagMatched(true).build();
|
||||
private static final GetBadgesResponse BADGES_ETAG_MATCHED =
|
||||
GetBadgesResponse.newBuilder().setEtagMatched(true).build();
|
||||
|
||||
private final RemoteConfigsManager remoteConfigsManager;
|
||||
private final BadgeTranslator badgeTranslator;
|
||||
private final List<String> badgeIds;
|
||||
|
||||
public RemoteConfigurationGrpcService(final RemoteConfigsManager remoteConfigsManager) {
|
||||
// Badge information varies based on the provided Accept-Language header. Here we cache the etag for the resolved
|
||||
// language, so if the caller provides a matching etag we don't have to build the full badge response.
|
||||
private final ConcurrentHashMap<Locale, ByteString> localeToBadgesEtag = new ConcurrentHashMap<>();
|
||||
|
||||
public RemoteConfigurationGrpcService(
|
||||
final RemoteConfigsManager remoteConfigsManager,
|
||||
final BadgeTranslator badgeTranslator,
|
||||
final List<String> badgeIds) {
|
||||
this.remoteConfigsManager = remoteConfigsManager;
|
||||
this.badgeTranslator = badgeTranslator;
|
||||
this.badgeIds = badgeIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +78,32 @@ public class RemoteConfigurationGrpcService extends SimpleRemoteConfigurationGrp
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetBadgesResponse getBadges(final GetBadgesRequest request) {
|
||||
final List<Locale> acceptableLanguages = RequestAttributesUtil.getAvailableAcceptedLocales();
|
||||
final Locale locale = badgeTranslator.resolveLocale(acceptableLanguages);
|
||||
|
||||
final ByteString cachedEtag = localeToBadgesEtag.get(locale);
|
||||
if (cachedEtag != null && cachedEtag.equals(request.getEtag())) {
|
||||
return BADGES_ETAG_MATCHED;
|
||||
}
|
||||
|
||||
final Map<String, Badge> badgesById = badgeIds.stream().collect(Collectors.toMap(
|
||||
Function.identity(),
|
||||
badgeId -> BadgeGrpcHelper.toGrpcBadge(badgeTranslator.translate(acceptableLanguages, badgeId))));
|
||||
final Badges badges = Badges.newBuilder().putAllBadges(badgesById).build();
|
||||
|
||||
final TaggedBadges taggedBadges = TaggedBadges.newBuilder()
|
||||
.setBadges(badges)
|
||||
.setEtag(etag(badges))
|
||||
.build();
|
||||
|
||||
// This could race and multiple threads could decide to build-and-cache. That's fine, they should all calculate
|
||||
// the same etag.
|
||||
localeToBadgesEtag.put(locale, taggedBadges.getEtag());
|
||||
return GetBadgesResponse.newBuilder().setTaggedBadges(taggedBadges).build();
|
||||
}
|
||||
|
||||
private static ByteString etag(final Message message) {
|
||||
final ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getSerializedSize());
|
||||
final CodedOutputStream cos = CodedOutputStream.newInstance(baos);
|
||||
|
||||
@@ -9,6 +9,7 @@ option java_multiple_files = true;
|
||||
|
||||
package org.signal.chat.remoteconfiguration;
|
||||
|
||||
import "org/signal/chat/common.proto";
|
||||
import "org/signal/chat/require.proto";
|
||||
import "org/signal/chat/tag.proto";
|
||||
|
||||
@@ -20,6 +21,16 @@ service RemoteConfiguration {
|
||||
// values are based on the authenticated account and caller's client platform,
|
||||
// which is derived from the "User-Agent" header.
|
||||
rpc GetConfiguration(GetConfigurationRequest) returns (GetConfigurationResponse) {}
|
||||
|
||||
// Returns detailed information for all configured badges, keyed by badge ID.
|
||||
//
|
||||
// Badge descriptions may contain localized strings. Callers should set their
|
||||
// language preferences via an "Accept-Language" header on the request
|
||||
// https://datatracker.ietf.org/doc/html/rfc3282#section-3
|
||||
//
|
||||
// Callers may cache a badges result for up to 1 day before checking (via
|
||||
// etag) if there are any new updates.
|
||||
rpc GetBadges(GetBadgesRequest) returns (GetBadgesResponse) {}
|
||||
}
|
||||
|
||||
message GetConfigurationRequest {
|
||||
@@ -54,3 +65,32 @@ message TaggedConfiguration {
|
||||
// changed.
|
||||
bytes etag = 2;
|
||||
}
|
||||
|
||||
message GetBadgesRequest {
|
||||
// If present, the etag from a prior GetBadgesResponse. If the provided etag
|
||||
// matches the current badge etag, the server may elide the badge response.
|
||||
bytes etag = 1;
|
||||
}
|
||||
|
||||
message GetBadgesResponse {
|
||||
oneof response {
|
||||
// The full set of badges and corresponding etag.
|
||||
TaggedBadges tagged_badges = 1;
|
||||
|
||||
// The etag in the request matched the current badge etag.
|
||||
bool etag_matched = 2 [(tag.reason) = "etag_match"];
|
||||
}
|
||||
}
|
||||
|
||||
message Badges {
|
||||
// A map of badge ID to the detailed information for that badge.
|
||||
map<string, common.Badge> badges = 1;
|
||||
}
|
||||
|
||||
message TaggedBadges {
|
||||
Badges badges = 1;
|
||||
|
||||
// An entity tag for `badges`. This may be supplied in a subsequent request
|
||||
// to optimize bandwidth when the set of badges has not changed.
|
||||
bytes etag = 2;
|
||||
}
|
||||
+93
-1
@@ -5,8 +5,13 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
@@ -14,7 +19,9 @@ import com.google.protobuf.ByteString;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -23,23 +30,39 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mock;
|
||||
import org.signal.chat.remoteconfiguration.GetBadgesRequest;
|
||||
import org.signal.chat.remoteconfiguration.GetBadgesResponse;
|
||||
import org.signal.chat.remoteconfiguration.GetConfigurationRequest;
|
||||
import org.signal.chat.remoteconfiguration.GetConfigurationResponse;
|
||||
import org.signal.chat.remoteconfiguration.RemoteConfigurationGrpc;
|
||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||
import org.whispersystems.textsecuregcm.entities.Badge;
|
||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||
|
||||
class RemoteConfigurationGrpcServiceTest extends
|
||||
SimpleBaseGrpcTest<RemoteConfigurationGrpcService, RemoteConfigurationGrpc.RemoteConfigurationBlockingStub> {
|
||||
|
||||
private static final List<String> BADGE_IDS = List.of("B1", "B2");
|
||||
|
||||
@Mock
|
||||
private RemoteConfigsManager remoteConfigsManager;
|
||||
|
||||
@Mock
|
||||
private BadgeTranslator badgeTranslator;
|
||||
|
||||
@Override
|
||||
protected RemoteConfigurationGrpcService createServiceBeforeEachTest() {
|
||||
getMockRequestAttributesInterceptor().setRequestAttributes(
|
||||
new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, null));
|
||||
|
||||
return new RemoteConfigurationGrpcService(remoteConfigsManager);
|
||||
when(badgeTranslator.resolveLocale(any())).thenReturn(Locale.US);
|
||||
when(badgeTranslator.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"))));
|
||||
when(badgeTranslator.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
|
||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"))));
|
||||
|
||||
return new RemoteConfigurationGrpcService(remoteConfigsManager, badgeTranslator, BADGE_IDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -149,4 +172,73 @@ class RemoteConfigurationGrpcServiceTest extends
|
||||
config1.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, _) -> a, () -> new TreeMap<String, String>(Comparator.reverseOrder()))))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBadges() {
|
||||
final GetBadgesResponse response = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().build());
|
||||
|
||||
assertTrue(response.hasTaggedBadges());
|
||||
final Map<String, org.signal.chat.common.Badge> badges =
|
||||
response.getTaggedBadges().getBadges().getBadgesMap();
|
||||
assertEquals(Set.of("B1", "B2"), badges.keySet());
|
||||
assertEquals("B1", badges.get("B1").getId());
|
||||
assertEquals("cat1", badges.get("B1").getCategory());
|
||||
assertEquals("B2", badges.get("B2").getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBadgesEtagMatched() {
|
||||
final GetBadgesResponse response = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().build());
|
||||
final ByteString etag = response.getTaggedBadges().getEtag();
|
||||
|
||||
final GetBadgesResponse cachedResponse = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().setEtag(etag).build());
|
||||
|
||||
assertTrue(cachedResponse.getEtagMatched());
|
||||
assertFalse(cachedResponse.hasTaggedBadges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBadgesWrongEtag() {
|
||||
final GetBadgesResponse response = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().setEtag(ByteString.copyFrom(new byte[32])).build());
|
||||
|
||||
assertFalse(response.hasEtagMatched());
|
||||
assertTrue(response.hasTaggedBadges());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBadgesComputesOncePerLocale() {
|
||||
final RequestAttributes frAttributes =
|
||||
new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, "fr-fr");
|
||||
final List<Locale> frLocales = List.of(Locale.FRANCE);
|
||||
final RequestAttributes caAttributes =
|
||||
new RequestAttributes(InetAddresses.forString("127.0.0.1"), null, "en-ca");
|
||||
final List<Locale> caLocales = List.of(Locale.CANADA);
|
||||
|
||||
when(badgeTranslator.resolveLocale(frLocales)).thenReturn(Locale.FRANCE);
|
||||
when(badgeTranslator.resolveLocale(caLocales)).thenReturn(Locale.CANADA);
|
||||
|
||||
// return slightly different values based on the language so the locales configurations have different etags
|
||||
when(badgeTranslator.translate(eq(frLocales), eq("B1")))
|
||||
.thenReturn(new Badge("B1", "cat1", "name1", "desc1", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"))));
|
||||
when(badgeTranslator.translate(eq(caLocales), eq("B1")))
|
||||
.thenReturn(new Badge("B1", "dog1", "name1", "desc1", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"))));
|
||||
|
||||
getMockRequestAttributesInterceptor().setRequestAttributes(caAttributes);
|
||||
final GetBadgesResponse caResponse = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().build());
|
||||
final GetBadgesResponse caResponseCached = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().setEtag(caResponse.getTaggedBadges().getEtag()).build());
|
||||
assertTrue(caResponseCached.getEtagMatched());
|
||||
verify(badgeTranslator, times(1)).translate(eq(caLocales), eq("B1"));
|
||||
|
||||
getMockRequestAttributesInterceptor().setRequestAttributes(frAttributes);
|
||||
final GetBadgesResponse frResponse = authenticatedServiceStub()
|
||||
.getBadges(GetBadgesRequest.newBuilder().setEtag(caResponse.getTaggedBadges().getEtag()).build());
|
||||
assertFalse(frResponse.hasEtagMatched());
|
||||
verify(badgeTranslator, times(1)).translate(eq(frLocales), eq("B1"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user