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.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
||||
|
@ -446,8 +447,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||
DispatchManager dispatchManager = new DispatchManager(pubSubClientFactory, Optional.empty());
|
||||
PubSubManager pubSubManager = new PubSubManager(pubsubClient, dispatchManager);
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, accountsManager, config.getApnConfiguration());
|
||||
FcmSender fcmSender = new FcmSender(gcmSenderExecutor, accountsManager, config.getFcmConfiguration().credentials());
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
|
||||
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);
|
||||
DynamicRateLimiters dynamicRateLimiters = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
|
@ -470,17 +473,16 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
||||
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);
|
||||
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager);
|
||||
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
||||
config.getRecaptchaConfiguration().getProjectPath(),
|
||||
config.getRecaptchaConfiguration().getCredentialConfigurationJson(),
|
||||
dynamicConfigurationManager);
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, fcmSender, pushChallengeDynamoDb);
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager, pushChallengeDynamoDb);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||
recaptchaClient, dynamicRateLimiters);
|
||||
RateLimitChallengeOptionManager rateLimitChallengeOptionManager =
|
||||
|
@ -548,10 +550,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
FtxClient ftxClient = new FtxClient(currencyClient);
|
||||
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, ftxClient, config.getPaymentsServiceConfiguration().getPaymentCurrencies());
|
||||
|
||||
apnSender.setApnFallbackManager(apnFallbackManager);
|
||||
environment.lifecycle().manage(apnSender);
|
||||
environment.lifecycle().manage(apnFallbackManager);
|
||||
environment.lifecycle().manage(pubSubManager);
|
||||
environment.lifecycle().manage(messageSender);
|
||||
environment.lifecycle().manage(accountDatabaseCrawler);
|
||||
environment.lifecycle().manage(directoryReconciliationAccountDatabaseCrawler);
|
||||
environment.lifecycle().manage(accountCleanerAccountDatabaseCrawler);
|
||||
|
@ -607,7 +608,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
config.getWebSocketConfiguration(), 90000);
|
||||
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||
webSocketEnvironment.setConnectListener(
|
||||
new AuthenticatedConnectListener(receiptSender, messagesManager, messageSender, apnFallbackManager,
|
||||
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager, apnFallbackManager,
|
||||
clientPresenceManager, websocketScheduledExecutor));
|
||||
webSocketEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||
webSocketEnvironment.jersey().register(new ContentLengthFilter(TrafficSource.WEBSOCKET));
|
||||
|
@ -619,7 +620,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
environment.jersey().register(
|
||||
new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters,
|
||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
recaptchaClient, fcmSender, apnSender, verifyExperimentEnrollmentManager,
|
||||
recaptchaClient, pushNotificationManager, verifyExperimentEnrollmentManager,
|
||||
changeNumberManager, backupCredentialsGenerator));
|
||||
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.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
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.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||
|
@ -137,8 +136,7 @@ public class AccountController {
|
|||
private final TurnTokenGenerator turnTokenGenerator;
|
||||
private final Map<String, Integer> testDevices;
|
||||
private final RecaptchaClient recaptchaClient;
|
||||
private final FcmSender fcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator;
|
||||
|
||||
private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager;
|
||||
|
@ -153,8 +151,7 @@ public class AccountController {
|
|||
TurnTokenGenerator turnTokenGenerator,
|
||||
Map<String, Integer> testDevices,
|
||||
RecaptchaClient recaptchaClient,
|
||||
FcmSender fcmSender,
|
||||
APNSender apnSender,
|
||||
PushNotificationManager pushNotificationManager,
|
||||
TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager,
|
||||
ChangeNumberManager changeNumberManager,
|
||||
ExternalServiceCredentialGenerator backupServiceCredentialGenerator)
|
||||
|
@ -168,8 +165,7 @@ public class AccountController {
|
|||
this.testDevices = testDevices;
|
||||
this.turnTokenGenerator = turnTokenGenerator;
|
||||
this.recaptchaClient = recaptchaClient;
|
||||
this.fcmSender = fcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager;
|
||||
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
|
||||
this.changeNumberManager = changeNumberManager;
|
||||
|
@ -179,15 +175,17 @@ public class AccountController {
|
|||
@GET
|
||||
@Path("/{type}/preauth/{token}/{number}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getPreAuth(@PathParam("type") String pushType,
|
||||
@PathParam("token") String pushToken,
|
||||
public Response getPreAuth(@PathParam("type") String pushType,
|
||||
@PathParam("token") String pushToken,
|
||||
@PathParam("number") String number,
|
||||
@QueryParam("voip") Optional<Boolean> useVoip)
|
||||
@QueryParam("voip") @DefaultValue("true") boolean useVoip)
|
||||
throws ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
|
||||
if (!"apn".equals(pushType) && !"fcm".equals(pushType)) {
|
||||
return Response.status(400).build();
|
||||
}
|
||||
final PushNotification.TokenType tokenType = switch(pushType) {
|
||||
case "apn" -> useVoip ? PushNotification.TokenType.APN_VOIP : PushNotification.TokenType.APN;
|
||||
case "fcm" -> PushNotification.TokenType.FCM;
|
||||
default -> throw new BadRequestException();
|
||||
};
|
||||
|
||||
Util.requireNormalizedNumber(number);
|
||||
|
||||
|
@ -198,14 +196,7 @@ public class AccountController {
|
|||
null);
|
||||
|
||||
pendingAccounts.store(number, storedVerificationCode);
|
||||
|
||||
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();
|
||||
}
|
||||
pushNotificationManager.sendRegistrationChallengeNotification(pushToken, tokenType, storedVerificationCode.getPushCode());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
|
|
@ -10,16 +10,11 @@ import static com.codahale.metrics.MetricRegistry.name;
|
|||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.codec.DecoderException;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
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.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
|
||||
|
@ -27,9 +22,7 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
|
||||
public class PushChallengeManager {
|
||||
private final APNSender apnSender;
|
||||
private final FcmSender fcmSender;
|
||||
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
private final PushChallengeDynamoDb pushChallengeDynamoDb;
|
||||
|
||||
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 SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||
|
||||
public PushChallengeManager(final APNSender apnSender, final FcmSender fcmSender,
|
||||
public PushChallengeManager(final PushNotificationManager pushNotificationManager,
|
||||
final PushChallengeDynamoDb pushChallengeDynamoDb) {
|
||||
|
||||
this.apnSender = apnSender;
|
||||
this.fcmSender = fcmSender;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.pushChallengeDynamoDb = pushChallengeDynamoDb;
|
||||
}
|
||||
|
||||
public void sendChallenge(final Account account) throws NotPushRegisteredException {
|
||||
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];
|
||||
random.nextBytes(token);
|
||||
|
||||
|
@ -67,17 +55,18 @@ public class PushChallengeManager {
|
|||
final String platform;
|
||||
|
||||
if (pushChallengeDynamoDb.add(account.getUuid(), token, CHALLENGE_TTL)) {
|
||||
final String tokenHex = Hex.encodeHexString(token);
|
||||
pushNotificationManager.sendRateLimitChallengeNotification(account, Hex.encodeHexString(token));
|
||||
|
||||
sent = true;
|
||||
|
||||
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();
|
||||
} 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();
|
||||
} 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 {
|
||||
sent = false;
|
||||
|
|
|
@ -4,57 +4,47 @@
|
|||
*/
|
||||
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.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.slf4j.Logger;
|
||||
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 io.dropwizard.lifecycle.Managed;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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;
|
||||
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;
|
||||
public class APNSender implements Managed, PushNotificationSender {
|
||||
|
||||
private final ExecutorService executor;
|
||||
private final AccountsManager accountsManager;
|
||||
private final String bundleId;
|
||||
private final boolean sandbox;
|
||||
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
|
||||
{
|
||||
this.executor = executor;
|
||||
this.accountsManager = accountsManager;
|
||||
this.bundleId = configuration.getBundleId();
|
||||
this.sandbox = configuration.isSandboxEnabled();
|
||||
this.apnsClient = new RetryingApnsClient(configuration.getSigningKey(),
|
||||
|
@ -64,50 +54,64 @@ public class APNSender implements Managed {
|
|||
}
|
||||
|
||||
@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.accountsManager = accountsManager;
|
||||
this.apnsClient = apnsClient;
|
||||
this.sandbox = sandbox;
|
||||
this.bundleId = bundleId;
|
||||
}
|
||||
|
||||
public ListenableFuture<ApnResult> sendMessage(final ApnMessage message) {
|
||||
String topic = bundleId;
|
||||
@Override
|
||||
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()) {
|
||||
topic = topic + ".voip";
|
||||
}
|
||||
|
||||
ListenableFuture<ApnResult> future = apnsClient.send(message.getApnId(), topic,
|
||||
message.getMessage(),
|
||||
Instant.ofEpochMilli(message.getExpirationTime()),
|
||||
message.isVoip(),
|
||||
message.getCollapseId());
|
||||
final boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP;
|
||||
|
||||
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
|
||||
public void onSuccess(@Nullable ApnResult result) {
|
||||
if (message.getChallengeData().isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
logger.warn("*** RECEIVED NULL APN RESULT ***");
|
||||
} else if (result.getStatus() == ApnResult.Status.NO_SUCH_USER) {
|
||||
message.getUuid().ifPresent(uuid -> handleUnregisteredUser(message.getApnId(), uuid, message.getDeviceId()));
|
||||
} else if (result.getStatus() == ApnResult.Status.GENERIC_FAILURE) {
|
||||
logger.warn("*** Got APN generic failure: " + result.getReason() + ", " + message.getUuid());
|
||||
// This should never happen
|
||||
completableSendFuture.completeExceptionally(new NullPointerException("apnResult was null"));
|
||||
} else {
|
||||
completableSendFuture.complete(switch (result.getStatus()) {
|
||||
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
|
||||
public void onFailure(@Nullable Throwable t) {
|
||||
logger.warn("Got fatal APNS exception", t);
|
||||
completableSendFuture.completeExceptionally(t);
|
||||
}
|
||||
}, executor);
|
||||
|
||||
return future;
|
||||
return completableSendFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,66 +122,4 @@ public class APNSender implements Managed {
|
|||
public void stop() {
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
||||
import org.whispersystems.textsecuregcm.redis.ClusterLuaScript;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
@ -184,7 +183,7 @@ public class ApnFallbackManager implements Managed {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.api.core.ApiFuture;
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
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.FirebaseMessagingException;
|
||||
import com.google.firebase.messaging.Message;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import com.google.firebase.messaging.MessagingErrorCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
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 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))) {
|
||||
FirebaseOptions options = FirebaseOptions.builder()
|
||||
.setCredentials(GoogleCredentials.fromStream(credentialInputStream))
|
||||
|
@ -52,102 +41,62 @@ public class FcmSender {
|
|||
}
|
||||
|
||||
this.executor = executor;
|
||||
this.accountsManager = accountsManager;
|
||||
this.firebaseMessagingClient = FirebaseMessaging.getInstance();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public FcmSender(ExecutorService executor, AccountsManager accountsManager, FirebaseMessaging firebaseMessagingClient) {
|
||||
this.accountsManager = accountsManager;
|
||||
public FcmSender(ExecutorService executor, FirebaseMessaging firebaseMessagingClient) {
|
||||
this.executor = executor;
|
||||
this.firebaseMessagingClient = firebaseMessagingClient;
|
||||
}
|
||||
|
||||
public void sendMessage(GcmMessage message) {
|
||||
@Override
|
||||
public CompletableFuture<SendPushNotificationResult> sendNotification(PushNotification pushNotification) {
|
||||
Message.Builder builder = Message.builder()
|
||||
.setToken(message.getGcmId())
|
||||
.setToken(pushNotification.deviceToken())
|
||||
.setAndroidConfig(AndroidConfig.builder()
|
||||
.setPriority(AndroidConfig.Priority.HIGH)
|
||||
.build());
|
||||
|
||||
final String key = switch (message.getType()) {
|
||||
final String key = switch (pushNotification.notificationType()) {
|
||||
case NOTIFICATION -> "notification";
|
||||
case CHALLENGE -> "challenge";
|
||||
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 CompletableFuture<SendPushNotificationResult> completableSendFuture = new CompletableFuture<>();
|
||||
|
||||
sendFuture.addListener(() -> {
|
||||
Tags tags = Tags.of("type", key);
|
||||
|
||||
try {
|
||||
sendFuture.get();
|
||||
completableSendFuture.complete(new SendPushNotificationResult(true, null, false));
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof final FirebaseMessagingException firebaseMessagingException) {
|
||||
final String errorCode;
|
||||
|
||||
if (firebaseMessagingException.getMessagingErrorCode() != null) {
|
||||
errorCode = firebaseMessagingException.getMessagingErrorCode().name().toLowerCase();
|
||||
errorCode = firebaseMessagingException.getMessagingErrorCode().name();
|
||||
} else {
|
||||
logger.warn("Received an FCM exception with no error code", firebaseMessagingException);
|
||||
errorCode = "unknown";
|
||||
}
|
||||
|
||||
tags = tags.and("errorCode", errorCode);
|
||||
|
||||
switch (firebaseMessagingException.getMessagingErrorCode()) {
|
||||
|
||||
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());
|
||||
}
|
||||
completableSendFuture.complete(new SendPushNotificationResult(false,
|
||||
errorCode,
|
||||
firebaseMessagingException.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED));
|
||||
} else {
|
||||
throw new RuntimeException("Failed to send message", e);
|
||||
completableSendFuture.completeExceptionally(e.getCause());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// This should never happen; by definition, if we're in the future's listener, the future is done, and so
|
||||
// `get()` should return immediately.
|
||||
throw new IllegalStateException("Interrupted while getting send future result", e);
|
||||
} finally {
|
||||
Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment();
|
||||
completableSendFuture.completeExceptionally(e);
|
||||
}
|
||||
}, executor);
|
||||
}
|
||||
|
||||
private void handleBadRegistration(GcmMessage message) {
|
||||
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();
|
||||
return completableSendFuture;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.metrics.PushLatencyManager;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage.Type;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
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,
|
||||
|
@ -33,41 +30,30 @@ import org.whispersystems.textsecuregcm.util.Util;
|
|||
* @see org.whispersystems.textsecuregcm.storage.MessageAvailabilityListener
|
||||
* @see ReceiptSender
|
||||
*/
|
||||
public class MessageSender implements Managed {
|
||||
public class MessageSender {
|
||||
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final FcmSender fcmSender;
|
||||
private final APNSender apnSender;
|
||||
private final PushLatencyManager pushLatencyManager;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
private final PushLatencyManager pushLatencyManager;
|
||||
|
||||
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
||||
private static final String CHANNEL_TAG_NAME = "channel";
|
||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
||||
private static final String CHANNEL_TAG_NAME = "channel";
|
||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||
private static final String CLIENT_ONLINE_TAG_NAME = "clientOnline";
|
||||
|
||||
public MessageSender(ApnFallbackManager apnFallbackManager,
|
||||
ClientPresenceManager clientPresenceManager,
|
||||
MessagesManager messagesManager,
|
||||
FcmSender fcmSender,
|
||||
APNSender apnSender,
|
||||
PushLatencyManager pushLatencyManager)
|
||||
{
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
public MessageSender(ClientPresenceManager clientPresenceManager,
|
||||
MessagesManager messagesManager,
|
||||
PushNotificationManager pushNotificationManager,
|
||||
PushLatencyManager pushLatencyManager) {
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.fcmSender = fcmSender;
|
||||
this.apnSender = apnSender;
|
||||
this.pushLatencyManager = pushLatencyManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.pushLatencyManager = pushLatencyManager;
|
||||
}
|
||||
|
||||
public void sendMessage(final Account account, final Device device, final Envelope message, boolean online)
|
||||
throws NotPushRegisteredException
|
||||
{
|
||||
if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) {
|
||||
throw new NotPushRegisteredException("No delivery possible!");
|
||||
}
|
||||
throws NotPushRegisteredException {
|
||||
|
||||
final String channel;
|
||||
|
||||
|
@ -98,59 +84,24 @@ public class MessageSender implements Managed {
|
|||
clientPresent = clientPresenceManager.isPresent(account.getUuid(), device.getId());
|
||||
|
||||
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(
|
||||
Tag.of(CHANNEL_TAG_NAME, channel),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent)));
|
||||
Tag.of(CHANNEL_TAG_NAME, channel),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(CLIENT_ONLINE_TAG_NAME, String.valueOf(clientPresent)));
|
||||
|
||||
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() {
|
||||
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.push.ApnFallbackManager;
|
||||
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.redis.RedisOperation;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
@ -42,20 +43,21 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
|||
|
||||
private final ReceiptSender receiptSender;
|
||||
private final MessagesManager messagesManager;
|
||||
private final MessageSender messageSender;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final ClientPresenceManager clientPresenceManager;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
public AuthenticatedConnectListener(ReceiptSender receiptSender,
|
||||
MessagesManager messagesManager,
|
||||
final MessageSender messageSender, ApnFallbackManager apnFallbackManager,
|
||||
PushNotificationManager pushNotificationManager,
|
||||
ApnFallbackManager apnFallbackManager,
|
||||
ClientPresenceManager clientPresenceManager,
|
||||
ScheduledExecutorService scheduledExecutorService)
|
||||
{
|
||||
this.receiptSender = receiptSender;
|
||||
this.messagesManager = messagesManager;
|
||||
this.messageSender = messageSender;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.clientPresenceManager = clientPresenceManager;
|
||||
this.scheduledExecutorService = scheduledExecutorService;
|
||||
|
@ -97,7 +99,10 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
|||
messagesManager.removeMessageAvailabilityListener(connection);
|
||||
|
||||
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());
|
||||
|
||||
final ArgumentCaptor<ApnMessage> messageCaptor = ArgumentCaptor.forClass(ApnMessage.class);
|
||||
verify(apnSender).sendMessage(messageCaptor.capture());
|
||||
final ArgumentCaptor<PushNotification> notificationCaptor = ArgumentCaptor.forClass(PushNotification.class);
|
||||
verify(apnSender).sendNotification(notificationCaptor.capture());
|
||||
|
||||
final ApnMessage message = messageCaptor.getValue();
|
||||
final PushNotification pushNotification = notificationCaptor.getValue();
|
||||
|
||||
assertEquals(VOIP_APN_ID, message.getApnId());
|
||||
assertEquals(Optional.of(ACCOUNT_UUID), message.getUuid());
|
||||
assertEquals(DEVICE_ID, message.getDeviceId());
|
||||
assertEquals(VOIP_APN_ID, pushNotification.deviceToken());
|
||||
assertEquals(account, pushNotification.destination());
|
||||
assertEquals(device, pushNotification.destinationDevice());
|
||||
|
||||
assertEquals(0, worker.processNextSlot());
|
||||
}
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
|
||||
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.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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.Message;
|
||||
import com.google.firebase.messaging.MessagingErrorCode;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
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.tests.util.SynchronousExecutorService;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
class FcmSenderTest {
|
||||
|
||||
private ExecutorService executorService;
|
||||
private AccountsManager accountsManager;
|
||||
private FirebaseMessaging firebaseMessaging;
|
||||
|
||||
private FcmSender fcmSender;
|
||||
|
@ -41,10 +39,9 @@ class FcmSenderTest {
|
|||
@BeforeEach
|
||||
void setUp() {
|
||||
executorService = new SynchronousExecutorService();
|
||||
accountsManager = mock(AccountsManager.class);
|
||||
firebaseMessaging = mock(FirebaseMessaging.class);
|
||||
|
||||
fcmSender = new FcmSender(executorService, accountsManager, firebaseMessaging);
|
||||
fcmSender = new FcmSender(executorService, firebaseMessaging);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
@ -57,35 +54,44 @@ class FcmSenderTest {
|
|||
|
||||
@Test
|
||||
void testSendMessage() {
|
||||
AccountsHelper.setupMockUpdate(accountsManager);
|
||||
|
||||
final GcmMessage message = new GcmMessage("foo", UUID.randomUUID(), 1, GcmMessage.Type.NOTIFICATION, Optional.empty());
|
||||
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||
|
||||
final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
|
||||
sendFuture.set("message-id");
|
||||
|
||||
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||
|
||||
fcmSender.sendMessage(message);
|
||||
final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join();
|
||||
|
||||
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||
assertTrue(result.accepted());
|
||||
assertNull(result.errorCode());
|
||||
assertFalse(result.unregistered());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendUninstalled() {
|
||||
final UUID destinationUuid = UUID.randomUUID();
|
||||
final String gcmId = "foo";
|
||||
void testSendMessageRejected() {
|
||||
final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null);
|
||||
|
||||
final Account destinationAccount = mock(Account.class);
|
||||
final Device destinationDevice = mock(Device.class );
|
||||
final FirebaseMessagingException invalidArgumentException = mock(FirebaseMessagingException.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(accountsManager.getByAccountIdentifier(destinationUuid)).thenReturn(Optional.of(destinationAccount));
|
||||
when(destinationDevice.getGcmId()).thenReturn(gcmId);
|
||||
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||
|
||||
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);
|
||||
when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
|
||||
|
@ -95,11 +101,27 @@ class FcmSenderTest {
|
|||
|
||||
when(firebaseMessaging.sendAsync(any())).thenReturn(sendFuture);
|
||||
|
||||
fcmSender.sendMessage(message);
|
||||
final SendPushNotificationResult result = fcmSender.sendNotification(pushNotification).join();
|
||||
|
||||
verify(firebaseMessaging).sendAsync(any(Message.class));
|
||||
verify(accountsManager).getByAccountIdentifier(destinationUuid);
|
||||
verify(accountsManager).updateDevice(eq(destinationAccount), eq(1L), any());
|
||||
verify(destinationDevice).setUninstalledFeedbackTimestamp(Util.todayInMillis());
|
||||
assertFalse(result.accepted());
|
||||
assertEquals("UNREGISTERED", result.errorCode());
|
||||
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;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -37,8 +39,7 @@ class MessageSenderTest {
|
|||
|
||||
private ClientPresenceManager clientPresenceManager;
|
||||
private MessagesManager messagesManager;
|
||||
private FcmSender fcmSender;
|
||||
private APNSender apnSender;
|
||||
private PushNotificationManager pushNotificationManager;
|
||||
private MessageSender messageSender;
|
||||
|
||||
private static final UUID ACCOUNT_UUID = UUID.randomUUID();
|
||||
|
@ -53,13 +54,10 @@ class MessageSenderTest {
|
|||
|
||||
clientPresenceManager = mock(ClientPresenceManager.class);
|
||||
messagesManager = mock(MessagesManager.class);
|
||||
fcmSender = mock(FcmSender.class);
|
||||
apnSender = mock(APNSender.class);
|
||||
messageSender = new MessageSender(mock(ApnFallbackManager.class),
|
||||
clientPresenceManager,
|
||||
pushNotificationManager = mock(PushNotificationManager.class);
|
||||
messageSender = new MessageSender(clientPresenceManager,
|
||||
messagesManager,
|
||||
fcmSender,
|
||||
apnSender,
|
||||
pushNotificationManager,
|
||||
mock(PushLatencyManager.class));
|
||||
|
||||
when(account.getUuid()).thenReturn(ACCOUNT_UUID);
|
||||
|
@ -80,8 +78,7 @@ class MessageSenderTest {
|
|||
|
||||
assertTrue(envelopeArgumentCaptor.getValue().getEphemeral());
|
||||
|
||||
verifyNoInteractions(fcmSender);
|
||||
verifyNoInteractions(apnSender);
|
||||
verifyNoInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -92,8 +89,7 @@ class MessageSenderTest {
|
|||
messageSender.sendMessage(account, device, message, true);
|
||||
|
||||
verify(messagesManager, never()).insert(any(), anyLong(), any());
|
||||
verifyNoInteractions(fcmSender);
|
||||
verifyNoInteractions(apnSender);
|
||||
verifyNoInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -110,8 +106,7 @@ class MessageSenderTest {
|
|||
|
||||
assertFalse(envelopeArgumentCaptor.getValue().getEphemeral());
|
||||
assertEquals(message, envelopeArgumentCaptor.getValue());
|
||||
verifyNoInteractions(fcmSender);
|
||||
verifyNoInteractions(apnSender);
|
||||
verifyNoInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -122,8 +117,7 @@ class MessageSenderTest {
|
|||
messageSender.sendMessage(account, device, message, false);
|
||||
|
||||
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
||||
verify(fcmSender).sendMessage(any());
|
||||
verifyNoInteractions(apnSender);
|
||||
verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -134,8 +128,7 @@ class MessageSenderTest {
|
|||
messageSender.sendMessage(account, device, message, false);
|
||||
|
||||
verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
|
||||
verifyNoInteractions(fcmSender);
|
||||
verify(apnSender).sendMessage(any());
|
||||
verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -143,11 +136,11 @@ class MessageSenderTest {
|
|||
when(clientPresenceManager.isPresent(ACCOUNT_UUID, DEVICE_ID)).thenReturn(false);
|
||||
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);
|
||||
verifyNoInteractions(fcmSender);
|
||||
verifyNoInteractions(apnSender);
|
||||
}
|
||||
|
||||
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.NonNormalizedPhoneNumberResponse;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||
import org.whispersystems.textsecuregcm.push.ApnMessage;
|
||||
import org.whispersystems.textsecuregcm.push.FcmSender;
|
||||
import org.whispersystems.textsecuregcm.push.GcmMessage;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient;
|
||||
import org.whispersystems.textsecuregcm.sms.SmsSender;
|
||||
import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager;
|
||||
|
@ -147,8 +145,7 @@ class AccountControllerTest {
|
|||
private static Account senderHasStorage = mock(Account.class);
|
||||
private static Account senderTransfer = mock(Account.class);
|
||||
private static RecaptchaClient recaptchaClient = mock(RecaptchaClient.class);
|
||||
private static FcmSender fcmSender = mock(FcmSender.class);
|
||||
private static APNSender apnSender = mock(APNSender.class);
|
||||
private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
|
||||
private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
|
||||
|
||||
private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||
|
@ -179,8 +176,7 @@ class AccountControllerTest {
|
|||
turnTokenGenerator,
|
||||
Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE),
|
||||
recaptchaClient,
|
||||
fcmSender,
|
||||
apnSender,
|
||||
pushNotificationManager,
|
||||
verifyExperimentEnrollmentManager,
|
||||
changeNumberManager,
|
||||
storageCredentialGenerator))
|
||||
|
@ -322,8 +318,7 @@ class AccountControllerTest {
|
|||
senderHasStorage,
|
||||
senderTransfer,
|
||||
recaptchaClient,
|
||||
fcmSender,
|
||||
apnSender,
|
||||
pushNotificationManager,
|
||||
verifyExperimentEnrollmentManager,
|
||||
changeNumberManager);
|
||||
|
||||
|
@ -339,14 +334,12 @@ class AccountControllerTest {
|
|||
|
||||
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());
|
||||
assertThat(captor.getValue().getGcmId()).isEqualTo("mytoken");
|
||||
assertThat(captor.getValue().getData().isPresent()).isTrue();
|
||||
assertThat(captor.getValue().getData().get().length()).isEqualTo(32);
|
||||
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||
eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture());
|
||||
|
||||
verifyNoMoreInteractions(apnSender);
|
||||
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -358,14 +351,12 @@ class AccountControllerTest {
|
|||
|
||||
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());
|
||||
assertThat(captor.getValue().getGcmId()).isEqualTo("mytoken");
|
||||
assertThat(captor.getValue().getData().isPresent()).isTrue();
|
||||
assertThat(captor.getValue().getData().get().length()).isEqualTo(32);
|
||||
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||
eq("mytoken"), eq(PushNotification.TokenType.FCM), challengeTokenCaptor.capture());
|
||||
|
||||
verifyNoMoreInteractions(apnSender);
|
||||
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -377,16 +368,12 @@ class AccountControllerTest {
|
|||
|
||||
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());
|
||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
||||
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();
|
||||
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||
eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture());
|
||||
|
||||
verifyNoMoreInteractions(fcmSender);
|
||||
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -399,16 +386,12 @@ class AccountControllerTest {
|
|||
|
||||
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());
|
||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
||||
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();
|
||||
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||
eq("mytoken"), eq(PushNotification.TokenType.APN_VOIP), challengeTokenCaptor.capture());
|
||||
|
||||
verifyNoMoreInteractions(fcmSender);
|
||||
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -421,16 +404,12 @@ class AccountControllerTest {
|
|||
|
||||
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());
|
||||
assertThat(captor.getValue().getApnId()).isEqualTo("mytoken");
|
||||
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();
|
||||
verify(pushNotificationManager).sendRegistrationChallengeNotification(
|
||||
eq("mytoken"), eq(PushNotification.TokenType.APN), challengeTokenCaptor.capture());
|
||||
|
||||
verifyNoMoreInteractions(fcmSender);
|
||||
assertThat(challengeTokenCaptor.getValue().length()).isEqualTo(32);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -443,8 +422,7 @@ class AccountControllerTest {
|
|||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
assertThat(response.readEntity(String.class)).isBlank();
|
||||
|
||||
verifyNoMoreInteractions(fcmSender);
|
||||
verifyNoMoreInteractions(apnSender);
|
||||
verifyNoMoreInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -462,8 +440,7 @@ class AccountControllerTest {
|
|||
assertThat(responseEntity.getOriginalNumber()).isEqualTo(number);
|
||||
assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111");
|
||||
|
||||
verifyNoMoreInteractions(fcmSender);
|
||||
verifyNoMoreInteractions(apnSender);
|
||||
verifyNoMoreInteractions(pushNotificationManager);
|
||||
}
|
||||
|
||||
@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.push.ApnFallbackManager;
|
||||
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.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
@ -104,7 +104,7 @@ class WebSocketConnectionTest {
|
|||
MessagesManager storedMessages = mock(MessagesManager.class);
|
||||
WebSocketAccountAuthenticator webSocketAuthenticator = new WebSocketAccountAuthenticator(accountAuthenticator);
|
||||
AuthenticatedConnectListener connectListener = new AuthenticatedConnectListener(receiptSender, storedMessages,
|
||||
mock(MessageSender.class), apnFallbackManager, mock(ClientPresenceManager.class),
|
||||
mock(PushNotificationManager.class), apnFallbackManager, mock(ClientPresenceManager.class),
|
||||
retrySchedulingExecutor);
|
||||
WebSocketSessionContext sessionContext = mock(WebSocketSessionContext.class);
|
||||
|
||||
|
|
Loading…
Reference in New Issue