Introduce common push notification interfaces/pathways

This commit is contained in:
Jon Chambers
2022-08-03 10:07:53 -04:00
committed by GitHub
parent 0d24828539
commit 6f0faae4ce
23 changed files with 843 additions and 1011 deletions

View File

@@ -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));

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -9,12 +9,4 @@ public class NotPushRegisteredException extends Exception {
public NotPushRegisteredException() {
super();
}
public NotPushRegisteredException(String s) {
super(s);
}
public NotPushRegisteredException(Exception e) {
super(e);
}
}

View File

@@ -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,
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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) {
}

View File

@@ -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) {
}
}
});
}