Expire APNs tokens if they haven't been updated since the expiration timestamp
This commit is contained in:
parent
1cf174a613
commit
2f76738b50
|
@ -10,6 +10,7 @@ import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tags;
|
import io.micrometer.core.instrument.Tags;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -131,7 +132,11 @@ public class PushNotificationManager {
|
||||||
|
|
||||||
if (result.unregistered() && pushNotification.destination() != null
|
if (result.unregistered() && pushNotification.destination() != null
|
||||||
&& pushNotification.destinationDevice() != null) {
|
&& pushNotification.destinationDevice() != null) {
|
||||||
handleDeviceUnregistered(pushNotification.destination(), pushNotification.destinationDevice());
|
|
||||||
|
handleDeviceUnregistered(pushNotification.destination(),
|
||||||
|
pushNotification.destinationDevice(),
|
||||||
|
pushNotification.tokenType(),
|
||||||
|
result.unregisteredTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.accepted() &&
|
if (result.accepted() &&
|
||||||
|
@ -164,28 +169,53 @@ public class PushNotificationManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDeviceUnregistered(final Account account, final Device device) {
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
if (StringUtils.isNotBlank(device.getGcmId())) {
|
private void handleDeviceUnregistered(final Account account,
|
||||||
final String originalFcmId = device.getGcmId();
|
final Device device,
|
||||||
|
final PushNotification.TokenType tokenType,
|
||||||
|
final Optional<Instant> maybeTokenInvalidationTimestamp) {
|
||||||
|
|
||||||
// Reread the account to avoid marking the caller's account as stale. The consumers of this class tend to
|
final boolean tokenExpired = maybeTokenInvalidationTimestamp.map(tokenInvalidationTimestamp ->
|
||||||
// promise not to modify accounts. There's no need to force the caller to be considered mutable just for
|
tokenInvalidationTimestamp.isAfter(Instant.ofEpochMilli(device.getPushTimestamp()))).orElse(true);
|
||||||
// updating an uninstalled feedback timestamp though.
|
|
||||||
final Optional<Account> rereadAccount = accountsManager.getByAccountIdentifier(account.getUuid());
|
if (tokenExpired) {
|
||||||
if (rereadAccount.isEmpty()) {
|
if (tokenType == PushNotification.TokenType.APN || tokenType == PushNotification.TokenType.APN_VOIP) {
|
||||||
// Don't bother removing the token; the account is gone
|
apnPushNotificationScheduler.cancelScheduledNotifications(account, device).whenComplete(logErrors());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rereadAccount.get().getDevice(device.getId()).ifPresent(rereadDevice ->
|
clearPushToken(account, device, tokenType);
|
||||||
accountsManager.updateDevice(rereadAccount.get(), device.getId(), d -> {
|
|
||||||
// Don't clear the token if it's already changed
|
|
||||||
if (originalFcmId.equals(d.getGcmId())) {
|
|
||||||
d.setGcmId(null);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
apnPushNotificationScheduler.cancelScheduledNotifications(account, device).whenComplete(logErrors());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clearPushToken(final Account account, final Device device, final PushNotification.TokenType tokenType) {
|
||||||
|
final String originalToken = getPushToken(device, tokenType);
|
||||||
|
|
||||||
|
if (originalToken == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reread the account to avoid marking the caller's account as stale. The consumers of this class tend to
|
||||||
|
// promise not to modify accounts. There's no need to force the caller to be considered mutable just for
|
||||||
|
// updating an uninstalled feedback timestamp though.
|
||||||
|
accountsManager.getByAccountIdentifier(account.getUuid()).ifPresent(rereadAccount ->
|
||||||
|
rereadAccount.getDevice(device.getId()).ifPresent(rereadDevice ->
|
||||||
|
accountsManager.updateDevice(rereadAccount, device.getId(), d -> {
|
||||||
|
// Don't clear the token if it's already changed
|
||||||
|
if (originalToken.equals(getPushToken(d, tokenType))) {
|
||||||
|
switch (tokenType) {
|
||||||
|
case FCM -> d.setGcmId(null);
|
||||||
|
case APN -> d.setApnId(null);
|
||||||
|
case APN_VOIP -> d.setVoipApnId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPushToken(final Device device, final PushNotification.TokenType tokenType) {
|
||||||
|
return switch (tokenType) {
|
||||||
|
case FCM -> device.getGcmId();
|
||||||
|
case APN -> device.getApnId();
|
||||||
|
case APN_VOIP -> device.getVoipApnId();
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.common.net.HttpHeaders;
|
import com.google.common.net.HttpHeaders;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
@ -244,9 +245,13 @@ class PushNotificationManagerTest {
|
||||||
void testSendNotificationUnregisteredApn() {
|
void testSendNotificationUnregisteredApn() {
|
||||||
final Account account = mock(Account.class);
|
final Account account = mock(Account.class);
|
||||||
final Device device = mock(Device.class);
|
final Device device = mock(Device.class);
|
||||||
|
final UUID aci = UUID.randomUUID();
|
||||||
when(device.getId()).thenReturn(Device.PRIMARY_ID);
|
when(device.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
|
when(device.getApnId()).thenReturn("apns-token");
|
||||||
|
when(device.getVoipApnId()).thenReturn("apns-voip-token");
|
||||||
when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device));
|
when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device));
|
||||||
|
when(account.getUuid()).thenReturn(aci);
|
||||||
|
when(accountsManager.getByAccountIdentifier(aci)).thenReturn(Optional.of(account));
|
||||||
|
|
||||||
final PushNotification pushNotification = new PushNotification(
|
final PushNotification pushNotification = new PushNotification(
|
||||||
"token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true);
|
"token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true);
|
||||||
|
@ -260,11 +265,45 @@ class PushNotificationManagerTest {
|
||||||
pushNotificationManager.sendNotification(pushNotification);
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
verifyNoInteractions(fcmSender);
|
verifyNoInteractions(fcmSender);
|
||||||
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
|
verify(accountsManager).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
|
||||||
verify(device, never()).setGcmId(any());
|
verify(device).setVoipApnId(null);
|
||||||
|
verify(device, never()).setApnId(any());
|
||||||
verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device);
|
verify(apnPushNotificationScheduler).cancelScheduledNotifications(account, device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSendNotificationUnregisteredApnTokenUpdated() {
|
||||||
|
final Instant tokenTimestamp = Instant.now();
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
final UUID aci = UUID.randomUUID();
|
||||||
|
when(device.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
|
when(device.getApnId()).thenReturn("apns-token");
|
||||||
|
when(device.getVoipApnId()).thenReturn("apns-voip-token");
|
||||||
|
when(device.getPushTimestamp()).thenReturn(tokenTimestamp.toEpochMilli());
|
||||||
|
when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device));
|
||||||
|
when(account.getUuid()).thenReturn(aci);
|
||||||
|
when(accountsManager.getByAccountIdentifier(aci)).thenReturn(Optional.of(account));
|
||||||
|
|
||||||
|
final PushNotification pushNotification = new PushNotification(
|
||||||
|
"token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true);
|
||||||
|
|
||||||
|
when(apnSender.sendNotification(pushNotification))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.of(tokenTimestamp.minusSeconds(60)))));
|
||||||
|
|
||||||
|
when(apnPushNotificationScheduler.cancelScheduledNotifications(account, device))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
|
pushNotificationManager.sendNotification(pushNotification);
|
||||||
|
|
||||||
|
verifyNoInteractions(fcmSender);
|
||||||
|
verify(accountsManager, never()).updateDevice(eq(account), eq(Device.PRIMARY_ID), any());
|
||||||
|
verify(device, never()).setVoipApnId(any());
|
||||||
|
verify(device, never()).setApnId(any());
|
||||||
|
verify(apnPushNotificationScheduler, never()).cancelScheduledNotifications(account, device);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testHandleMessagesRetrieved() {
|
void testHandleMessagesRetrieved() {
|
||||||
final UUID accountIdentifier = UUID.randomUUID();
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
|
Loading…
Reference in New Issue