Add configuration for regional SMS verification text
This commit is contained in:
parent
1999bd2bcb
commit
760462f8fb
|
@ -5,11 +5,16 @@ twilio: # Twilio gateway configuration
|
||||||
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
messagingServiceSid: # Twilio SID for the message service to use for non-NANPA.
|
||||||
verifyServiceSid: # Twilio SID for a Verify service
|
verifyServiceSid: # Twilio SID for a Verify service
|
||||||
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service.
|
||||||
iosVerificationText: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
defaultClientVerificationTexts:
|
||||||
androidNgVerificationText: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1.
|
||||||
android202001VerificationText: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1.
|
||||||
android202103VerificationText: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1.
|
||||||
genericVerificationText: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1.
|
||||||
|
generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1.
|
||||||
|
regionalClientVerificationTexts: # Map of country codes to custom texts
|
||||||
|
999: # example country code
|
||||||
|
ios:
|
||||||
|
# … all keys from defaultClientVerificationTexts are required
|
||||||
androidAppHash: # Hash appended to Android
|
androidAppHash: # Hash appended to Android
|
||||||
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
|
verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
@ -37,20 +39,11 @@ public class TwilioConfiguration {
|
||||||
@Valid
|
@Valid
|
||||||
private RetryConfiguration retry = new RetryConfiguration();
|
private RetryConfiguration retry = new RetryConfiguration();
|
||||||
|
|
||||||
@NotEmpty
|
@Valid
|
||||||
private String iosVerificationText;
|
private TwilioVerificationTextConfiguration defaultClientVerificationTexts;
|
||||||
|
|
||||||
@NotEmpty
|
@Valid
|
||||||
private String androidNgVerificationText;
|
private Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts = Collections.emptyMap();
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
private String android202001VerificationText;
|
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
private String android202103VerificationText;
|
|
||||||
|
|
||||||
@NotEmpty
|
|
||||||
private String genericVerificationText;
|
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
private String androidAppHash;
|
private String androidAppHash;
|
||||||
|
@ -129,49 +122,23 @@ public class TwilioConfiguration {
|
||||||
this.retry = retry;
|
this.retry = retry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIosVerificationText() {
|
public TwilioVerificationTextConfiguration getDefaultClientVerificationTexts() {
|
||||||
return iosVerificationText;
|
return defaultClientVerificationTexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void setIosVerificationText(String iosVerificationText) {
|
public void setDefaultClientVerificationTexts(TwilioVerificationTextConfiguration defaultClientVerificationTexts) {
|
||||||
this.iosVerificationText = iosVerificationText;
|
this.defaultClientVerificationTexts = defaultClientVerificationTexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAndroidNgVerificationText() {
|
|
||||||
return androidNgVerificationText;
|
public Map<String,TwilioVerificationTextConfiguration> getRegionalClientVerificationTexts() {
|
||||||
|
return regionalClientVerificationTexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void setAndroidNgVerificationText(String androidNgVerificationText) {
|
public void setRegionalClientVerificationTexts(final Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts) {
|
||||||
this.androidNgVerificationText = androidNgVerificationText;
|
this.regionalClientVerificationTexts = regionalClientVerificationTexts;
|
||||||
}
|
|
||||||
|
|
||||||
public String getAndroid202001VerificationText() {
|
|
||||||
return android202001VerificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public void setAndroid202001VerificationText(String android202001VerificationText) {
|
|
||||||
this.android202001VerificationText = android202001VerificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAndroid202103VerificationText() {
|
|
||||||
return android202103VerificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public void setAndroid202103VerificationText(String android202103VerificationText) {
|
|
||||||
this.android202103VerificationText = android202103VerificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGenericVerificationText() {
|
|
||||||
return genericVerificationText;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public void setGenericVerificationText(String genericVerificationText) {
|
|
||||||
this.genericVerificationText = genericVerificationText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAndroidAppHash() {
|
public String getAndroidAppHash() {
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public class TwilioVerificationTextConfiguration {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String ios;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String androidNg;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String android202001;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String android202103;
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@NotEmpty
|
||||||
|
private String generic;
|
||||||
|
|
||||||
|
public String getIosText() {
|
||||||
|
return ios;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIosText(String ios) {
|
||||||
|
this.ios = ios;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAndroidNgText() {
|
||||||
|
return androidNg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAndroidNgText(final String androidNg) {
|
||||||
|
this.androidNg = androidNg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAndroid202001Text() {
|
||||||
|
return android202001;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAndroid202001Text(final String android202001) {
|
||||||
|
this.android202001 = android202001;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAndroid202103Text() {
|
||||||
|
return android202103;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAndroid202103Text(final String android202103) {
|
||||||
|
this.android202103 = android202103;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGenericText() {
|
||||||
|
return generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGenericText(final String generic) {
|
||||||
|
this.generic = generic;
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||||
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
|
import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher;
|
||||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
@ -64,11 +65,9 @@ public class TwilioSmsSender {
|
||||||
private final String nanpaMessagingServiceSid;
|
private final String nanpaMessagingServiceSid;
|
||||||
private final String localDomain;
|
private final String localDomain;
|
||||||
private final Random random;
|
private final Random random;
|
||||||
private final String androidNgVerificationText;
|
|
||||||
private final String android202001VerificationText;
|
private final TwilioVerificationTextConfiguration defaultClientVerificationTexts;
|
||||||
private final String android202103VerificationText;
|
private final Map<String,TwilioVerificationTextConfiguration> regionalClientVerificationTexts;
|
||||||
private final String iosVerificationText;
|
|
||||||
private final String genericVerificationText;
|
|
||||||
|
|
||||||
private final FaultTolerantHttpClient httpClient;
|
private final FaultTolerantHttpClient httpClient;
|
||||||
private final URI smsUri;
|
private final URI smsUri;
|
||||||
|
@ -88,11 +87,6 @@ public class TwilioSmsSender {
|
||||||
this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid();
|
this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid();
|
||||||
this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid();
|
this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid();
|
||||||
this.random = new Random(System.currentTimeMillis());
|
this.random = new Random(System.currentTimeMillis());
|
||||||
this.androidNgVerificationText = twilioConfiguration.getAndroidNgVerificationText();
|
|
||||||
this.android202001VerificationText = twilioConfiguration.getAndroid202001VerificationText();
|
|
||||||
this.android202103VerificationText = twilioConfiguration.getAndroid202103VerificationText();
|
|
||||||
this.iosVerificationText = twilioConfiguration.getIosVerificationText();
|
|
||||||
this.genericVerificationText = twilioConfiguration.getGenericVerificationText();
|
|
||||||
this.smsUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Messages.json");
|
this.smsUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Messages.json");
|
||||||
this.voxUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Calls.json" );
|
this.voxUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Calls.json" );
|
||||||
this.httpClient = FaultTolerantHttpClient.newBuilder()
|
this.httpClient = FaultTolerantHttpClient.newBuilder()
|
||||||
|
@ -105,6 +99,9 @@ public class TwilioSmsSender {
|
||||||
.withName("twilio")
|
.withName("twilio")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
this.defaultClientVerificationTexts = twilioConfiguration.getDefaultClientVerificationTexts();
|
||||||
|
this.regionalClientVerificationTexts = twilioConfiguration.getRegionalClientVerificationTexts();
|
||||||
|
|
||||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
this.twilioVerifySender = new TwilioVerifySender(baseVerifyUri, httpClient, twilioConfiguration);
|
this.twilioVerifySender = new TwilioVerifySender(baseVerifyUri, httpClient, twilioConfiguration);
|
||||||
}
|
}
|
||||||
|
@ -134,19 +131,25 @@ public class TwilioSmsSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getBodyFormatString(@Nonnull String destination, @Nullable String clientType) {
|
private String getBodyFormatString(@Nonnull String destination, @Nullable String clientType) {
|
||||||
|
|
||||||
|
final String countryCode = Util.getCountryCode(destination);
|
||||||
|
|
||||||
|
final TwilioVerificationTextConfiguration verificationTexts = regionalClientVerificationTexts
|
||||||
|
.getOrDefault(countryCode, defaultClientVerificationTexts);
|
||||||
|
|
||||||
final String result;
|
final String result;
|
||||||
if ("ios".equals(clientType)) {
|
if ("ios".equals(clientType)) {
|
||||||
result = iosVerificationText;
|
result = verificationTexts.getIosText();
|
||||||
} else if ("android-ng".equals(clientType)) {
|
} else if ("android-ng".equals(clientType)) {
|
||||||
result = androidNgVerificationText;
|
result = verificationTexts.getAndroidNgText();
|
||||||
} else if ("android-2020-01".equals(clientType)) {
|
} else if ("android-2020-01".equals(clientType)) {
|
||||||
result = android202001VerificationText;
|
result = verificationTexts.getAndroid202001Text();
|
||||||
} else if ("android-2021-03".equals(clientType)) {
|
} else if ("android-2021-03".equals(clientType)) {
|
||||||
result = android202103VerificationText;
|
result = verificationTexts.getAndroid202103Text();
|
||||||
} else {
|
} else {
|
||||||
result = genericVerificationText;
|
result = verificationTexts.getGenericText();
|
||||||
}
|
}
|
||||||
if (destination.startsWith("+86")) { // is China
|
if ("86".equals(countryCode)) { // is China
|
||||||
return result + "\u2008";
|
return result + "\u2008";
|
||||||
// Twilio recommends adding this character to the end of strings delivered to China because some carriers in
|
// Twilio recommends adding this character to the end of strings delivered to China because some carriers in
|
||||||
// China are blocking GSM-7 encoding and this will force Twilio to send using UCS-2 instead.
|
// China are blocking GSM-7 encoding and this will force Twilio to send using UCS-2 instead.
|
||||||
|
|
|
@ -5,20 +5,29 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.tests.sms;
|
package org.whispersystems.textsecuregcm.tests.sms;
|
||||||
|
|
||||||
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.post;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
|
||||||
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
||||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.junit.WireMockRule;
|
import com.github.tomakehurst.wiremock.junit.WireMockRule;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale.LanguageRange;
|
import java.util.Locale.LanguageRange;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTwilioConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTwilioConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
|
||||||
|
@ -62,15 +71,29 @@ public class TwilioSmsSenderTest {
|
||||||
configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID);
|
configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID);
|
||||||
configuration.setVerifyServiceSid(VERIFY_SERVICE_SID);
|
configuration.setVerifyServiceSid(VERIFY_SERVICE_SID);
|
||||||
configuration.setLocalDomain(LOCAL_DOMAIN);
|
configuration.setLocalDomain(LOCAL_DOMAIN);
|
||||||
configuration.setIosVerificationText("Verify on iOS: %1$s\n\nsomelink://verify/%1$s");
|
|
||||||
configuration.setAndroidNgVerificationText("<#> Verify on AndroidNg: %1$s\n\ncharacters");
|
configuration.setDefaultClientVerificationTexts(createTwlilioVerificationText(""));
|
||||||
configuration.setAndroid202001VerificationText("Verify on Android202001: %1$s\n\nsomelink://verify/%1$s\n\ncharacters");
|
|
||||||
configuration.setAndroid202103VerificationText("Verify on Android202103: %1$s\n\ncharacters");
|
configuration.setRegionalClientVerificationTexts(
|
||||||
configuration.setGenericVerificationText("Verify on whatever: %1$s");
|
Map.of("33", createTwlilioVerificationText("[33] "))
|
||||||
|
);
|
||||||
configuration.setAndroidAppHash("someHash");
|
configuration.setAndroidAppHash("someHash");
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TwilioVerificationTextConfiguration createTwlilioVerificationText(final String prefix) {
|
||||||
|
|
||||||
|
TwilioVerificationTextConfiguration verificationTextConfiguration = new TwilioVerificationTextConfiguration();
|
||||||
|
|
||||||
|
verificationTextConfiguration.setIosText(prefix + "Verify on iOS: %1$s\n\nsomelink://verify/%1$s");
|
||||||
|
verificationTextConfiguration.setAndroidNgText(prefix + "<#> Verify on AndroidNg: %1$s\n\ncharacters");
|
||||||
|
verificationTextConfiguration.setAndroid202001Text(prefix + "Verify on Android202001: %1$s\n\nsomelink://verify/%1$s\n\ncharacters");
|
||||||
|
verificationTextConfiguration.setAndroid202103Text(prefix + "Verify on Android202103: %1$s\n\ncharacters");
|
||||||
|
verificationTextConfiguration.setGenericText(prefix + "Verify on whatever: %1$s");
|
||||||
|
|
||||||
|
return verificationTextConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
private void setupSuccessStubForSms() {
|
private void setupSuccessStubForSms() {
|
||||||
wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
|
wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
|
||||||
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
.withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN)
|
||||||
|
@ -250,4 +273,18 @@ public class TwilioSmsSenderTest {
|
||||||
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||||
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B861065529988&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters%E2%80%88")));
|
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B861065529988&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters%E2%80%88")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSendSmsRegionalVerificationText() {
|
||||||
|
setupSuccessStubForSms();
|
||||||
|
|
||||||
|
boolean success = sender.deliverSmsVerification("+33655512673", Optional.of("android-ng"), "123-456").join();
|
||||||
|
|
||||||
|
assertThat(success).isTrue();
|
||||||
|
|
||||||
|
verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json"))
|
||||||
|
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
|
||||||
|
.withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B33655512673&Body=%5B33%5D+%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters")));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue