mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 04:58:07 +01:00
Send non-urgent push notifications with lower priority
This commit is contained in:
@@ -455,7 +455,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
|
||||
FcmSender fcmSender = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials());
|
||||
ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler(pushSchedulerCluster, apnSender, accountsManager);
|
||||
PushNotificationManager pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnPushNotificationScheduler, pushLatencyManager);
|
||||
PushNotificationManager pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnPushNotificationScheduler, pushLatencyManager, dynamicConfigurationManager);
|
||||
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
|
||||
DynamicRateLimiters dynamicRateLimiters = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
|
||||
ProvisioningManager provisioningManager = new ProvisioningManager(pubSubManager);
|
||||
|
||||
@@ -64,6 +64,10 @@ public class DynamicConfiguration {
|
||||
@Valid
|
||||
DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
DynamicPushNotificationConfiguration pushNotifications = new DynamicPushNotificationConfiguration();
|
||||
|
||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||
final String experimentName) {
|
||||
return Optional.ofNullable(experiments.get(experimentName));
|
||||
@@ -126,4 +130,8 @@ public class DynamicConfiguration {
|
||||
public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() {
|
||||
return messagePersister;
|
||||
}
|
||||
|
||||
public DynamicPushNotificationConfiguration getPushNotificationConfiguration() {
|
||||
return pushNotifications;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2013-2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DynamicPushNotificationConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean lowUrgencyEnabled = false;
|
||||
|
||||
public boolean isLowUrgencyEnabled() {
|
||||
return lowUrgencyEnabled;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ public class APNSender implements Managed, PushNotificationSender {
|
||||
.setLocalizedAlertMessage("APN_Message")
|
||||
.build();
|
||||
|
||||
@VisibleForTesting
|
||||
static final String APN_BACKGROUND_PAYLOAD = new SimpleApnsPayloadBuilder()
|
||||
.setContentAvailable(true)
|
||||
.build();
|
||||
|
||||
@VisibleForTesting
|
||||
static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L);
|
||||
|
||||
@@ -83,7 +88,13 @@ public class APNSender implements Managed, PushNotificationSender {
|
||||
final boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP;
|
||||
|
||||
final String payload = switch (notification.notificationType()) {
|
||||
case NOTIFICATION -> isVoip ? APN_VOIP_NOTIFICATION_PAYLOAD : APN_NSE_NOTIFICATION_PAYLOAD;
|
||||
case NOTIFICATION -> {
|
||||
if (isVoip) {
|
||||
yield APN_VOIP_NOTIFICATION_PAYLOAD;
|
||||
} else {
|
||||
yield notification.urgent() ? APN_NSE_NOTIFICATION_PAYLOAD : APN_BACKGROUND_PAYLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
case CHALLENGE -> new SimpleApnsPayloadBuilder()
|
||||
.setSound("default")
|
||||
@@ -98,8 +109,19 @@ public class APNSender implements Managed, PushNotificationSender {
|
||||
.build();
|
||||
};
|
||||
|
||||
final PushType pushType;
|
||||
|
||||
if (isVoip) {
|
||||
pushType = PushType.VOIP;
|
||||
} else {
|
||||
pushType = notification.urgent() ? PushType.ALERT : PushType.BACKGROUND;
|
||||
}
|
||||
|
||||
final DeliveryPriority deliveryPriority =
|
||||
(notification.urgent() || isVoip) ? DeliveryPriority.IMMEDIATE : DeliveryPriority.CONSERVE_POWER;
|
||||
|
||||
final String collapseId =
|
||||
(notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && !isVoip)
|
||||
(notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && notification.urgent() && !isVoip)
|
||||
? "incoming-message" : null;
|
||||
|
||||
final Instant start = Instant.now();
|
||||
@@ -108,8 +130,8 @@ public class APNSender implements Managed, PushNotificationSender {
|
||||
topic,
|
||||
payload,
|
||||
MAX_EXPIRATION,
|
||||
DeliveryPriority.IMMEDIATE,
|
||||
isVoip ? PushType.VOIP : PushType.ALERT,
|
||||
deliveryPriority,
|
||||
pushType,
|
||||
collapseId))
|
||||
.whenComplete((response, throwable) -> {
|
||||
// Note that we deliberately run this small bit of non-blocking measurement on the "send notification" thread
|
||||
|
||||
@@ -250,7 +250,7 @@ public class ApnPushNotificationScheduler implements Managed {
|
||||
return;
|
||||
}
|
||||
|
||||
apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device));
|
||||
apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true));
|
||||
retry.increment();
|
||||
}
|
||||
|
||||
@@ -263,8 +263,7 @@ public class ApnPushNotificationScheduler implements Managed {
|
||||
getLastBackgroundNotificationTimestampKey(account, device),
|
||||
String.valueOf(clock.millis()), new SetArgs().ex(BACKGROUND_NOTIFICATION_PERIOD)));
|
||||
|
||||
// TODO Set priority, etc.
|
||||
apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device));
|
||||
apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, false));
|
||||
|
||||
backgroundNotificationSentCounter.increment();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class FcmSender implements PushNotificationSender {
|
||||
Message.Builder builder = Message.builder()
|
||||
.setToken(pushNotification.deviceToken())
|
||||
.setAndroidConfig(AndroidConfig.builder()
|
||||
.setPriority(AndroidConfig.Priority.HIGH)
|
||||
.setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL)
|
||||
.build());
|
||||
|
||||
final String key = switch (pushNotification.notificationType()) {
|
||||
|
||||
@@ -50,7 +50,7 @@ public class MessageSender {
|
||||
this.pushLatencyManager = pushLatencyManager;
|
||||
}
|
||||
|
||||
public void sendMessage(final Account account, final Device device, final Envelope message, boolean online)
|
||||
public void sendMessage(final Account account, final Device device, final Envelope message, final boolean online)
|
||||
throws NotPushRegisteredException {
|
||||
|
||||
final String channel;
|
||||
@@ -83,7 +83,7 @@ public class MessageSender {
|
||||
|
||||
if (!clientPresent) {
|
||||
try {
|
||||
pushNotificationManager.sendNewMessageNotification(account, device.getId());
|
||||
pushNotificationManager.sendNewMessageNotification(account, device.getId(), message.getUrgent());
|
||||
|
||||
final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId());
|
||||
RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
|
||||
|
||||
@@ -14,7 +14,8 @@ public record PushNotification(String deviceToken,
|
||||
NotificationType notificationType,
|
||||
@Nullable String data,
|
||||
@Nullable Account destination,
|
||||
@Nullable Device destinationDevice) {
|
||||
@Nullable Device destinationDevice,
|
||||
boolean urgent) {
|
||||
|
||||
public enum NotificationType {
|
||||
NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
|
||||
|
||||
@@ -13,10 +13,12 @@ import io.micrometer.core.instrument.Tags;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
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.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
@@ -27,6 +29,7 @@ public class PushNotificationManager {
|
||||
private final FcmSender fcmSender;
|
||||
private final ApnPushNotificationScheduler apnPushNotificationScheduler;
|
||||
private final PushLatencyManager pushLatencyManager;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification");
|
||||
private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification");
|
||||
@@ -37,25 +40,31 @@ public class PushNotificationManager {
|
||||
final APNSender apnSender,
|
||||
final FcmSender fcmSender,
|
||||
final ApnPushNotificationScheduler apnPushNotificationScheduler,
|
||||
final PushLatencyManager pushLatencyManager) {
|
||||
final PushLatencyManager pushLatencyManager,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this.accountsManager = accountsManager;
|
||||
this.apnSender = apnSender;
|
||||
this.fcmSender = fcmSender;
|
||||
this.apnPushNotificationScheduler = apnPushNotificationScheduler;
|
||||
this.pushLatencyManager = pushLatencyManager;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
}
|
||||
|
||||
public void sendNewMessageNotification(final Account destination, final long destinationDeviceId) throws NotPushRegisteredException {
|
||||
public void sendNewMessageNotification(final Account destination, final long destinationDeviceId, final boolean urgent) throws NotPushRegisteredException {
|
||||
final Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new);
|
||||
final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
|
||||
|
||||
final boolean effectiveUrgent =
|
||||
dynamicConfigurationManager.getConfiguration().getPushNotificationConfiguration().isLowUrgencyEnabled() ?
|
||||
urgent : true;
|
||||
|
||||
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||
PushNotification.NotificationType.NOTIFICATION, null, destination, device));
|
||||
PushNotification.NotificationType.NOTIFICATION, null, destination, device, effectiveUrgent));
|
||||
}
|
||||
|
||||
public void sendRegistrationChallengeNotification(final String deviceToken, final PushNotification.TokenType tokenType, final String challengeToken) {
|
||||
sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null));
|
||||
sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true));
|
||||
}
|
||||
|
||||
public void sendRateLimitChallengeNotification(final Account destination, final String challengeToken)
|
||||
@@ -65,7 +74,7 @@ public class PushNotificationManager {
|
||||
final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
|
||||
|
||||
sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
|
||||
PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device));
|
||||
PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true));
|
||||
}
|
||||
|
||||
public void handleMessagesRetrieved(final Account account, final Device device, final String userAgent) {
|
||||
@@ -92,44 +101,55 @@ public class PushNotificationManager {
|
||||
|
||||
@VisibleForTesting
|
||||
void sendNotification(final PushNotification pushNotification) {
|
||||
final PushNotificationSender sender = switch (pushNotification.tokenType()) {
|
||||
case FCM -> fcmSender;
|
||||
case APN, APN_VOIP -> apnSender;
|
||||
};
|
||||
if (pushNotification.tokenType() == PushNotification.TokenType.APN && !pushNotification.urgent()) {
|
||||
// APNs imposes a per-device limit on background push notifications; schedule a notification for some time in the
|
||||
// future (possibly even now!) rather than sending a notification directly
|
||||
apnPushNotificationScheduler.scheduleBackgroundNotification(pushNotification.destination(),
|
||||
pushNotification.destinationDevice());
|
||||
} else {
|
||||
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()));
|
||||
sender.sendNotification(pushNotification).whenComplete((result, throwable) -> {
|
||||
if (throwable == null) {
|
||||
Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(),
|
||||
"notificationType", pushNotification.notificationType().name(),
|
||||
"urgent", String.valueOf(pushNotification.urgent()),
|
||||
"accepted", String.valueOf(result.accepted()),
|
||||
"unregistered", String.valueOf(result.unregistered()));
|
||||
|
||||
if (StringUtils.isNotBlank(result.errorCode())) {
|
||||
tags = tags.and("errorCode", result.errorCode());
|
||||
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(
|
||||
() -> apnPushNotificationScheduler.scheduleRecurringVoipNotification(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();
|
||||
}
|
||||
|
||||
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(() -> apnPushNotificationScheduler.scheduleRecurringVoipNotification(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) {
|
||||
|
||||
@@ -52,7 +52,8 @@ public class ReceiptSender {
|
||||
.setSourceDevice((int) sourceDeviceId)
|
||||
.setDestinationUuid(destinationUuid.toString())
|
||||
.setTimestamp(messageId)
|
||||
.setType(Envelope.Type.SERVER_DELIVERY_RECEIPT);
|
||||
.setType(Envelope.Type.SERVER_DELIVERY_RECEIPT)
|
||||
.setUrgent(false);
|
||||
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
for (final Device destinationDevice : destinationAccount.getDevices()) {
|
||||
|
||||
@@ -110,7 +110,9 @@ public class ChangeNumberManager {
|
||||
.setSourceUuid(sourceAndDestinationAccount.getUuid().toString())
|
||||
.setSourceDevice((int) Device.MASTER_ID)
|
||||
.setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString())
|
||||
.setUrgent(true)
|
||||
.build();
|
||||
|
||||
messageSender.sendMessage(sourceAndDestinationAccount, destinationDevice.get(), envelope, false);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
logger.debug("Not registered", e);
|
||||
|
||||
@@ -96,7 +96,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
|
||||
|
||||
if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) {
|
||||
try {
|
||||
pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId());
|
||||
pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId(), true);
|
||||
} catch (NotPushRegisteredException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user