Add a command to clear IAP issued receipt count

Co-authored-by: Katherine <katherine@signal.org>
This commit is contained in:
ravi-signal
2025-09-10 11:00:02 -05:00
committed by GitHub
parent 61b162d0a1
commit c544628dfe
8 changed files with 153 additions and 1 deletions

View File

@@ -273,6 +273,7 @@ import org.whispersystems.textsecuregcm.workers.BackupMetricsCommand;
import org.whispersystems.textsecuregcm.workers.BackupUsageRecalculationCommand;
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
import org.whispersystems.textsecuregcm.workers.ClearIssuedReceiptRedemptionsCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.IdleDeviceNotificationSchedulerFactory;
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
@@ -342,6 +343,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
bootstrap.addCommand(new BackupUsageRecalculationCommand());
bootstrap.addCommand(new RemoveExpiredLinkedDevicesCommand());
bootstrap.addCommand(new NotifyIdleDevicesCommand());
bootstrap.addCommand(new ClearIssuedReceiptRedemptionsCommand());
bootstrap.addCommand(new ProcessScheduledJobsServiceCommand("process-idle-device-notification-jobs",
"Processes scheduled jobs to send notifications to idle devices",

View File

@@ -34,6 +34,7 @@ import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
@@ -109,6 +110,24 @@ public class IssuedReceiptsManager {
});
}
/**
* Clear the recorded issuances for a particular item
*
* @param processorItemId The itemId within the processor to clear
* @param processor The processor
* @return a future that yields true if the item was deleted, false if the item already did not exist
*/
public CompletableFuture<Boolean> clearIssuance(String processorItemId, PaymentProvider processor) {
final AttributeValue key = dynamoDbKey(processor, processorItemId);
final DeleteItemRequest deleteItemRequest = DeleteItemRequest.builder()
.tableName(table)
.key(Map.of(KEY_PROCESSOR_ITEM_ID, key))
.returnValues(ReturnValue.ALL_OLD)
.build();
return dynamoDbAsyncClient.deleteItem(deleteItemRequest)
.thenApply(item -> item.hasAttributes() && !item.attributes().isEmpty());
}
@VisibleForTesting
static AttributeValue dynamoDbKey(final PaymentProvider processor, String processorItemId) {
if (processor == PaymentProvider.STRIPE) {

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.workers;
import io.dropwizard.core.Application;
import io.dropwizard.core.setup.Environment;
import java.time.Clock;
import java.util.Optional;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.SubscriberCredentials;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.storage.Subscriptions;
import org.whispersystems.textsecuregcm.subscriptions.PaymentProvider;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionPaymentProcessor;
public class ClearIssuedReceiptRedemptionsCommand extends AbstractCommandWithDependencies {
private final Logger logger = LoggerFactory.getLogger(ClearIssuedReceiptRedemptionsCommand.class);
public ClearIssuedReceiptRedemptionsCommand() {
super(new Application<>() {
@Override
public void run(WhisperServerConfiguration configuration, Environment environment) {
}
}, "clear-issued-receipt-redemptions", "Clear issued receipt redemptions");
}
@Override
public void configure(Subparser subparser) {
super.configure(subparser);
subparser.addArgument("-s", "--subscriber-id")
.dest("subscriberId")
.type(String.class)
.required(true)
.help("The subscriber-id whose receipt redemptions should be clear");
}
@Override
protected void run(Environment environment, Namespace namespace, WhisperServerConfiguration configuration,
CommandDependencies deps) throws Exception {
try {
final String subscriberId = namespace.getString("subscriberId");
final SubscriberCredentials creds = SubscriberCredentials
.process(Optional.empty(), subscriberId, Clock.systemUTC());
final IssuedReceiptsManager issuedReceiptsManager = deps.issuedReceiptsManager();
final SubscriptionManager subscriptionManager = deps.subscriptionManager();
final Subscriptions.Record subscriber = subscriptionManager.getSubscriber(creds);
final PaymentProvider processorType = subscriber.getProcessorCustomer()
.orElseThrow(() -> new IllegalArgumentException("susbcriber did not have a subscription"))
.processor();
final SubscriptionPaymentProcessor processor = switch (processorType) {
case APPLE_APP_STORE -> deps.appleAppStoreManager();
case GOOGLE_PLAY_BILLING -> deps.googlePlayBillingManager();
default ->
throw new IllegalStateException("Cannot clear issued receipts for a non-IAP processor: " + processorType);
};
final SubscriptionPaymentProcessor.ReceiptItem receiptItem = processor.getReceiptItem(subscriber.subscriptionId);
final boolean deleted = issuedReceiptsManager.clearIssuance(receiptItem.itemId(), processorType).join();
logger.info("Deleted issuances for receiptItem: {}, subscriberId: {}, hadExistingIssuances: {}",
receiptItem.itemId(), subscriberId, deleted);
} catch (Exception ex) {
logger.warn("Removal Exception", ex);
throw new RuntimeException(ex);
}
}
}

View File

@@ -11,16 +11,23 @@ import com.codahale.metrics.MetricRegistry;
import com.fasterxml.jackson.databind.DeserializationFeature;
import io.dropwizard.core.setup.Environment;
import io.lettuce.core.resource.ClientResources;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.time.Clock;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.WhisperServerService;
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
@@ -72,6 +79,10 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
import org.whispersystems.textsecuregcm.storage.SingleUseECPreKeyStore;
import org.whispersystems.textsecuregcm.storage.SingleUseKEMPreKeyStore;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.storage.Subscriptions;
import org.whispersystems.textsecuregcm.subscriptions.AppleAppStoreManager;
import org.whispersystems.textsecuregcm.subscriptions.GooglePlayBillingManager;
import org.whispersystems.textsecuregcm.util.ManagedAwsCrt;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@@ -101,6 +112,9 @@ record CommandDependencies(
ClientResources.Builder redisClusterClientResourcesBuilder,
BackupManager backupManager,
IssuedReceiptsManager issuedReceiptsManager,
GooglePlayBillingManager googlePlayBillingManager,
AppleAppStoreManager appleAppStoreManager,
SubscriptionManager subscriptionManager,
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
DynamoDbAsyncClient dynamoDbAsyncClient,
PhoneNumberIdentifiers phoneNumberIdentifiers,
@@ -110,7 +124,7 @@ record CommandDependencies(
final String name,
final Environment environment,
final WhisperServerConfiguration configuration)
throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException {
throws IOException, GeneralSecurityException, InvalidInputException {
Clock clock = Clock.systemUTC();
environment.getObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@@ -310,6 +324,30 @@ record CommandDependencies(
configuration.getDynamoDbTables().getIssuedReceipts().getGenerator(),
configuration.getDynamoDbTables().getIssuedReceipts().getmaxIssuedReceiptsPerPaymentId());
final ServerSecretParams zkSecretParams = new ServerSecretParams(configuration.getZkConfig().serverSecret().value());
final ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
GooglePlayBillingManager googlePlayBillingManager = new GooglePlayBillingManager(
new ByteArrayInputStream(configuration.getGooglePlayBilling().credentialsJson().value().getBytes(StandardCharsets.UTF_8)),
configuration.getGooglePlayBilling().packageName(),
configuration.getGooglePlayBilling().applicationName(),
configuration.getGooglePlayBilling().productIdToLevel());
AppleAppStoreManager appleAppStoreManager = new AppleAppStoreManager(
configuration.getAppleAppStore().env(),
configuration.getAppleAppStore().bundleId(),
configuration.getAppleAppStore().appAppleId(),
configuration.getAppleAppStore().issuerId(),
configuration.getAppleAppStore().keyId(),
configuration.getAppleAppStore().encodedKey().value(),
configuration.getAppleAppStore().subscriptionGroupId(),
configuration.getAppleAppStore().productIdToLevel(),
configuration.getAppleAppStore().appleRootCerts(),
configuration.getAppleAppStore().retryConfigurationName());
final SubscriptionManager subscriptionManager = new SubscriptionManager(
new Subscriptions(configuration.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient),
List.of(googlePlayBillingManager, appleAppStoreManager),
zkReceiptOperations,
issuedReceiptsManager);
APNSender apnSender = new APNSender(apnSenderExecutor, configuration.getApnConfiguration());
FcmSender fcmSender = new FcmSender(fcmSenderExecutor, configuration.getFcmConfiguration().credentials().value());
PushNotificationScheduler pushNotificationScheduler = new PushNotificationScheduler(pushSchedulerCluster,
@@ -346,6 +384,9 @@ record CommandDependencies(
redisClientResourcesBuilder,
backupManager,
issuedReceiptsManager,
googlePlayBillingManager,
appleAppStoreManager,
subscriptionManager,
dynamicConfigurationManager,
dynamoDbAsyncClient,
phoneNumberIdentifiers,