Retire `VoiceVerificationController`
This commit is contained in:
parent
38a0737afb
commit
cd4a4b1dcf
|
@ -229,11 +229,6 @@ unidentifiedDelivery:
|
||||||
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||||
expiresDays: 7
|
expiresDays: 7
|
||||||
|
|
||||||
voiceVerification:
|
|
||||||
url: https://cdn-ca.signal.org/verification/
|
|
||||||
locales:
|
|
||||||
- en
|
|
||||||
|
|
||||||
recaptcha:
|
recaptcha:
|
||||||
projectPath: projects/example
|
projectPath: projects/example
|
||||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||||
|
|
|
@ -49,7 +49,6 @@ import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
||||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||||
|
|
||||||
|
@ -191,11 +190,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
||||||
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
@JsonProperty
|
|
||||||
private VoiceVerificationConfiguration voiceVerification;
|
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -303,10 +297,6 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return hCaptcha;
|
return hCaptcha;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VoiceVerificationConfiguration getVoiceVerificationConfiguration() {
|
|
||||||
return voiceVerification;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||||
return webSocket;
|
return webSocket;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,6 @@ import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
|
|
||||||
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
||||||
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||||
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||||
|
@ -650,8 +649,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)));
|
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)));
|
||||||
environment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
environment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||||
environment.jersey().register(new TimestampResponseFilter());
|
environment.jersey().register(new TimestampResponseFilter());
|
||||||
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(),
|
|
||||||
config.getVoiceVerificationConfiguration().getLocales()));
|
|
||||||
|
|
||||||
///
|
///
|
||||||
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
|
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import javax.validation.Valid;
|
|
||||||
import javax.validation.constraints.NotEmpty;
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public class VoiceVerificationConfiguration {
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@Valid
|
|
||||||
@NotEmpty
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
@JsonProperty
|
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
private List<String> locales;
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<String> getLocales() {
|
|
||||||
return new HashSet<>(locales);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.controllers;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale.LanguageRange;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import javax.ws.rs.POST;
|
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.PathParam;
|
|
||||||
import javax.ws.rs.Produces;
|
|
||||||
import javax.ws.rs.QueryParam;
|
|
||||||
import javax.ws.rs.core.MediaType;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
|
||||||
@Path("/v1/voice/")
|
|
||||||
public class VoiceVerificationController {
|
|
||||||
|
|
||||||
private static final String PLAY_TWIML = """
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
<Play>%s</Play>
|
|
||||||
</Response>
|
|
||||||
""";
|
|
||||||
|
|
||||||
private static final String DEFAULT_LOCALE = "en-US";
|
|
||||||
|
|
||||||
|
|
||||||
private final String baseUrl;
|
|
||||||
private final Set<String> supportedLocales;
|
|
||||||
|
|
||||||
public VoiceVerificationController(String baseUrl, Set<String> supportedLocales) {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.supportedLocales = supportedLocales;
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("/description/{code}")
|
|
||||||
@Produces(MediaType.APPLICATION_XML)
|
|
||||||
public Response getDescription(@PathParam("code") String code, @QueryParam("l") List<String> locales) {
|
|
||||||
code = code.replaceAll("[^0-9]", "");
|
|
||||||
|
|
||||||
if (code.length() != 6) {
|
|
||||||
return Response.status(400).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locales == null) {
|
|
||||||
locales = Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<LanguageRange> priorityList;
|
|
||||||
try {
|
|
||||||
priorityList = locales.stream()
|
|
||||||
.map(LanguageRange::new)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
return Response.status(400).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String localeMatch = Util.findBestLocale(priorityList, supportedLocales).orElse(DEFAULT_LOCALE);
|
|
||||||
|
|
||||||
return getLocalizedDescription(code, localeMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Response getLocalizedDescription(String code, String locale) {
|
|
||||||
String path = constructUrlForLocale(baseUrl, locale);
|
|
||||||
|
|
||||||
return Response.ok()
|
|
||||||
.entity(String.format(PLAY_TWIML,
|
|
||||||
path + "verification.mp3",
|
|
||||||
path + code.charAt(0) + "_middle.mp3",
|
|
||||||
path + code.charAt(1) + "_middle.mp3",
|
|
||||||
path + code.charAt(2) + "_middle.mp3",
|
|
||||||
path + code.charAt(3) + "_middle.mp3",
|
|
||||||
path + code.charAt(4) + "_middle.mp3",
|
|
||||||
path + code.charAt(5) + "_falling.mp3",
|
|
||||||
path + "verification.mp3",
|
|
||||||
path + code.charAt(0) + "_middle.mp3",
|
|
||||||
path + code.charAt(1) + "_middle.mp3",
|
|
||||||
path + code.charAt(2) + "_middle.mp3",
|
|
||||||
path + code.charAt(3) + "_middle.mp3",
|
|
||||||
path + code.charAt(4) + "_middle.mp3",
|
|
||||||
path + code.charAt(5) + "_falling.mp3",
|
|
||||||
path + "verification.mp3",
|
|
||||||
path + code.charAt(0) + "_middle.mp3",
|
|
||||||
path + code.charAt(1) + "_middle.mp3",
|
|
||||||
path + code.charAt(2) + "_middle.mp3",
|
|
||||||
path + code.charAt(3) + "_middle.mp3",
|
|
||||||
path + code.charAt(4) + "_middle.mp3",
|
|
||||||
path + code.charAt(5) + "_falling.mp3"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String constructUrlForLocale(String baseUrl, String locale) {
|
|
||||||
if (!baseUrl.endsWith("/")) {
|
|
||||||
baseUrl += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseUrl + locale + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2021 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
|
|
||||||
import io.dropwizard.testing.FixtureHelpers;
|
|
||||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
|
||||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
|
||||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
|
||||||
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
|
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
|
||||||
class VoiceVerificationControllerTest {
|
|
||||||
|
|
||||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
|
||||||
.addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(
|
|
||||||
ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class)))
|
|
||||||
.addProvider(new RateLimitExceededExceptionMapper())
|
|
||||||
.setMapper(SystemMapper.getMapper())
|
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
|
||||||
.addResource(new VoiceVerificationController("https://foo.com/bar",
|
|
||||||
new HashSet<>(Arrays.asList("pt-BR", "ru"))))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlLocale() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.queryParam("l", "pt-BR")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_pt_br.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlSplitLocale() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.queryParam("l", "ru-RU")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_ru.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlUnsupportedLocale() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.queryParam("l", "es-MX")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_en_us.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlMultipleLocales() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.queryParam("l", "es-MX")
|
|
||||||
.queryParam("l", "ru-RU")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_ru.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlMissingLocale() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_en_us.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlMalformedCode() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/1234...56")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
|
||||||
assertThat(response.readEntity(String.class)).isXmlEqualTo(FixtureHelpers.fixture("fixtures/voice_verification_en_us.xml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlBadCodeLength() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/1234567")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(400);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testTwimlMalformedLocale() {
|
|
||||||
Response response =
|
|
||||||
resources.getJerseyTest()
|
|
||||||
.target("/v1/voice/description/123456")
|
|
||||||
.queryParam("l", "it IT ,")
|
|
||||||
.request()
|
|
||||||
.post(null);
|
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(400);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Play>https://foo.com/bar/en-US/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/en-US/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/en-US/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/en-US/6_falling.mp3</Play>
|
|
||||||
</Response>
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/pt-BR/6_falling.mp3</Play>
|
|
||||||
</Response>
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Response>
|
|
||||||
<Play>https://foo.com/bar/ru/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/ru/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/6_falling.mp3</Play>
|
|
||||||
<Pause length="1"/>
|
|
||||||
<Play>https://foo.com/bar/ru/verification.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/1_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/2_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/3_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/4_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/5_middle.mp3</Play>
|
|
||||||
<Play>https://foo.com/bar/ru/6_falling.mp3</Play>
|
|
||||||
</Response>
|
|
Loading…
Reference in New Issue