Introduce common push notification interfaces/pathways
This commit is contained in:
parent
0d24828539
commit
6f0faae4ce
|
@ -147,6 +147,7 @@ import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
import org.whispersystems.textsecuregcm.push.FcmSender;
|
||||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||||
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
||||||
|
@ -446,8 +447,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.empty());
|
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.empty());
|
||||||
PubSubManager pubSubManager = new PubSubManager(pubsubClient, dispatchManager);
|
PubSubManager pubSubManager = new PubSubManager(pubsubClient, dispatchManager);
|
||||||
APNSender apnSender = new APNSender(apnSenderExecutor, accountsManager, config.getApnConfiguration());
|
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
|
||||||
FcmSender fcmSender = new FcmSender(gcmSenderExecutor, accountsManager, config.getFcmConfiguration().credentials());
|
FcmSender fcmSender = new FcmSender(gcmSenderExecutor, config.getFcmConfiguration().credentials());
|
||||||
|
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
||||||
|
PushNotificationManager pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnFallbackManager);
|
||||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
|
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
|
||||||
DynamicRateLimiters dynamicRateLimiters = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
|
DynamicRateLimiters dynamicRateLimiters = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
|
||||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||||
|
@ -470,17 +473,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||||
|
|
||||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
|
||||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
||||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||||
MessageSender messageSender = new MessageSender(apnFallbackManager, clientPresenceManager, messagesManager, fcmSender, apnSender, pushLatencyManager);
|
MessageSender messageSender = new MessageSender(clientPresenceManager, messagesManager, pushNotificationManager, pushLatencyManager);
|
||||||
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
|
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
|
||||||
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
||||||
config.getRecaptchaConfiguration().getProjectPath(),
|
config.getRecaptchaConfiguration().getProjectPath(),
|
||||||
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),
|
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),
|
||||||
dynamicConfigurationManager);
|
dynamicConfigurationManager);
|
||||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, fcmSender, pushChallengeDynamoDb);
|
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager, pushChallengeDynamoDb);
|
||||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||||
recaptchaClient, dynamicRateLimiters);
|
recaptchaClient, dynamicRateLimiters);
|
||||||
RateLimitChallengeOptionManager rateLimitChallengeOptionManager =
|
RateLimitChallengeOptionManager rateLimitChallengeOptionManager =
|
||||||
|
@ -548,10 +550,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
FtxClient ftxClient = new FtxClient(currencyClient);
|
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||||
|
|
||||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
environment.lifecycle().manage(apnSender);
|
||||||
environment.lifecycle().manage(apnFallbackManager);
|
environment.lifecycle().manage(apnFallbackManager);
|
||||||
environment.lifecycle().manage(pubSubManager);
|
environment.lifecycle().manage(pubSubManager);
|
||||||
environment.lifecycle().manage(messageSender);
|
|
||||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||||
environment.lifecycle().manage(directoryReconciliationAccountDatabaseCrawler);
|
environment.lifecycle().manage(directoryReconciliationAccountDatabaseCrawler);
|
||||||
environment.lifecycle().manage(accountCleanerAccountDatabaseCrawler);
|
environment.lifecycle().manage(accountCleanerAccountDatabaseCrawler);
|
||||||
|
@ -607,7 +608,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getWebSocketConfiguration(), 90000);
|
config.getWebSocketConfiguration(), 90000);
|
||||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||||
webSocketEnvironment.setConnectListener(
|
webSocketEnvironment.setConnectListener(
|
||||||
new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager,
|
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager, apnFallbackManager,
|
||||||
clientPresenceManager, websocketScheduledExecutor));
|
clientPresenceManager, websocketScheduledExecutor));
|
||||||
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||||
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
||||||
|
@ -619,7 +620,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
environment.jersey().register(
|
environment.jersey().register(
|
||||||
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
||||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||||
recaptchaClient, fcmSender, apnSender, verifyExperimentEnrollmentManager,
|
recaptchaClient, pushNotificationManager, verifyExperimentEnrollmentManager,
|
||||||
changeNumberManager, backupCredentialsGenerator));
|
changeNumberManager, backupCredentialsGenerator));
|
||||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import javax.validation.constraints.NotNull;
|
||||||
import javax.ws.rs.BadRequestException;
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
import javax.ws.rs.ForbiddenException;
|
import javax.ws.rs.ForbiddenException;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.HEAD;
|
import javax.ws.rs.HEAD;
|
||||||
|
@ -74,10 +75,8 @@ import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
|
||||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||||
|
@ -137,8 +136,7 @@ public class AccountController {
|
||||||
private final TurnTokenGenerator turnTokenGenerator;
|
private final TurnTokenGenerator turnTokenGenerator;
|
||||||
private final Map<String, Integer> testDevices;
|
private final Map<String, Integer> testDevices;
|
||||||
private final RecaptchaClient recaptchaClient;
|
private final RecaptchaClient recaptchaClient;
|
||||||
private final FcmSender fcmSender;
|
private final PushNotificationManager pushNotificationManager;
|
||||||
private final APNSender apnSender;
|
|
||||||
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
||||||
|
|
||||||
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
|
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
|
||||||
|
@ -153,8 +151,7 @@ public class AccountController {
|
||||||
TurnTokenGenerator turnTokenGenerator,
|
TurnTokenGenerator turnTokenGenerator,
|
||||||
Map<String, Integer> testDevices,
|
Map<String, Integer> testDevices,
|
||||||
RecaptchaClient recaptchaClient,
|
RecaptchaClient recaptchaClient,
|
||||||
FcmSender fcmSender,
|
PushNotificationManager pushNotificationManager,
|
||||||
APNSender apnSender,
|
|
||||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
|
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
|
||||||
ChangeNumberManager changeNumberManager,
|
ChangeNumberManager changeNumberManager,
|
||||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
|
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
|
||||||
|
@ -168,8 +165,7 @@ public class AccountController {
|
||||||
this.testDevices = testDevices;
|
this.testDevices = testDevices;
|
||||||
this.turnTokenGenerator = turnTokenGenerator;
|
this.turnTokenGenerator = turnTokenGenerator;
|
||||||
this.recaptchaClient = recaptchaClient;
|
this.recaptchaClient = recaptchaClient;
|
||||||
this.fcmSender = fcmSender;
|
this.pushNotificationManager = pushNotificationManager;
|
||||||
this.apnSender = apnSender;
|
|
||||||
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
|
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
|
||||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||||
this.changeNumberManager = changeNumberManager;
|
this.changeNumberManager = changeNumberManager;
|
||||||
|
@ -179,15 +175,17 @@ public class AccountController {
|
||||||
@GET
|
@GET
|
||||||
@Path("/{type}/preauth/{token}/{number}")
|
@Path("/{type}/preauth/{token}/{number}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response getPreAuth(@PathParam("type") String pushType,
|
public Response getPreAuth(@PathParam("type") String pushType,
|
||||||
@PathParam("token") String pushToken,
|
@PathParam("token") String pushToken,
|
||||||
@PathParam("number") String number,
|
@PathParam("number") String number,
|
||||||
@QueryParam("voip") Optional<Boolean> useVoip)
|
@QueryParam("voip") @DefaultValue("true") boolean useVoip)
|
||||||
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||||
|
|
||||||
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
|
final PushNotification.TokenType tokenType = switch(pushType) {
|
||||||
return Response.status(400).build();
|
case "apn" -> useVoip ? PushNotification.TokenType.APN_VOIP : PushNotification.TokenType.APN;
|
||||||
}
|
case "fcm" -> PushNotification.TokenType.FCM;
|
||||||
|
default -> throw new BadRequestException();
|
||||||
|
};
|
||||||
|
|
||||||
Util.requireNormalizedNumber(number);
|
Util.requireNormalizedNumber(number);
|
||||||
|
|
||||||
|
@ -198,14 +196,7 @@ public class AccountController {
|
||||||
null);
|
null);
|
||||||
|
|
||||||
pendingAccounts.store(number, storedVerificationCode);
|
pendingAccounts.store(number, storedVerificationCode);
|
||||||
|
pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.getPushCode());
|
||||||
if ("fcm".equals(pushType)) {
|
|
||||||
fcmSender.sendMessage(new GcmMessage(pushToken, null, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
|
||||||
} else if ("apn".equals(pushType)) {
|
|
||||||
apnSender.sendMessage(new ApnMessage(pushToken, null, 0, useVoip.orElse(true), ApnMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode())));
|
|
||||||
} else {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.ok().build();
|
return Response.ok().build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,11 @@ import static com.codahale.metrics.MetricRegistry.name;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Optional;
|
|
||||||
import org.apache.commons.codec.DecoderException;
|
import org.apache.commons.codec.DecoderException;
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
|
||||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
|
||||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
|
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
|
||||||
|
@ -27,9 +22,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||||
|
|
||||||
public class PushChallengeManager {
|
public class PushChallengeManager {
|
||||||
private final APNSender apnSender;
|
private final PushNotificationManager pushNotificationManager;
|
||||||
private final FcmSender fcmSender;
|
|
||||||
|
|
||||||
private final PushChallengeDynamoDb pushChallengeDynamoDb;
|
private final PushChallengeDynamoDb pushChallengeDynamoDb;
|
||||||
|
|
||||||
private final SecureRandom random = new SecureRandom();
|
private final SecureRandom random = new SecureRandom();
|
||||||
|
@ -45,21 +38,16 @@ public class PushChallengeManager {
|
||||||
private static final String SUCCESS_TAG_NAME = "success";
|
private static final String SUCCESS_TAG_NAME = "success";
|
||||||
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||||
|
|
||||||
public PushChallengeManager(final APNSender apnSender, final FcmSender fcmSender,
|
public PushChallengeManager(final PushNotificationManager pushNotificationManager,
|
||||||
final PushChallengeDynamoDb pushChallengeDynamoDb) {
|
final PushChallengeDynamoDb pushChallengeDynamoDb) {
|
||||||
|
|
||||||
this.apnSender = apnSender;
|
this.pushNotificationManager = pushNotificationManager;
|
||||||
this.fcmSender = fcmSender;
|
|
||||||
this.pushChallengeDynamoDb = pushChallengeDynamoDb;
|
this.pushChallengeDynamoDb = pushChallengeDynamoDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendChallenge(final Account account) throws NotPushRegisteredException {
|
public void sendChallenge(final Account account) throws NotPushRegisteredException {
|
||||||
final Device masterDevice = account.getMasterDevice().orElseThrow(NotPushRegisteredException::new);
|
final Device masterDevice = account.getMasterDevice().orElseThrow(NotPushRegisteredException::new);
|
||||||
|
|
||||||
if (StringUtils.isAllBlank(masterDevice.getGcmId(), masterDevice.getApnId())) {
|
|
||||||
throw new NotPushRegisteredException();
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] token = new byte[CHALLENGE_TOKEN_LENGTH];
|
final byte[] token = new byte[CHALLENGE_TOKEN_LENGTH];
|
||||||
random.nextBytes(token);
|
random.nextBytes(token);
|
||||||
|
|
||||||
|
@ -67,17 +55,18 @@ public class PushChallengeManager {
|
||||||
final String platform;
|
final String platform;
|
||||||
|
|
||||||
if (pushChallengeDynamoDb.add(account.getUuid(), token, CHALLENGE_TTL)) {
|
if (pushChallengeDynamoDb.add(account.getUuid(), token, CHALLENGE_TTL)) {
|
||||||
final String tokenHex = Hex.encodeHexString(token);
|
pushNotificationManager.sendRateLimitChallengeNotification(account, Hex.encodeHexString(token));
|
||||||
|
|
||||||
sent = true;
|
sent = true;
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(masterDevice.getGcmId())) {
|
if (StringUtils.isNotBlank(masterDevice.getGcmId())) {
|
||||||
fcmSender.sendMessage(new GcmMessage(masterDevice.getGcmId(), account.getUuid(), 0, GcmMessage.Type.RATE_LIMIT_CHALLENGE, Optional.of(tokenHex)));
|
|
||||||
platform = ClientPlatform.ANDROID.name().toLowerCase();
|
platform = ClientPlatform.ANDROID.name().toLowerCase();
|
||||||
} else if (StringUtils.isNotBlank(masterDevice.getApnId())) {
|
} else if (StringUtils.isNotBlank(masterDevice.getApnId())) {
|
||||||
apnSender.sendMessage(new ApnMessage(masterDevice.getApnId(), account.getUuid(), 0, false, Type.RATE_LIMIT_CHALLENGE, Optional.of(tokenHex)));
|
|
||||||
platform = ClientPlatform.IOS.name().toLowerCase();
|
platform = ClientPlatform.IOS.name().toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError();
|
// This should never happen; if the account has neither an APN nor FCM token, sending the challenge will result
|
||||||
|
// in a `NotPushRegisteredException`
|
||||||
|
platform = "unrecognized";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sent = false;
|
sent = false;
|
||||||
|
|
|
@ -4,57 +4,47 @@
|
||||||
*/
|
*/
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
import com.codahale.metrics.Meter;
|
|
||||||
import com.codahale.metrics.MetricRegistry;
|
|
||||||
import com.codahale.metrics.SharedMetricRegistries;
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import org.slf4j.Logger;
|
import io.dropwizard.lifecycle.Managed;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
|
||||||
import org.whispersystems.textsecuregcm.push.RetryingApnsClient.ApnResult;
|
|
||||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.push.RetryingApnsClient.ApnResult;
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
public class APNSender implements Managed, PushNotificationSender {
|
||||||
import io.dropwizard.lifecycle.Managed;
|
|
||||||
|
|
||||||
public class APNSender implements Managed {
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(APNSender.class);
|
|
||||||
|
|
||||||
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
|
|
||||||
private static final Meter unregisteredEventStale = metricRegistry.meter(name(APNSender.class, "unregistered_event_stale"));
|
|
||||||
private static final Meter unregisteredEventFresh = metricRegistry.meter(name(APNSender.class, "unregistered_event_fresh"));
|
|
||||||
|
|
||||||
private ApnFallbackManager fallbackManager;
|
|
||||||
|
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final AccountsManager accountsManager;
|
|
||||||
private final String bundleId;
|
private final String bundleId;
|
||||||
private final boolean sandbox;
|
private final boolean sandbox;
|
||||||
private final RetryingApnsClient apnsClient;
|
private final RetryingApnsClient apnsClient;
|
||||||
|
|
||||||
public APNSender(ExecutorService executor, AccountsManager accountsManager, ApnConfiguration configuration)
|
@VisibleForTesting
|
||||||
|
static final String APN_VOIP_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String APN_NSE_NOTIFICATION_PAYLOAD = "{\"aps\":{\"mutable-content\":1,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String APN_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"challenge\" : \"%s\"}";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String APN_RATE_LIMIT_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"rateLimitChallenge\" : \"%s\"}";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L);
|
||||||
|
|
||||||
|
public APNSender(ExecutorService executor, ApnConfiguration configuration)
|
||||||
throws IOException, NoSuchAlgorithmException, InvalidKeyException
|
throws IOException, NoSuchAlgorithmException, InvalidKeyException
|
||||||
{
|
{
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.bundleId = configuration.getBundleId();
|
this.bundleId = configuration.getBundleId();
|
||||||
this.sandbox = configuration.isSandboxEnabled();
|
this.sandbox = configuration.isSandboxEnabled();
|
||||||
this.apnsClient = new RetryingApnsClient(configuration.getSigningKey(),
|
this.apnsClient = new RetryingApnsClient(configuration.getSigningKey(),
|
||||||
|
@ -64,50 +54,64 @@ public class APNSender implements Managed {
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public APNSender(ExecutorService executor, AccountsManager accountsManager, RetryingApnsClient apnsClient, String bundleId, boolean sandbox) {
|
public APNSender(ExecutorService executor, RetryingApnsClient apnsClient, String bundleId, boolean sandbox) {
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.apnsClient = apnsClient;
|
this.apnsClient = apnsClient;
|
||||||
this.sandbox = sandbox;
|
this.sandbox = sandbox;
|
||||||
this.bundleId = bundleId;
|
this.bundleId = bundleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<ApnResult> sendMessage(final ApnMessage message) {
|
@Override
|
||||||
String topic = bundleId;
|
public CompletableFuture<SendPushNotificationResult> sendNotification(final PushNotification notification) {
|
||||||
|
final String topic = switch (notification.tokenType()) {
|
||||||
|
case APN -> bundleId;
|
||||||
|
case APN_VOIP -> bundleId + ".voip";
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported token type: " + notification.tokenType());
|
||||||
|
};
|
||||||
|
|
||||||
if (message.isVoip()) {
|
final boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP;
|
||||||
topic = topic + ".voip";
|
|
||||||
}
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> future = apnsClient.send(message.getApnId(), topic,
|
|
||||||
message.getMessage(),
|
|
||||||
Instant.ofEpochMilli(message.getExpirationTime()),
|
|
||||||
message.isVoip(),
|
|
||||||
message.getCollapseId());
|
|
||||||
|
|
||||||
Futures.addCallback(future, new FutureCallback<>() {
|
final String payload = switch (notification.notificationType()) {
|
||||||
|
case NOTIFICATION -> isVoip ? APN_VOIP_NOTIFICATION_PAYLOAD : APN_NSE_NOTIFICATION_PAYLOAD;
|
||||||
|
case CHALLENGE -> String.format(APN_CHALLENGE_PAYLOAD, notification.data());
|
||||||
|
case RATE_LIMIT_CHALLENGE -> String.format(APN_RATE_LIMIT_CHALLENGE_PAYLOAD, notification.data());
|
||||||
|
};
|
||||||
|
|
||||||
|
final String collapseId =
|
||||||
|
(notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && !isVoip)
|
||||||
|
? "incoming-message" : null;
|
||||||
|
|
||||||
|
final ListenableFuture<ApnResult> sendFuture = apnsClient.send(notification.deviceToken(),
|
||||||
|
topic,
|
||||||
|
payload,
|
||||||
|
MAX_EXPIRATION,
|
||||||
|
isVoip,
|
||||||
|
collapseId);
|
||||||
|
|
||||||
|
final CompletableFuture<SendPushNotificationResult> completableSendFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Futures.addCallback(sendFuture, new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(@Nullable ApnResult result) {
|
public void onSuccess(@Nullable ApnResult result) {
|
||||||
if (message.getChallengeData().isPresent()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
logger.warn("*** RECEIVED NULL APN RESULT ***");
|
// This should never happen
|
||||||
} else if (result.getStatus() == ApnResult.Status.NO_SUCH_USER) {
|
completableSendFuture.completeExceptionally(new NullPointerException("apnResult was null"));
|
||||||
message.getUuid().ifPresent(uuid -> handleUnregisteredUser(message.getApnId(), uuid, message.getDeviceId()));
|
} else {
|
||||||
} else if (result.getStatus() == ApnResult.Status.GENERIC_FAILURE) {
|
completableSendFuture.complete(switch (result.getStatus()) {
|
||||||
logger.warn("*** Got APN generic failure: " + result.getReason() + ", " + message.getUuid());
|
case SUCCESS -> new SendPushNotificationResult(true, null, false);
|
||||||
|
case NO_SUCH_USER -> new SendPushNotificationResult(false, result.getReason(), true);
|
||||||
|
case GENERIC_FAILURE -> new SendPushNotificationResult(false, result.getReason(), false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@Nullable Throwable t) {
|
public void onFailure(@Nullable Throwable t) {
|
||||||
logger.warn("Got fatal APNS exception", t);
|
completableSendFuture.completeExceptionally(t);
|
||||||
}
|
}
|
||||||
}, executor);
|
}, executor);
|
||||||
|
|
||||||
return future;
|
return completableSendFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -118,66 +122,4 @@ public class APNSender implements Managed {
|
||||||
public void stop() {
|
public void stop() {
|
||||||
this.apnsClient.disconnect();
|
this.apnsClient.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setApnFallbackManager(ApnFallbackManager fallbackManager) {
|
|
||||||
this.fallbackManager = fallbackManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUnregisteredUser(String registrationId, UUID uuid, long deviceId) {
|
|
||||||
// logger.info("Got APN Unregistered: " + number + "," + deviceId);
|
|
||||||
|
|
||||||
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
|
|
||||||
|
|
||||||
if (account.isEmpty()) {
|
|
||||||
logger.info("No account found: {}", uuid);
|
|
||||||
unregisteredEventStale.mark();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<Device> device = account.get().getDevice(deviceId);
|
|
||||||
|
|
||||||
if (device.isEmpty()) {
|
|
||||||
logger.info("No device found: {}", uuid);
|
|
||||||
unregisteredEventStale.mark();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!registrationId.equals(device.get().getApnId()) &&
|
|
||||||
!registrationId.equals(device.get().getVoipApnId()))
|
|
||||||
{
|
|
||||||
logger.info("Registration ID does not match: " + registrationId + ", " + device.get().getApnId() + ", " + device.get().getVoipApnId());
|
|
||||||
unregisteredEventStale.mark();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (registrationId.equals(device.get().getApnId())) {
|
|
||||||
// logger.info("APN Unregister APN ID matches! " + number + ", " + deviceId);
|
|
||||||
// } else if (registrationId.equals(device.get().getVoipApnId())) {
|
|
||||||
// logger.info("APN Unregister VoIP ID matches! " + number + ", " + deviceId);
|
|
||||||
// }
|
|
||||||
|
|
||||||
long tokenTimestamp = device.get().getPushTimestamp();
|
|
||||||
|
|
||||||
if (tokenTimestamp != 0 && System.currentTimeMillis() < tokenTimestamp + TimeUnit.SECONDS.toMillis(10))
|
|
||||||
{
|
|
||||||
logger.info("APN Unregister push timestamp is more recent: {}, {}", tokenTimestamp, uuid);
|
|
||||||
unregisteredEventStale.mark();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.info("APN Unregister timestamp matches: " + device.get().getApnId() + ", " + device.get().getVoipApnId());
|
|
||||||
// device.get().setApnId(null);
|
|
||||||
// device.get().setVoipApnId(null);
|
|
||||||
// device.get().setFetchesMessages(false);
|
|
||||||
// accountsManager.update(account.get());
|
|
||||||
|
|
||||||
// if (fallbackManager != null) {
|
|
||||||
// fallbackManager.cancel(new WebsocketAddress(number, deviceId));
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (fallbackManager != null) {
|
|
||||||
RedisOperation.unchecked(() -> fallbackManager.cancel(account.get(), device.get()));
|
|
||||||
unregisteredEventFresh.mark();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import io.lettuce.core.ScriptOutputType;
|
||||||
import io.lettuce.core.cluster.SlotHash;
|
import io.lettuce.core.cluster.SlotHash;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
|
||||||
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
|
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
|
||||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
@ -184,7 +183,7 @@ public class ApnFallbackManager implements Managed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
apnSender.sendMessage(new ApnMessage(apnId, account.getUuid(), device.getId(), true, Type.NOTIFICATION, Optional.empty()));
|
apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device));
|
||||||
retry.mark();
|
retry.mark();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
|
||||||
public class ApnMessage {
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String APN_VOIP_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
|
||||||
public static final String APN_NSE_NOTIFICATION_PAYLOAD = "{\"aps\":{\"mutable-content\":1,\"alert\":{\"loc-key\":\"APN_Message\"}}}";
|
|
||||||
public static final String APN_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"challenge\" : \"%s\"}";
|
|
||||||
public static final String APN_RATE_LIMIT_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"rateLimitChallenge\" : \"%s\"}";
|
|
||||||
public static final long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L;
|
|
||||||
|
|
||||||
private final String apnId;
|
|
||||||
private final long deviceId;
|
|
||||||
private final boolean isVoip;
|
|
||||||
private final Type type;
|
|
||||||
private final Optional<String> challengeData;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final UUID uuid;
|
|
||||||
|
|
||||||
public ApnMessage(String apnId, @Nullable UUID uuid, long deviceId, boolean isVoip, Type type, Optional<String> challengeData) {
|
|
||||||
this.apnId = apnId;
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.isVoip = isVoip;
|
|
||||||
this.type = type;
|
|
||||||
this.challengeData = challengeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVoip() {
|
|
||||||
return isVoip;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getApnId() {
|
|
||||||
return apnId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMessage() {
|
|
||||||
switch (type) {
|
|
||||||
case NOTIFICATION:
|
|
||||||
return this.isVoip() ? APN_VOIP_NOTIFICATION_PAYLOAD : APN_NSE_NOTIFICATION_PAYLOAD;
|
|
||||||
|
|
||||||
case CHALLENGE:
|
|
||||||
return String.format(APN_CHALLENGE_PAYLOAD, challengeData.orElseThrow(AssertionError::new));
|
|
||||||
|
|
||||||
case RATE_LIMIT_CHALLENGE:
|
|
||||||
return String.format(APN_RATE_LIMIT_CHALLENGE_PAYLOAD, challengeData.orElseThrow(AssertionError::new));
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getCollapseId() {
|
|
||||||
if (type == Type.NOTIFICATION && !isVoip) {
|
|
||||||
return "incoming-message";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
public Optional<String> getChallengeData() {
|
|
||||||
return challengeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getExpirationTime() {
|
|
||||||
return MAX_EXPIRATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<UUID> getUuid() {
|
|
||||||
return Optional.ofNullable(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDeviceId() {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
|
||||||
|
|
||||||
import com.google.api.core.ApiFuture;
|
import com.google.api.core.ApiFuture;
|
||||||
import com.google.auth.oauth2.GoogleCredentials;
|
import com.google.auth.oauth2.GoogleCredentials;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
@ -16,33 +14,24 @@ import com.google.firebase.messaging.AndroidConfig;
|
||||||
import com.google.firebase.messaging.FirebaseMessaging;
|
import com.google.firebase.messaging.FirebaseMessaging;
|
||||||
import com.google.firebase.messaging.FirebaseMessagingException;
|
import com.google.firebase.messaging.FirebaseMessagingException;
|
||||||
import com.google.firebase.messaging.Message;
|
import com.google.firebase.messaging.Message;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import com.google.firebase.messaging.MessagingErrorCode;
|
||||||
import io.micrometer.core.instrument.Tags;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Optional;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
public class FcmSender {
|
public class FcmSender implements PushNotificationSender {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(FcmSender.class);
|
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final FirebaseMessaging firebaseMessagingClient;
|
private final FirebaseMessaging firebaseMessagingClient;
|
||||||
|
|
||||||
private static final String SENT_MESSAGE_COUNTER_NAME = name(FcmSender.class, "sentMessage");
|
private static final Logger logger = LoggerFactory.getLogger(FcmSender.class);
|
||||||
|
|
||||||
public FcmSender(ExecutorService executor, AccountsManager accountsManager, String credentials) throws IOException {
|
public FcmSender(ExecutorService executor, String credentials) throws IOException {
|
||||||
try (final ByteArrayInputStream credentialInputStream = new ByteArrayInputStream(credentials.getBytes(StandardCharsets.UTF_8))) {
|
try (final ByteArrayInputStream credentialInputStream = new ByteArrayInputStream(credentials.getBytes(StandardCharsets.UTF_8))) {
|
||||||
FirebaseOptions options = FirebaseOptions.builder()
|
FirebaseOptions options = FirebaseOptions.builder()
|
||||||
.setCredentials(GoogleCredentials.fromStream(credentialInputStream))
|
.setCredentials(GoogleCredentials.fromStream(credentialInputStream))
|
||||||
|
@ -52,102 +41,62 @@ public class FcmSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.firebaseMessagingClient = FirebaseMessaging.getInstance();
|
this.firebaseMessagingClient = FirebaseMessaging.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public FcmSender(ExecutorService executor, AccountsManager accountsManager, FirebaseMessaging firebaseMessagingClient) {
|
public FcmSender(ExecutorService executor, FirebaseMessaging firebaseMessagingClient) {
|
||||||
this.accountsManager = accountsManager;
|
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.firebaseMessagingClient = firebaseMessagingClient;
|
this.firebaseMessagingClient = firebaseMessagingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(GcmMessage message) {
|
@Override
|
||||||
|
public CompletableFuture<SendPushNotificationResult> sendNotification(PushNotification pushNotification) {
|
||||||
Message.Builder builder = Message.builder()
|
Message.Builder builder = Message.builder()
|
||||||
.setToken(message.getGcmId())
|
.setToken(pushNotification.deviceToken())
|
||||||
.setAndroidConfig(AndroidConfig.builder()
|
.setAndroidConfig(AndroidConfig.builder()
|
||||||
.setPriority(AndroidConfig.Priority.HIGH)
|
.setPriority(AndroidConfig.Priority.HIGH)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
final String key = switch (message.getType()) {
|
final String key = switch (pushNotification.notificationType()) {
|
||||||
case NOTIFICATION -> "notification";
|
case NOTIFICATION -> "notification";
|
||||||
case CHALLENGE -> "challenge";
|
case CHALLENGE -> "challenge";
|
||||||
case RATE_LIMIT_CHALLENGE -> "rateLimitChallenge";
|
case RATE_LIMIT_CHALLENGE -> "rateLimitChallenge";
|
||||||
};
|
};
|
||||||
|
|
||||||
builder.putData(key, message.getData().orElse(""));
|
builder.putData(key, pushNotification.data() != null ? pushNotification.data() : "");
|
||||||
|
|
||||||
final ApiFuture<String> sendFuture = firebaseMessagingClient.sendAsync(builder.build());
|
final ApiFuture<String> sendFuture = firebaseMessagingClient.sendAsync(builder.build());
|
||||||
|
final CompletableFuture<SendPushNotificationResult> completableSendFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
sendFuture.addListener(() -> {
|
sendFuture.addListener(() -> {
|
||||||
Tags tags = Tags.of("type", key);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendFuture.get();
|
sendFuture.get();
|
||||||
|
completableSendFuture.complete(new SendPushNotificationResult(true, null, false));
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
if (e.getCause() instanceof final FirebaseMessagingException firebaseMessagingException) {
|
if (e.getCause() instanceof final FirebaseMessagingException firebaseMessagingException) {
|
||||||
final String errorCode;
|
final String errorCode;
|
||||||
|
|
||||||
if (firebaseMessagingException.getMessagingErrorCode() != null) {
|
if (firebaseMessagingException.getMessagingErrorCode() != null) {
|
||||||
errorCode = firebaseMessagingException.getMessagingErrorCode().name().toLowerCase();
|
errorCode = firebaseMessagingException.getMessagingErrorCode().name();
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Received an FCM exception with no error code", firebaseMessagingException);
|
logger.warn("Received an FCM exception with no error code", firebaseMessagingException);
|
||||||
errorCode = "unknown";
|
errorCode = "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
tags = tags.and("errorCode", errorCode);
|
completableSendFuture.complete(new SendPushNotificationResult(false,
|
||||||
|
errorCode,
|
||||||
switch (firebaseMessagingException.getMessagingErrorCode()) {
|
firebaseMessagingException.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED));
|
||||||
|
|
||||||
case UNREGISTERED -> handleBadRegistration(message);
|
|
||||||
case THIRD_PARTY_AUTH_ERROR, INVALID_ARGUMENT, INTERNAL, QUOTA_EXCEEDED, SENDER_ID_MISMATCH, UNAVAILABLE ->
|
|
||||||
logger.debug("Unrecoverable Error ::: (error={}}), (gcm_id={}}), (destination={}}), (device_id={}})",
|
|
||||||
firebaseMessagingException.getMessagingErrorCode(), message.getGcmId(), message.getUuid(), message.getDeviceId());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("Failed to send message", e);
|
completableSendFuture.completeExceptionally(e.getCause());
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// This should never happen; by definition, if we're in the future's listener, the future is done, and so
|
// This should never happen; by definition, if we're in the future's listener, the future is done, and so
|
||||||
// `get()` should return immediately.
|
// `get()` should return immediately.
|
||||||
throw new IllegalStateException("Interrupted while getting send future result", e);
|
completableSendFuture.completeExceptionally(e);
|
||||||
} finally {
|
|
||||||
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
|
|
||||||
}
|
}
|
||||||
}, executor);
|
}, executor);
|
||||||
}
|
|
||||||
|
|
||||||
private void handleBadRegistration(GcmMessage message) {
|
return completableSendFuture;
|
||||||
Optional<Account> account = getAccountForEvent(message);
|
|
||||||
|
|
||||||
if (account.isPresent()) {
|
|
||||||
//noinspection OptionalGetWithoutIsPresent
|
|
||||||
Device device = account.get().getDevice(message.getDeviceId()).get();
|
|
||||||
|
|
||||||
if (device.getUninstalledFeedbackTimestamp() == 0) {
|
|
||||||
accountsManager.updateDevice(account.get(), message.getDeviceId(), d ->
|
|
||||||
d.setUninstalledFeedbackTimestamp(Util.todayInMillis()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Account> getAccountForEvent(GcmMessage message) {
|
|
||||||
Optional<Account> account = message.getUuid().flatMap(accountsManager::getByAccountIdentifier);
|
|
||||||
|
|
||||||
if (account.isPresent()) {
|
|
||||||
Optional<Device> device = account.get().getDevice(message.getDeviceId());
|
|
||||||
|
|
||||||
if (device.isPresent()) {
|
|
||||||
if (message.getGcmId().equals(device.get().getGcmId())) {
|
|
||||||
|
|
||||||
if (device.get().getPushTimestamp() == 0 || System.currentTimeMillis() > (device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10))) {
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
|
||||||
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
|
||||||
public class GcmMessage {
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
|
|
||||||
}
|
|
||||||
|
|
||||||
private final String gcmId;
|
|
||||||
private final int deviceId;
|
|
||||||
private final Type type;
|
|
||||||
private final Optional<String> data;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private final UUID uuid;
|
|
||||||
|
|
||||||
public GcmMessage(String gcmId, @Nullable UUID uuid, int deviceId, Type type, Optional<String> data) {
|
|
||||||
this.gcmId = gcmId;
|
|
||||||
this.uuid = uuid;
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.type = type;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getGcmId() {
|
|
||||||
return gcmId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<UUID> getUuid() {
|
|
||||||
return Optional.ofNullable(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDeviceId() {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<String> getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -7,18 +7,15 @@ package org.whispersystems.textsecuregcm.push;
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||||
|
|
||||||
import io.dropwizard.lifecycle.Managed;
|
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.Tag;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
|
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
|
||||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages,
|
* A MessageSender sends Signal messages to destination devices. Messages may be "normal" user-to-user messages,
|
||||||
|
@ -33,41 +30,30 @@ import org.whispersystems.textsecuregcm.util.Util;
|
||||||
* @see org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener
|
* @see org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener
|
||||||
* @see ReceiptSender
|
* @see ReceiptSender
|
||||||
*/
|
*/
|
||||||
public class MessageSender implements Managed {
|
public class MessageSender {
|
||||||
|
|
||||||
private final ApnFallbackManager apnFallbackManager;
|
private final ClientPresenceManager clientPresenceManager;
|
||||||
private final ClientPresenceManager clientPresenceManager;
|
private final MessagesManager messagesManager;
|
||||||
private final MessagesManager messagesManager;
|
private final PushNotificationManager pushNotificationManager;
|
||||||
private final FcmSender fcmSender;
|
private final PushLatencyManager pushLatencyManager;
|
||||||
private final APNSender apnSender;
|
|
||||||
private final PushLatencyManager pushLatencyManager;
|
|
||||||
|
|
||||||
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
||||||
private static final String CHANNEL_TAG_NAME = "channel";
|
private static final String CHANNEL_TAG_NAME = "channel";
|
||||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||||
private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline";
|
private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline";
|
||||||
|
|
||||||
public MessageSender(ApnFallbackManager apnFallbackManager,
|
public MessageSender(ClientPresenceManager clientPresenceManager,
|
||||||
ClientPresenceManager clientPresenceManager,
|
MessagesManager messagesManager,
|
||||||
MessagesManager messagesManager,
|
PushNotificationManager pushNotificationManager,
|
||||||
FcmSender fcmSender,
|
PushLatencyManager pushLatencyManager) {
|
||||||
APNSender apnSender,
|
|
||||||
PushLatencyManager pushLatencyManager)
|
|
||||||
{
|
|
||||||
this.apnFallbackManager = apnFallbackManager;
|
|
||||||
this.clientPresenceManager = clientPresenceManager;
|
this.clientPresenceManager = clientPresenceManager;
|
||||||
this.messagesManager = messagesManager;
|
this.messagesManager = messagesManager;
|
||||||
this.fcmSender = fcmSender;
|
this.pushNotificationManager = pushNotificationManager;
|
||||||
this.apnSender = apnSender;
|
this.pushLatencyManager = pushLatencyManager;
|
||||||
this.pushLatencyManager = pushLatencyManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessage(final Account account, final Device device, final Envelope message, boolean online)
|
public void sendMessage(final Account account, final Device device, final Envelope message, boolean online)
|
||||||
throws NotPushRegisteredException
|
throws NotPushRegisteredException {
|
||||||
{
|
|
||||||
if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) {
|
|
||||||
throw new NotPushRegisteredException("No delivery possible!");
|
|
||||||
}
|
|
||||||
|
|
||||||
final String channel;
|
final String channel;
|
||||||
|
|
||||||
|
@ -98,59 +84,24 @@ public class MessageSender implements Managed {
|
||||||
clientPresent = clientPresenceManager.isPresent(account.getUuid(), device.getId());
|
clientPresent = clientPresenceManager.isPresent(account.getUuid(), device.getId());
|
||||||
|
|
||||||
if (!clientPresent) {
|
if (!clientPresent) {
|
||||||
sendNewMessageNotification(account, device);
|
try {
|
||||||
|
pushNotificationManager.sendNewMessageNotification(account, device.getId());
|
||||||
|
|
||||||
|
final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId());
|
||||||
|
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
|
||||||
|
} catch (final NotPushRegisteredException e) {
|
||||||
|
if (!device.getFetchesMessages()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Tag> tags = List.of(
|
final List<Tag> tags = List.of(
|
||||||
Tag.of(CHANNEL_TAG_NAME, channel),
|
Tag.of(CHANNEL_TAG_NAME, channel),
|
||||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||||
Tag.of(CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent)));
|
Tag.of(CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent)));
|
||||||
|
|
||||||
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
|
Metrics.counter(SEND_COUNTER_NAME, tags).increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendNewMessageNotification(final Account account, final Device device) {
|
|
||||||
if (!Util.isEmpty(device.getGcmId())) {
|
|
||||||
sendGcmNotification(account, device);
|
|
||||||
} else if (!Util.isEmpty(device.getApnId()) || !Util.isEmpty(device.getVoipApnId())) {
|
|
||||||
sendApnNotification(account, device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendGcmNotification(Account account, Device device) {
|
|
||||||
GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getUuid(),
|
|
||||||
(int)device.getId(), GcmMessage.Type.NOTIFICATION, Optional.empty());
|
|
||||||
|
|
||||||
fcmSender.sendMessage(gcmMessage);
|
|
||||||
|
|
||||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendApnNotification(Account account, Device device) {
|
|
||||||
ApnMessage apnMessage;
|
|
||||||
|
|
||||||
final boolean useVoip = !Util.isEmpty(device.getVoipApnId());
|
|
||||||
|
|
||||||
if (useVoip) {
|
|
||||||
apnMessage = new ApnMessage(device.getVoipApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
|
||||||
RedisOperation.unchecked(() -> apnFallbackManager.schedule(account, device));
|
|
||||||
} else {
|
|
||||||
apnMessage = new ApnMessage(device.getApnId(), account.getUuid(), device.getId(), useVoip, Type.NOTIFICATION, Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
apnSender.sendMessage(apnMessage);
|
|
||||||
|
|
||||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
apnSender.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
apnSender.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,4 @@ public class NotPushRegisteredException extends Exception {
|
||||||
public NotPushRegisteredException() {
|
public NotPushRegisteredException() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public NotPushRegisteredException(String s) {
|
|
||||||
super(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NotPushRegisteredException(Exception e) {
|
|
||||||
super(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public record PushNotification(String deviceToken,
|
||||||
|
TokenType tokenType,
|
||||||
|
NotificationType notificationType,
|
||||||
|
@Nullable String data,
|
||||||
|
@Nullable Account destination,
|
||||||
|
@Nullable Device destinationDevice) {
|
||||||
|
|
||||||
|
public enum NotificationType {
|
||||||
|
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TokenType {
|
||||||
|
FCM,
|
||||||
|
APN,
|
||||||
|
APN_VOIP,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import io.micrometer.core.instrument.Tags;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||||
|
|
||||||
|
public class PushNotificationManager {
|
||||||
|
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
|
private final APNSender apnSender;
|
||||||
|
private final FcmSender fcmSender;
|
||||||
|
private final ApnFallbackManager fallbackManager;
|
||||||
|
|
||||||
|
private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification");
|
||||||
|
private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification");
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(PushNotificationManager.class);
|
||||||
|
|
||||||
|
public PushNotificationManager(final AccountsManager accountsManager,
|
||||||
|
final APNSender apnSender,
|
||||||
|
final FcmSender fcmSender,
|
||||||
|
final ApnFallbackManager fallbackManager) {
|
||||||
|
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
|
this.apnSender = apnSender;
|
||||||
|
this.fcmSender = fcmSender;
|
||||||
|
this.fallbackManager = fallbackManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendNewMessageNotification(final Account destination, final long destinationDeviceId) throws NotPushRegisteredException {
|
||||||
|
final Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new);
|
||||||
|
final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
|
||||||
|
|
||||||
|
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||||
|
PushNotification.NotificationType.NOTIFICATION, null, destination, device));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendRegistrationChallengeNotification(final String deviceToken, final PushNotification.TokenType tokenType, final String challengeToken) {
|
||||||
|
sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendRateLimitChallengeNotification(final Account destination, final String challengeToken)
|
||||||
|
throws NotPushRegisteredException {
|
||||||
|
|
||||||
|
final Device device = destination.getDevice(Device.MASTER_ID).orElseThrow(NotPushRegisteredException::new);
|
||||||
|
final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
|
||||||
|
|
||||||
|
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||||
|
PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Pair<String, PushNotification.TokenType> getToken(final Device device) throws NotPushRegisteredException {
|
||||||
|
final Pair<String, PushNotification.TokenType> tokenAndType;
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(device.getGcmId())) {
|
||||||
|
tokenAndType = new Pair<>(device.getGcmId(), PushNotification.TokenType.FCM);
|
||||||
|
} else if (StringUtils.isNotBlank(device.getVoipApnId())) {
|
||||||
|
tokenAndType = new Pair<>(device.getVoipApnId(), PushNotification.TokenType.APN_VOIP);
|
||||||
|
} else if (StringUtils.isNotBlank(device.getApnId())) {
|
||||||
|
tokenAndType = new Pair<>(device.getApnId(), PushNotification.TokenType.APN);
|
||||||
|
} else {
|
||||||
|
throw new NotPushRegisteredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenAndType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void sendNotification(final PushNotification pushNotification) {
|
||||||
|
final PushNotificationSender sender = switch (pushNotification.tokenType()) {
|
||||||
|
case FCM -> fcmSender;
|
||||||
|
case APN, APN_VOIP -> apnSender;
|
||||||
|
};
|
||||||
|
|
||||||
|
sender.sendNotification(pushNotification).whenComplete((result, throwable) -> {
|
||||||
|
if (throwable == null) {
|
||||||
|
Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(),
|
||||||
|
"notificationType", pushNotification.notificationType().name(),
|
||||||
|
"accepted", String.valueOf(result.accepted()),
|
||||||
|
"unregistered", String.valueOf(result.unregistered()));
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(result.errorCode())) {
|
||||||
|
tags = tags.and("errorCode", result.errorCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
Metrics.counter(SENT_NOTIFICATION_COUNTER_NAME, tags).increment();
|
||||||
|
|
||||||
|
if (result.unregistered() && pushNotification.destination() != null && pushNotification.destinationDevice() != null) {
|
||||||
|
handleDeviceUnregistered(pushNotification.destination(), pushNotification.destinationDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.accepted() &&
|
||||||
|
pushNotification.tokenType() == PushNotification.TokenType.APN_VOIP &&
|
||||||
|
pushNotification.notificationType() == PushNotification.NotificationType.NOTIFICATION &&
|
||||||
|
pushNotification.destination() != null &&
|
||||||
|
pushNotification.destinationDevice() != null) {
|
||||||
|
|
||||||
|
RedisOperation.unchecked(() -> fallbackManager.schedule(pushNotification.destination(),
|
||||||
|
pushNotification.destinationDevice()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("Failed to deliver {} push notification to {} ({})",
|
||||||
|
pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(), throwable);
|
||||||
|
|
||||||
|
Metrics.counter(FAILED_NOTIFICATION_COUNTER_NAME, "cause", throwable.getClass().getSimpleName()).increment();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDeviceUnregistered(final Account account, final Device device) {
|
||||||
|
if (StringUtils.isNotBlank(device.getGcmId())) {
|
||||||
|
if (device.getUninstalledFeedbackTimestamp() == 0) {
|
||||||
|
accountsManager.updateDevice(account, device.getId(), d ->
|
||||||
|
d.setUninstalledFeedbackTimestamp(Util.todayInMillis()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RedisOperation.unchecked(() -> fallbackManager.cancel(account, device));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface PushNotificationSender {
|
||||||
|
|
||||||
|
CompletableFuture<SendPushNotificationResult> sendNotification(PushNotification notification);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public record SendPushNotificationResult(boolean accepted, @Nullable String errorCode, boolean unregistered) {
|
||||||
|
}
|
|
@ -20,7 +20,8 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
@ -42,20 +43,21 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||||
|
|
||||||
private final ReceiptSender receiptSender;
|
private final ReceiptSender receiptSender;
|
||||||
private final MessagesManager messagesManager;
|
private final MessagesManager messagesManager;
|
||||||
private final MessageSender messageSender;
|
private final PushNotificationManager pushNotificationManager;
|
||||||
private final ApnFallbackManager apnFallbackManager;
|
private final ApnFallbackManager apnFallbackManager;
|
||||||
private final ClientPresenceManager clientPresenceManager;
|
private final ClientPresenceManager clientPresenceManager;
|
||||||
private final ScheduledExecutorService scheduledExecutorService;
|
private final ScheduledExecutorService scheduledExecutorService;
|
||||||
|
|
||||||
public AuthenticatedConnectListener(ReceiptSender receiptSender,
|
public AuthenticatedConnectListener(ReceiptSender receiptSender,
|
||||||
MessagesManager messagesManager,
|
MessagesManager messagesManager,
|
||||||
final MessageSender messageSender, ApnFallbackManager apnFallbackManager,
|
PushNotificationManager pushNotificationManager,
|
||||||
|
ApnFallbackManager apnFallbackManager,
|
||||||
ClientPresenceManager clientPresenceManager,
|
ClientPresenceManager clientPresenceManager,
|
||||||
ScheduledExecutorService scheduledExecutorService)
|
ScheduledExecutorService scheduledExecutorService)
|
||||||
{
|
{
|
||||||
this.receiptSender = receiptSender;
|
this.receiptSender = receiptSender;
|
||||||
this.messagesManager = messagesManager;
|
this.messagesManager = messagesManager;
|
||||||
this.messageSender = messageSender;
|
this.pushNotificationManager = pushNotificationManager;
|
||||||
this.apnFallbackManager = apnFallbackManager;
|
this.apnFallbackManager = apnFallbackManager;
|
||||||
this.clientPresenceManager = clientPresenceManager;
|
this.clientPresenceManager = clientPresenceManager;
|
||||||
this.scheduledExecutorService = scheduledExecutorService;
|
this.scheduledExecutorService = scheduledExecutorService;
|
||||||
|
@ -97,7 +99,10 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||||
messagesManager.removeMessageAvailabilityListener(connection);
|
messagesManager.removeMessageAvailabilityListener(connection);
|
||||||
|
|
||||||
if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) {
|
if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) {
|
||||||
messageSender.sendNewMessageNotification(auth.getAccount(), device);
|
try {
|
||||||
|
pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId());
|
||||||
|
} catch (NotPushRegisteredException ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.eatthepath.pushy.apns.ApnsClient;
|
||||||
|
import com.eatthepath.pushy.apns.ApnsPushNotification;
|
||||||
|
import com.eatthepath.pushy.apns.DeliveryPriority;
|
||||||
|
import com.eatthepath.pushy.apns.PushNotificationResponse;
|
||||||
|
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
|
||||||
|
import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
||||||
|
|
||||||
|
class APNSenderTest {
|
||||||
|
|
||||||
|
private static final String DESTINATION_APN_ID = "foo";
|
||||||
|
|
||||||
|
private Account destinationAccount;
|
||||||
|
private Device destinationDevice;
|
||||||
|
|
||||||
|
private ApnsClient apnsClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
destinationAccount = mock(Account.class);
|
||||||
|
destinationDevice = mock(Device.class);
|
||||||
|
|
||||||
|
apnsClient = mock(ApnsClient.class);
|
||||||
|
|
||||||
|
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
|
||||||
|
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendVoip() {
|
||||||
|
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
||||||
|
when(response.isAccepted()).thenReturn(true);
|
||||||
|
|
||||||
|
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
||||||
|
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
||||||
|
|
||||||
|
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
||||||
|
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
|
||||||
|
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
|
||||||
|
|
||||||
|
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
|
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
||||||
|
verify(apnsClient).sendNotification(notification.capture());
|
||||||
|
|
||||||
|
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
||||||
|
assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
|
||||||
|
assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
|
||||||
|
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
||||||
|
assertThat(notification.getValue().getTopic()).isEqualTo("foo.voip");
|
||||||
|
|
||||||
|
assertThat(result.accepted()).isTrue();
|
||||||
|
assertThat(result.errorCode()).isNull();
|
||||||
|
assertThat(result.unregistered()).isFalse();
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(apnsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendApns() {
|
||||||
|
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
||||||
|
when(response.isAccepted()).thenReturn(true);
|
||||||
|
|
||||||
|
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
||||||
|
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
||||||
|
|
||||||
|
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
||||||
|
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
|
||||||
|
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
|
||||||
|
|
||||||
|
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
|
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
||||||
|
verify(apnsClient).sendNotification(notification.capture());
|
||||||
|
|
||||||
|
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
||||||
|
assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
|
||||||
|
assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_NSE_NOTIFICATION_PAYLOAD);
|
||||||
|
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
||||||
|
assertThat(notification.getValue().getTopic()).isEqualTo("foo");
|
||||||
|
|
||||||
|
assertThat(result.accepted()).isTrue();
|
||||||
|
assertThat(result.errorCode()).isNull();
|
||||||
|
assertThat(result.unregistered()).isFalse();
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(apnsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUnregisteredUser() throws Exception {
|
||||||
|
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
||||||
|
when(response.isAccepted()).thenReturn(false);
|
||||||
|
when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered"));
|
||||||
|
|
||||||
|
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
||||||
|
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
||||||
|
|
||||||
|
|
||||||
|
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
||||||
|
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
|
||||||
|
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
|
||||||
|
|
||||||
|
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
|
||||||
|
when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
|
||||||
|
|
||||||
|
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
|
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
||||||
|
verify(apnsClient).sendNotification(notification.capture());
|
||||||
|
|
||||||
|
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
||||||
|
assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
|
||||||
|
assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
|
||||||
|
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
||||||
|
|
||||||
|
assertThat(result.accepted()).isFalse();
|
||||||
|
assertThat(result.errorCode()).isEqualTo("Unregistered");
|
||||||
|
assertThat(result.unregistered()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenericFailure() {
|
||||||
|
ApnsClient apnsClient = mock(ApnsClient.class);
|
||||||
|
|
||||||
|
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
||||||
|
when(response.isAccepted()).thenReturn(false);
|
||||||
|
when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic"));
|
||||||
|
|
||||||
|
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
||||||
|
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
||||||
|
|
||||||
|
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
||||||
|
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
|
||||||
|
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
|
||||||
|
|
||||||
|
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
|
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
||||||
|
verify(apnsClient).sendNotification(notification.capture());
|
||||||
|
|
||||||
|
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
||||||
|
assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
|
||||||
|
assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
|
||||||
|
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
||||||
|
|
||||||
|
assertThat(result.accepted()).isFalse();
|
||||||
|
assertThat(result.errorCode()).isEqualTo("BadTopic");
|
||||||
|
assertThat(result.unregistered()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFailure() {
|
||||||
|
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
||||||
|
when(response.isAccepted()).thenReturn(true);
|
||||||
|
|
||||||
|
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
||||||
|
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), new IOException("lost connection")));
|
||||||
|
|
||||||
|
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
||||||
|
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
|
||||||
|
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join())
|
||||||
|
.isInstanceOf(CompletionException.class)
|
||||||
|
.hasCauseInstanceOf(IOException.class);
|
||||||
|
|
||||||
|
verify(apnsClient).sendNotification(any());
|
||||||
|
|
||||||
|
verifyNoMoreInteractions(apnsClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MockPushNotificationFuture <P extends ApnsPushNotification, V> extends PushNotificationFuture<P, V> {
|
||||||
|
|
||||||
|
MockPushNotificationFuture(final P pushNotification, final V response) {
|
||||||
|
super(pushNotification);
|
||||||
|
complete(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
MockPushNotificationFuture(final P pushNotification, final Exception exception) {
|
||||||
|
super(pushNotification);
|
||||||
|
completeExceptionally(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -100,14 +100,14 @@ class ApnFallbackManagerTest {
|
||||||
|
|
||||||
assertEquals(1, worker.processNextSlot());
|
assertEquals(1, worker.processNextSlot());
|
||||||
|
|
||||||
final ArgumentCaptor<ApnMessage> messageCaptor = ArgumentCaptor.forClass(ApnMessage.class);
|
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
|
||||||
verify(apnSender).sendMessage(messageCaptor.capture());
|
verify(apnSender).sendNotification(notificationCaptor.capture());
|
||||||
|
|
||||||
final ApnMessage message = messageCaptor.getValue();
|
final PushNotification pushNotification = notificationCaptor.getValue();
|
||||||
|
|
||||||
assertEquals(VOIP_APN_ID, message.getApnId());
|
assertEquals(VOIP_APN_ID, pushNotification.deviceToken());
|
||||||
assertEquals(Optional.of(ACCOUNT_UUID), message.getUuid());
|
assertEquals(account, pushNotification.destination());
|
||||||
assertEquals(DEVICE_ID, message.getDeviceId());
|
assertEquals(device, pushNotification.destinationDevice());
|
||||||
|
|
||||||
assertEquals(0, worker.processNextSlot());
|
assertEquals(0, worker.processNextSlot());
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,12 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
@ -16,24 +20,18 @@ import com.google.firebase.messaging.FirebaseMessaging;
|
||||||
import com.google.firebase.messaging.FirebaseMessagingException;
|
import com.google.firebase.messaging.FirebaseMessagingException;
|
||||||
import com.google.firebase.messaging.Message;
|
import com.google.firebase.messaging.Message;
|
||||||
import com.google.firebase.messaging.MessagingErrorCode;
|
import com.google.firebase.messaging.MessagingErrorCode;
|
||||||
import java.util.Optional;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
|
||||||
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
|
||||||
|
|
||||||
class FcmSenderTest {
|
class FcmSenderTest {
|
||||||
|
|
||||||
private ExecutorService executorService;
|
private ExecutorService executorService;
|
||||||
private AccountsManager accountsManager;
|
|
||||||
private FirebaseMessaging firebaseMessaging;
|
private FirebaseMessaging firebaseMessaging;
|
||||||
|
|
||||||
private FcmSender fcmSender;
|
private FcmSender fcmSender;
|
||||||
|
@ -41,10 +39,9 @@ class FcmSenderTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
executorService = new SynchronousExecutorService();
|
executorService = new SynchronousExecutorService();
|
||||||
accountsManager = mock(AccountsManager.class);
|
|
||||||
firebaseMessaging = mock(FirebaseMessaging.class);
|
firebaseMessaging = mock(FirebaseMessaging.class);
|
||||||
|
|
||||||
fcmSender = new FcmSender(executorService, accountsManager, firebaseMessaging);
|
fcmSender = new FcmSender(executorService, firebaseMessaging);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
@ -57,35 +54,44 @@ class FcmSenderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSendMessage() {
|
void testSendMessage() {
|
||||||
AccountsHelper.setupMockUpdate(accountsManager);
|
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||||
|
|
||||||
final GcmMessage message = new GcmMessage("foo", UUID.randomUUID(), 1, GcmMessage.Type.NOTIFICATION, Optional.empty());
|
|
||||||
|
|
||||||
final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
|
final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
|
||||||
sendFuture.set("message-id");
|
sendFuture.set("message-id");
|
||||||
|
|
||||||
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||||
|
|
||||||
fcmSender.sendMessage(message);
|
final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
verify(firebaseMessaging).sendAsync(any(Message.class));
|
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||||
|
assertTrue(result.accepted());
|
||||||
|
assertNull(result.errorCode());
|
||||||
|
assertFalse(result.unregistered());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSendUninstalled() {
|
void testSendMessageRejected() {
|
||||||
final UUID destinationUuid = UUID.randomUUID();
|
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||||
final String gcmId = "foo";
|
|
||||||
|
|
||||||
final Account destinationAccount = mock(Account.class);
|
final FirebaseMessagingException invalidArgumentException = mock(FirebaseMessagingException.class);
|
||||||
final Device destinationDevice = mock(Device.class );
|
when(invalidArgumentException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.INVALID_ARGUMENT);
|
||||||
|
|
||||||
AccountsHelper.setupMockUpdate(accountsManager);
|
final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
|
||||||
|
sendFuture.setException(invalidArgumentException);
|
||||||
|
|
||||||
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
|
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||||
when(accountsManager.getByAccountIdentifier(destinationUuid)).thenReturn(Optional.of(destinationAccount));
|
|
||||||
when(destinationDevice.getGcmId()).thenReturn(gcmId);
|
|
||||||
|
|
||||||
final GcmMessage message = new GcmMessage(gcmId, destinationUuid, 1, GcmMessage.Type.NOTIFICATION, Optional.empty());
|
final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
|
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||||
|
assertFalse(result.accepted());
|
||||||
|
assertEquals("INVALID_ARGUMENT", result.errorCode());
|
||||||
|
assertFalse(result.unregistered());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendMessageUnregistered() {
|
||||||
|
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||||
|
|
||||||
final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class);
|
final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class);
|
||||||
when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
|
when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
|
||||||
|
@ -95,11 +101,27 @@ class FcmSenderTest {
|
||||||
|
|
||||||
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||||
|
|
||||||
fcmSender.sendMessage(message);
|
final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join();
|
||||||
|
|
||||||
verify(firebaseMessaging).sendAsync(any(Message.class));
|
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||||
verify(accountsManager).getByAccountIdentifier(destinationUuid);
|
assertFalse(result.accepted());
|
||||||
verify(accountsManager).updateDevice(eq(destinationAccount), eq(1L), any());
|
assertEquals("UNREGISTERED", result.errorCode());
|
||||||
verify(destinationDevice).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
assertTrue(result.unregistered());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendMessageException() {
|
||||||
|
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||||
|
|
||||||
|
final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
|
||||||
|
sendFuture.setException(new IOException());
|
||||||
|
|
||||||
|
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||||
|
|
||||||
|
final CompletionException completionException =
|
||||||
|
assertThrows(CompletionException.class, () -> fcmSender.sendNotification(pushNotification).join());
|
||||||
|
|
||||||
|
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||||
|
assertTrue(completionException.getCause() instanceof IOException);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.push;
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -37,8 +39,7 @@ class MessageSenderTest {
|
||||||
|
|
||||||
private ClientPresenceManager clientPresenceManager;
|
private ClientPresenceManager clientPresenceManager;
|
||||||
private MessagesManager messagesManager;
|
private MessagesManager messagesManager;
|
||||||
private FcmSender fcmSender;
|
private PushNotificationManager pushNotificationManager;
|
||||||
private APNSender apnSender;
|
|
||||||
private MessageSender messageSender;
|
private MessageSender messageSender;
|
||||||
|
|
||||||
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
|
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
|
||||||
|
@ -53,13 +54,10 @@ class MessageSenderTest {
|
||||||
|
|
||||||
clientPresenceManager = mock(ClientPresenceManager.class);
|
clientPresenceManager = mock(ClientPresenceManager.class);
|
||||||
messagesManager = mock(MessagesManager.class);
|
messagesManager = mock(MessagesManager.class);
|
||||||
fcmSender = mock(FcmSender.class);
|
pushNotificationManager = mock(PushNotificationManager.class);
|
||||||
apnSender = mock(APNSender.class);
|
messageSender = new MessageSender(clientPresenceManager,
|
||||||
messageSender = new MessageSender(mock(ApnFallbackManager.class),
|
|
||||||
clientPresenceManager,
|
|
||||||
messagesManager,
|
messagesManager,
|
||||||
fcmSender,
|
pushNotificationManager,
|
||||||
apnSender,
|
|
||||||
mock(PushLatencyManager.class));
|
mock(PushLatencyManager.class));
|
||||||
|
|
||||||
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
|
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
|
||||||
|
@ -80,8 +78,7 @@ class MessageSenderTest {
|
||||||
|
|
||||||
assertTrue(envelopeArgumentCaptor.getValue().getEphemeral());
|
assertTrue(envelopeArgumentCaptor.getValue().getEphemeral());
|
||||||
|
|
||||||
verifyNoInteractions(fcmSender);
|
verifyNoInteractions(pushNotificationManager);
|
||||||
verifyNoInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -92,8 +89,7 @@ class MessageSenderTest {
|
||||||
messageSender.sendMessage(account, device, message, true);
|
messageSender.sendMessage(account, device, message, true);
|
||||||
|
|
||||||
verify(messagesManager, never()).insert(any(), anyLong(), any());
|
verify(messagesManager, never()).insert(any(), anyLong(), any());
|
||||||
verifyNoInteractions(fcmSender);
|
verifyNoInteractions(pushNotificationManager);
|
||||||
verifyNoInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -110,8 +106,7 @@ class MessageSenderTest {
|
||||||
|
|
||||||
assertFalse(envelopeArgumentCaptor.getValue().getEphemeral());
|
assertFalse(envelopeArgumentCaptor.getValue().getEphemeral());
|
||||||
assertEquals(message, envelopeArgumentCaptor.getValue());
|
assertEquals(message, envelopeArgumentCaptor.getValue());
|
||||||
verifyNoInteractions(fcmSender);
|
verifyNoInteractions(pushNotificationManager);
|
||||||
verifyNoInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -122,8 +117,7 @@ class MessageSenderTest {
|
||||||
messageSender.sendMessage(account, device, message, false);
|
messageSender.sendMessage(account, device, message, false);
|
||||||
|
|
||||||
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
||||||
verify(fcmSender).sendMessage(any());
|
verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
|
||||||
verifyNoInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -134,8 +128,7 @@ class MessageSenderTest {
|
||||||
messageSender.sendMessage(account, device, message, false);
|
messageSender.sendMessage(account, device, message, false);
|
||||||
|
|
||||||
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
||||||
verifyNoInteractions(fcmSender);
|
verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
|
||||||
verify(apnSender).sendMessage(any());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -143,11 +136,11 @@ class MessageSenderTest {
|
||||||
when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false);
|
when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false);
|
||||||
when(device.getFetchesMessages()).thenReturn(true);
|
when(device.getFetchesMessages()).thenReturn(true);
|
||||||
|
|
||||||
messageSender.sendMessage(account, device, message, false);
|
doThrow(NotPushRegisteredException.class)
|
||||||
|
.when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> messageSender.sendMessage(account, device, message, false));
|
||||||
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
||||||
verifyNoInteractions(fcmSender);
|
|
||||||
verifyNoInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MessageProtos.Envelope generateRandomMessage() {
|
private MessageProtos.Envelope generateRandomMessage() {
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2013-2022 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.push;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
class PushNotificationManagerTest {
|
||||||
|
|
||||||
|
private AccountsManager accountsManager;
|
||||||
|
private APNSender apnSender;
|
||||||
|
private FcmSender fcmSender;
|
||||||
|
private ApnFallbackManager apnFallbackManager;
|
||||||
|
|
||||||
|
private PushNotificationManager pushNotificationManager;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
accountsManager = mock(AccountsManager.class);
|
||||||
|
apnSender = mock(APNSender.class);
|
||||||
|
fcmSender = mock(FcmSender.class);
|
||||||
|
apnFallbackManager = mock(ApnFallbackManager.class);
|
||||||
|
|
||||||
|
AccountsHelper.setupMockUpdate(accountsManager);
|
||||||
|
|
||||||
|
pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnFallbackManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendNewMessageNotification() throws NotPushRegisteredException {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
final String deviceToken = "token";
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(device.getGcmId()).thenReturn(deviceToken);
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
when(fcmSender.sendNotification(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID);
|
||||||
|
verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendRegistrationChallengeNotification() {
|
||||||
|
final String deviceToken = "token";
|
||||||
|
final String challengeToken = "challenge";
|
||||||
|
|
||||||
|
when(apnSender.sendNotification(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendRegistrationChallengeNotification(deviceToken, PushNotification.TokenType.APN_VOIP, challengeToken);
|
||||||
|
verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendRateLimitChallengeNotification() throws NotPushRegisteredException {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
final String deviceToken = "token";
|
||||||
|
final String challengeToken = "challenge";
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(device.getApnId()).thenReturn(deviceToken);
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
when(apnSender.sendNotification(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendRateLimitChallengeNotification(account, challengeToken);
|
||||||
|
verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendNotification() {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final PushNotification pushNotification = new PushNotification(
|
||||||
|
"token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device);
|
||||||
|
|
||||||
|
when(fcmSender.sendNotification(pushNotification))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
|
verify(fcmSender).sendNotification(pushNotification);
|
||||||
|
verifyNoInteractions(apnSender);
|
||||||
|
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any());
|
||||||
|
verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
||||||
|
verifyNoInteractions(apnFallbackManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendNotificationApnVoip() {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final PushNotification pushNotification = new PushNotification(
|
||||||
|
"token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device);
|
||||||
|
|
||||||
|
when(apnSender.sendNotification(pushNotification))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
|
verify(apnSender).sendNotification(pushNotification);
|
||||||
|
verifyNoInteractions(fcmSender);
|
||||||
|
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any());
|
||||||
|
verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
||||||
|
verify(apnFallbackManager).schedule(account, device);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendNotificationUnregisteredFcm() {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(device.getGcmId()).thenReturn("token");
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final PushNotification pushNotification = new PushNotification(
|
||||||
|
"token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device);
|
||||||
|
|
||||||
|
when(fcmSender.sendNotification(pushNotification))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
|
verify(accountsManager).updateDevice(eq(account), eq(Device.MASTER_ID), any());
|
||||||
|
verify(device).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
||||||
|
verifyNoInteractions(apnSender);
|
||||||
|
verifyNoInteractions(apnFallbackManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendNotificationUnregisteredApn() {
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
|
||||||
|
when(device.getId()).thenReturn(Device.MASTER_ID);
|
||||||
|
when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final PushNotification pushNotification = new PushNotification(
|
||||||
|
"token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device);
|
||||||
|
|
||||||
|
when(apnSender.sendNotification(pushNotification))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
|
verifyNoInteractions(fcmSender);
|
||||||
|
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any());
|
||||||
|
verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
||||||
|
verify(apnFallbackManager).cancel(account, device);
|
||||||
|
}
|
||||||
|
}
|
|
@ -80,10 +80,8 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
|
||||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||||
|
@ -147,8 +145,7 @@ class AccountControllerTest {
|
||||||
private static Account senderHasStorage = mock(Account.class);
|
private static Account senderHasStorage = mock(Account.class);
|
||||||
private static Account senderTransfer = mock(Account.class);
|
private static Account senderTransfer = mock(Account.class);
|
||||||
private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
|
private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
|
||||||
private static FcmSender fcmSender = mock(FcmSender.class);
|
private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
|
||||||
private static APNSender apnSender = mock(APNSender.class);
|
|
||||||
private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
|
private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
|
||||||
|
|
||||||
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
|
@ -179,8 +176,7 @@ class AccountControllerTest {
|
||||||
turnTokenGenerator,
|
turnTokenGenerator,
|
||||||
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE),
|
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE),
|
||||||
recaptchaClient,
|
recaptchaClient,
|
||||||
fcmSender,
|
pushNotificationManager,
|
||||||
apnSender,
|
|
||||||
verifyExperimentEnrollmentManager,
|
verifyExperimentEnrollmentManager,
|
||||||
changeNumberManager,
|
changeNumberManager,
|
||||||
storageCredentialGenerator))
|
storageCredentialGenerator))
|
||||||
|
@ -322,8 +318,7 @@ class AccountControllerTest {
|
||||||
senderHasStorage,
|
senderHasStorage,
|
||||||
senderTransfer,
|
senderTransfer,
|
||||||
recaptchaClient,
|
recaptchaClient,
|
||||||
fcmSender,
|
pushNotificationManager,
|
||||||
apnSender,
|
|
||||||
verifyExperimentEnrollmentManager,
|
verifyExperimentEnrollmentManager,
|
||||||
changeNumberManager);
|
changeNumberManager);
|
||||||
|
|
||||||
|
@ -339,14 +334,12 @@ class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
ArgumentCaptor<GcmMessage> captor = ArgumentCaptor.forClass(GcmMessage.class);
|
final ArgumentCaptor<String> challengeTokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
verify(fcmSender, times(1)).sendMessage(captor.capture());
|
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||||
assertThat(captor.getValue().getGcmId()).isEqualTo("mytoken");
|
eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture());
|
||||||
assertThat(captor.getValue().getData().isPresent()).isTrue();
|
|
||||||
assertThat(captor.getValue().getData().get().length()).isEqualTo(32);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnSender);
|
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -358,14 +351,12 @@ class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
ArgumentCaptor<GcmMessage> captor = ArgumentCaptor.forClass(GcmMessage.class);
|
final ArgumentCaptor<String> challengeTokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
verify(fcmSender, times(1)).sendMessage(captor.capture());
|
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||||
assertThat(captor.getValue().getGcmId()).isEqualTo("mytoken");
|
eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture());
|
||||||
assertThat(captor.getValue().getData().isPresent()).isTrue();
|
|
||||||
assertThat(captor.getValue().getData().get().length()).isEqualTo(32);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnSender);
|
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -377,16 +368,12 @@ class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
ArgumentCaptor<ApnMessage> captor = ArgumentCaptor.forClass(ApnMessage.class);
|
final ArgumentCaptor<String> challengeTokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
verify(apnSender, times(1)).sendMessage(captor.capture());
|
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture());
|
||||||
assertThat(captor.getValue().getChallengeData().isPresent()).isTrue();
|
|
||||||
assertThat(captor.getValue().getChallengeData().get().length()).isEqualTo(32);
|
|
||||||
assertThat(captor.getValue().getMessage()).contains("\"challenge\" : \"" + captor.getValue().getChallengeData().get() + "\"");
|
|
||||||
assertThat(captor.getValue().isVoip()).isTrue();
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(fcmSender);
|
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -399,16 +386,12 @@ class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
ArgumentCaptor<ApnMessage> captor = ArgumentCaptor.forClass(ApnMessage.class);
|
final ArgumentCaptor<String> challengeTokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
verify(apnSender, times(1)).sendMessage(captor.capture());
|
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture());
|
||||||
assertThat(captor.getValue().getChallengeData().isPresent()).isTrue();
|
|
||||||
assertThat(captor.getValue().getChallengeData().get().length()).isEqualTo(32);
|
|
||||||
assertThat(captor.getValue().getMessage()).contains("\"challenge\" : \"" + captor.getValue().getChallengeData().get() + "\"");
|
|
||||||
assertThat(captor.getValue().isVoip()).isTrue();
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(fcmSender);
|
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -421,16 +404,12 @@ class AccountControllerTest {
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(200);
|
assertThat(response.getStatus()).isEqualTo(200);
|
||||||
|
|
||||||
ArgumentCaptor<ApnMessage> captor = ArgumentCaptor.forClass(ApnMessage.class);
|
final ArgumentCaptor<String> challengeTokenCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
verify(apnSender, times(1)).sendMessage(captor.capture());
|
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
eq("mytoken"), eq(PushNotification.TokenType.APN), challengeTokenCaptor.capture());
|
||||||
assertThat(captor.getValue().getChallengeData().isPresent()).isTrue();
|
|
||||||
assertThat(captor.getValue().getChallengeData().get().length()).isEqualTo(32);
|
|
||||||
assertThat(captor.getValue().getMessage()).contains("\"challenge\" : \"" + captor.getValue().getChallengeData().get() + "\"");
|
|
||||||
assertThat(captor.getValue().isVoip()).isFalse();
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(fcmSender);
|
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -443,8 +422,7 @@ class AccountControllerTest {
|
||||||
assertThat(response.getStatus()).isEqualTo(400);
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
assertThat(response.readEntity(String.class)).isBlank();
|
assertThat(response.readEntity(String.class)).isBlank();
|
||||||
|
|
||||||
verifyNoMoreInteractions(fcmSender);
|
verifyNoMoreInteractions(pushNotificationManager);
|
||||||
verifyNoMoreInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -462,8 +440,7 @@ class AccountControllerTest {
|
||||||
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
|
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
|
||||||
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
|
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
|
||||||
|
|
||||||
verifyNoMoreInteractions(fcmSender);
|
verifyNoMoreInteractions(pushNotificationManager);
|
||||||
verifyNoMoreInteractions(apnSender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
|
|
|
@ -1,415 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2013-2020 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.tests.push;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.mockito.Mockito.any;
|
|
||||||
import static org.mockito.Mockito.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import com.eatthepath.pushy.apns.ApnsClient;
|
|
||||||
import com.eatthepath.pushy.apns.ApnsPushNotification;
|
|
||||||
import com.eatthepath.pushy.apns.DeliveryPriority;
|
|
||||||
import com.eatthepath.pushy.apns.PushNotificationResponse;
|
|
||||||
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
|
|
||||||
import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
|
||||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
|
||||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
|
||||||
import org.whispersystems.textsecuregcm.push.RetryingApnsClient;
|
|
||||||
import org.whispersystems.textsecuregcm.push.RetryingApnsClient.ApnResult;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
|
||||||
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
|
|
||||||
|
|
||||||
class APNSenderTest {
|
|
||||||
|
|
||||||
private static final UUID DESTINATION_UUID = UUID.randomUUID();
|
|
||||||
private static final String DESTINATION_APN_ID = "foo";
|
|
||||||
|
|
||||||
private final AccountsManager accountsManager = mock(AccountsManager.class);
|
|
||||||
|
|
||||||
private final Account destinationAccount = mock(Account.class);
|
|
||||||
private final Device destinationDevice = mock(Device.class);
|
|
||||||
private final ApnFallbackManager fallbackManager = mock(ApnFallbackManager.class);
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setup() {
|
|
||||||
when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
|
|
||||||
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
|
|
||||||
when(accountsManager.getByAccountIdentifier(DESTINATION_UUID)).thenReturn(Optional.of(destinationAccount));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSendVoip() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(true);
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, true, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
ApnResult apnResult = sendFuture.get();
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_VOIP_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
assertThat(notification.getValue().getTopic()).isEqualTo("foo.voip");
|
|
||||||
|
|
||||||
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.SUCCESS);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSendApns() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(true);
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, false, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
ApnResult apnResult = sendFuture.get();
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NSE_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
assertThat(notification.getValue().getTopic()).isEqualTo("foo");
|
|
||||||
|
|
||||||
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.SUCCESS);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testUnregisteredUser() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(false);
|
|
||||||
when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered"));
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
|
||||||
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, true, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
|
|
||||||
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
|
|
||||||
when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
ApnResult apnResult = sendFuture.get();
|
|
||||||
|
|
||||||
Thread.sleep(1000); // =(
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_VOIP_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
|
|
||||||
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verify(accountsManager, times(1)).getByAccountIdentifier(eq(DESTINATION_UUID));
|
|
||||||
verify(destinationAccount, times(1)).getDevice(1);
|
|
||||||
verify(destinationDevice, times(1)).getApnId();
|
|
||||||
verify(destinationDevice, times(1)).getPushTimestamp();
|
|
||||||
// verify(destinationDevice, times(1)).setApnId(eq((String)null));
|
|
||||||
// verify(destinationDevice, times(1)).setVoipApnId(eq((String)null));
|
|
||||||
// verify(destinationDevice, times(1)).setFetchesMessages(eq(false));
|
|
||||||
// verify(accountsManager, times(1)).update(eq(destinationAccount));
|
|
||||||
verify(fallbackManager, times(1)).cancel(eq(destinationAccount), eq(destinationDevice));
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Test
|
|
||||||
// public void testVoipUnregisteredUser() throws Exception {
|
|
||||||
// ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
//
|
|
||||||
// PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
// when(response.isAccepted()).thenReturn(false);
|
|
||||||
// when(response.getRejectionReason()).thenReturn("Unregistered");
|
|
||||||
//
|
|
||||||
// DefaultPromise<PushNotificationResponse<SimpleApnsPushNotification>> result = new DefaultPromise<>(executor);
|
|
||||||
// result.setSuccess(response);
|
|
||||||
//
|
|
||||||
// when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
// .thenReturn(result);
|
|
||||||
//
|
|
||||||
// RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient, 10);
|
|
||||||
// ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30);
|
|
||||||
// APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
// apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
//
|
|
||||||
// when(destinationDevice.getApnId()).thenReturn("baz");
|
|
||||||
// when(destinationDevice.getVoipApnId()).thenReturn(DESTINATION_APN_ID);
|
|
||||||
// when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
|
|
||||||
//
|
|
||||||
// ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
// ApnResult apnResult = sendFuture.get();
|
|
||||||
//
|
|
||||||
// Thread.sleep(1000); // =(
|
|
||||||
//
|
|
||||||
// ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
// verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
//
|
|
||||||
// assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
// assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(30));
|
|
||||||
// assertThat(notification.getValue().getPayload()).isEqualTo("message");
|
|
||||||
// assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
//
|
|
||||||
// assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
|
|
||||||
//
|
|
||||||
// verifyNoMoreInteractions(apnsClient);
|
|
||||||
// verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER));
|
|
||||||
// verify(destinationAccount, times(1)).getDevice(1);
|
|
||||||
// verify(destinationDevice, times(1)).getApnId();
|
|
||||||
// verify(destinationDevice, times(1)).getVoipApnId();
|
|
||||||
// verify(destinationDevice, times(1)).getPushTimestamp();
|
|
||||||
// verify(destinationDevice, times(1)).setApnId(eq((String)null));
|
|
||||||
// verify(destinationDevice, times(1)).setVoipApnId(eq((String)null));
|
|
||||||
// verify(destinationDevice, times(1)).setFetchesMessages(eq(false));
|
|
||||||
// verify(accountsManager, times(1)).update(eq(destinationAccount));
|
|
||||||
// verify(fallbackManager, times(1)).cancel(eq(new WebsocketAddress(DESTINATION_NUMBER, 1)));
|
|
||||||
//
|
|
||||||
// verifyNoMoreInteractions(accountsManager);
|
|
||||||
// verifyNoMoreInteractions(fallbackManager);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testRecentUnregisteredUser() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(false);
|
|
||||||
when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered"));
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, true, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
|
|
||||||
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
|
|
||||||
when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis());
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
ApnResult apnResult = sendFuture.get();
|
|
||||||
|
|
||||||
Thread.sleep(1000); // =(
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_VOIP_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
|
|
||||||
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verify(accountsManager, times(1)).getByAccountIdentifier(eq(DESTINATION_UUID));
|
|
||||||
verify(destinationAccount, times(1)).getDevice(1);
|
|
||||||
verify(destinationDevice, times(1)).getApnId();
|
|
||||||
verify(destinationDevice, times(1)).getPushTimestamp();
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(destinationDevice);
|
|
||||||
verifyNoMoreInteractions(destinationAccount);
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Test
|
|
||||||
// public void testUnregisteredUserOldApnId() throws Exception {
|
|
||||||
// ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
//
|
|
||||||
// PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
// when(response.isAccepted()).thenReturn(false);
|
|
||||||
// when(response.getRejectionReason()).thenReturn("Unregistered");
|
|
||||||
//
|
|
||||||
// DefaultPromise<PushNotificationResponse<SimpleApnsPushNotification>> result = new DefaultPromise<>(executor);
|
|
||||||
// result.setSuccess(response);
|
|
||||||
//
|
|
||||||
// when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
// .thenReturn(result);
|
|
||||||
//
|
|
||||||
// RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient, 10);
|
|
||||||
// ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30);
|
|
||||||
// APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
// apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
//
|
|
||||||
// when(destinationDevice.getApnId()).thenReturn("baz");
|
|
||||||
// when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(12));
|
|
||||||
//
|
|
||||||
// ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
// ApnResult apnResult = sendFuture.get();
|
|
||||||
//
|
|
||||||
// Thread.sleep(1000); // =(
|
|
||||||
//
|
|
||||||
// ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
// verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
//
|
|
||||||
// assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
// assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(30));
|
|
||||||
// assertThat(notification.getValue().getPayload()).isEqualTo("message");
|
|
||||||
// assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
//
|
|
||||||
// assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER);
|
|
||||||
//
|
|
||||||
// verifyNoMoreInteractions(apnsClient);
|
|
||||||
// verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER));
|
|
||||||
// verify(destinationAccount, times(1)).getDevice(1);
|
|
||||||
// verify(destinationDevice, times(2)).getApnId();
|
|
||||||
// verify(destinationDevice, times(2)).getVoipApnId();
|
|
||||||
//
|
|
||||||
// verifyNoMoreInteractions(destinationDevice);
|
|
||||||
// verifyNoMoreInteractions(destinationAccount);
|
|
||||||
// verifyNoMoreInteractions(accountsManager);
|
|
||||||
// verifyNoMoreInteractions(fallbackManager);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testGenericFailure() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(false);
|
|
||||||
when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic"));
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, true, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
ApnResult apnResult = sendFuture.get();
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_VOIP_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
|
|
||||||
assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.GENERIC_FAILURE);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFailure() throws Exception {
|
|
||||||
ApnsClient apnsClient = mock(ApnsClient.class);
|
|
||||||
|
|
||||||
PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
|
|
||||||
when(response.isAccepted()).thenReturn(true);
|
|
||||||
|
|
||||||
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
|
|
||||||
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), new Exception("lost connection")));
|
|
||||||
|
|
||||||
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
|
|
||||||
ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_UUID, 1, true, Type.NOTIFICATION, Optional.empty());
|
|
||||||
APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false);
|
|
||||||
apnSender.setApnFallbackManager(fallbackManager);
|
|
||||||
|
|
||||||
ListenableFuture<ApnResult> sendFuture = apnSender.sendMessage(message);
|
|
||||||
|
|
||||||
try {
|
|
||||||
sendFuture.get();
|
|
||||||
throw new AssertionError();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
// good
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
|
|
||||||
verify(apnsClient, times(1)).sendNotification(notification.capture());
|
|
||||||
|
|
||||||
assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID);
|
|
||||||
assertThat(notification.getValue().getExpiration()).isEqualTo(Instant.ofEpochMilli(ApnMessage.MAX_EXPIRATION));
|
|
||||||
assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_VOIP_NOTIFICATION_PAYLOAD);
|
|
||||||
assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
|
|
||||||
|
|
||||||
verifyNoMoreInteractions(apnsClient);
|
|
||||||
verifyNoMoreInteractions(accountsManager);
|
|
||||||
verifyNoMoreInteractions(fallbackManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class MockPushNotificationFuture <P extends ApnsPushNotification, V> extends PushNotificationFuture<P, V> {
|
|
||||||
|
|
||||||
MockPushNotificationFuture(final P pushNotification, final V response) {
|
|
||||||
super(pushNotification);
|
|
||||||
complete(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
MockPushNotificationFuture(final P pushNotification, final Exception exception) {
|
|
||||||
super(pushNotification);
|
|
||||||
completeExceptionally(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -54,7 +54,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
@ -104,7 +104,7 @@ class WebSocketConnectionTest {
|
||||||
MessagesManager storedMessages = mock(MessagesManager.class);
|
MessagesManager storedMessages = mock(MessagesManager.class);
|
||||||
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
|
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
|
||||||
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, storedMessages,
|
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, storedMessages,
|
||||||
mock(MessageSender.class), apnFallbackManager, mock(ClientPresenceManager.class),
|
mock(PushNotificationManager.class), apnFallbackManager, mock(ClientPresenceManager.class),
|
||||||
retrySchedulingExecutor);
|
retrySchedulingExecutor);
|
||||||
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
|
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue