| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -5,6 +5,7 @@
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				package org.whispersystems.textsecuregcm.storage;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import static com.codahale.metrics.MetricRegistry.name;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import static java.util.Objects.requireNonNull;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import com.fasterxml.jackson.core.JsonProcessingException;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import com.google.common.annotations.VisibleForTesting;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -18,7 +19,6 @@ import java.security.MessageDigest;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.security.NoSuchAlgorithmException;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.time.Clock;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.time.Duration;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.time.Instant;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.ArrayList;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.HashMap;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.List;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -29,12 +29,14 @@ import java.util.UUID;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.concurrent.CompletionException;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.concurrent.CompletionStage;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.concurrent.TimeUnit;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.function.Predicate;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.function.Supplier;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import java.util.stream.Collectors;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import javax.annotation.Nonnull;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.slf4j.Logger;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.slf4j.LoggerFactory;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.whispersystems.textsecuregcm.util.AttributeValues;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.whispersystems.textsecuregcm.util.ExceptionUtils;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.whispersystems.textsecuregcm.util.SystemMapper;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import org.whispersystems.textsecuregcm.util.UUIDUtil;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -56,8 +58,31 @@ import software.amazon.awssdk.services.dynamodb.model.Update;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				import software.amazon.awssdk.utils.CompletableFutureUtils;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Logger log = LoggerFactory.getLogger(Accounts.class);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final byte RESERVED_USERNAME_HASH_VERSION = 1;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer RESERVE_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "reserveUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final String CONDITIONAL_CHECK_FAILED = "ConditionalCheckFailed";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final String TRANSACTION_CONFLICT = "TransactionConflict";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // uuid, primary key
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  static final String KEY_ACCOUNT_UUID = "U";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  // uuid, attribute on account table, primary key for PNI table
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -78,45 +103,32 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  static final String ATTR_TTL = "TTL";
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final Clock clock;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final DynamoDbClient client;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final DynamoDbAsyncClient asyncClient;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final String phoneNumberConstraintTableName;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final String phoneNumberIdentifierConstraintTableName;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final String usernamesConstraintTableName;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final String accountsTableName;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private final int scanPageSize;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final byte RESERVED_USERNAME_HASH_VERSION = 1;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CREATE_TIMER = Metrics.timer(name(Accounts.class, "create"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CHANGE_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "changeNumber"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer SET_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "setUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer RESERVE_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "reserveUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer CLEAR_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "clearUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_USERNAME_TIMER = Metrics.timer(name(Accounts.class, "getByUsername"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static final Logger log = LoggerFactory.getLogger(Accounts.class);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @VisibleForTesting
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Accounts(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Clock clock,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      DynamoDbClient client, DynamoDbAsyncClient asyncClient,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      String accountsTableName, String phoneNumberConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final DynamoDbClient client,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final DynamoDbAsyncClient asyncClient,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String accountsTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String phoneNumberConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String phoneNumberIdentifierConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final int scanPageSize) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    super(client);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.clock = clock;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.client = client;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.asyncClient = asyncClient;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.phoneNumberConstraintTableName = phoneNumberConstraintTableName;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.phoneNumberIdentifierConstraintTableName = phoneNumberIdentifierConstraintTableName;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -125,105 +137,61 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this.scanPageSize = scanPageSize;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Accounts(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      DynamoDbClient client, DynamoDbAsyncClient asyncClient,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      String accountsTableName, String phoneNumberConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      String phoneNumberIdentifierConstraintTableName, final String usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Accounts(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final DynamoDbClient client,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final DynamoDbAsyncClient asyncClient,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String accountsTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String phoneNumberConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String phoneNumberIdentifierConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final int scanPageSize) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this(Clock.systemUTC(), dynamicConfigurationManager, client, asyncClient, accountsTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    this(Clock.systemUTC(), client, asyncClient, accountsTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        phoneNumberConstraintTableName, phoneNumberIdentifierConstraintTableName, usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        scanPageSize);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public boolean create(Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public boolean create(final Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return CREATE_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItem phoneNumberConstraintPut = TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .put(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .tableName(phoneNumberConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .conditionExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "attribute_not_exists(#number) OR (attribute_exists(#number) AND #uuid = :uuid)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeNames(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        Map.of("#uuid", KEY_ACCOUNT_UUID,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            "#number", ATTR_ACCOUNT_E164))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeValues(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue numberAttr = AttributeValues.fromString(account.getNumber());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue pniUuidAttr = AttributeValues.fromUUID(account.getPhoneNumberIdentifier());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItem phoneNumberIdentifierConstraintPut = TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .put(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .tableName(phoneNumberIdentifierConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .conditionExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "attribute_not_exists(#pni) OR (attribute_exists(#pni) AND #uuid = :uuid)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeNames(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        Map.of("#uuid", KEY_ACCOUNT_UUID,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                            "#pni", ATTR_PNI_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeValues(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        Map.of(":uuid", AttributeValues.fromUUID(account.getUuid())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final TransactWriteItem phoneNumberConstraintPut = buildConstraintTablePutIfAbsent(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final Map<String, AttributeValue> item = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final TransactWriteItem phoneNumberIdentifierConstraintPut = buildConstraintTablePutIfAbsent(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniUuidAttr);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        // Add the UAK if it's in the account
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        account.getUnidentifiedAccessKey()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .map(AttributeValues::fromByteArray)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .ifPresent(uak -> item.put(ATTR_UAK, uak));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItem accountPut = TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .conditionExpression("attribute_not_exists(#number) OR #number = :number")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .expressionAttributeValues(Map.of(":number", AttributeValues.fromString(account.getNumber())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(accountsTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .item(item)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final TransactWriteItem accountPut = buildAccountPut(account, uuidAttr, numberAttr, pniUuidAttr);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .transactItems(phoneNumberConstraintPut, phoneNumberIdentifierConstraintPut, accountPut)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        } catch (TransactionCanceledException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        } catch (final TransactionCanceledException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          final CancellationReason accountCancellationReason = e.cancellationReasons().get(2);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if ("ConditionalCheckFailed".equals(accountCancellationReason.code())) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if (conditionalCheckFailed(accountCancellationReason)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            throw new IllegalArgumentException("account identifier present with different phone number");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          final CancellationReason phoneNumberConstraintCancellationReason = e.cancellationReasons().get(0);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          final CancellationReason phoneNumberIdentifierConstraintCancellationReason = e.cancellationReasons().get(1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if ("ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ||
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              "ConditionalCheckFailed".equals(phoneNumberIdentifierConstraintCancellationReason.code())) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if (conditionalCheckFailed(phoneNumberConstraintCancellationReason)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              || conditionalCheckFailed(phoneNumberIdentifierConstraintCancellationReason)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            // In theory, both reasons should trip in tandem and either should give us the information we need. Even so,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            // we'll be cautious here and make sure we're choosing a condition check that really failed.
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final CancellationReason reason = "ConditionalCheckFailed".equals(phoneNumberConstraintCancellationReason.code()) ?
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                phoneNumberConstraintCancellationReason : phoneNumberIdentifierConstraintCancellationReason;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final CancellationReason reason = conditionalCheckFailed(phoneNumberConstraintCancellationReason)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                ? phoneNumberConstraintCancellationReason
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                : phoneNumberIdentifierConstraintCancellationReason;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final ByteBuffer actualAccountUuid = reason.item().get(KEY_ACCOUNT_UUID).b().asByteBuffer();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            account.setUuid(UUIDUtil.fromByteBuffer(actualAccountUuid));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final Account existingAccount = getByAccountIdentifier(account.getUuid()).orElseThrow();
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -235,7 +203,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return false;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if ("TransactionConflict".equals(accountCancellationReason.code())) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if (TRANSACTION_CONFLICT.equals(accountCancellationReason.code())) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            // this should only happen if two clients manage to make concurrent create() calls
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            throw new ContestedOptimisticLockException();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          }
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -243,7 +211,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          // this shouldn't happen
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          throw new RuntimeException("could not create account: " + extractCancellationReasonCodes(e));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      } catch (JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      } catch (final JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        throw new IllegalArgumentException(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -275,62 +243,34 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final List<TransactWriteItem> writeItems = new ArrayList<>();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue uuidAttr = AttributeValues.fromUUID(account.getUuid());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue numberAttr = AttributeValues.fromString(number);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final AttributeValue pniAttr = AttributeValues.fromUUID(phoneNumberIdentifier);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(originalNumber)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .conditionExpression("attribute_not_exists(#number)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberIdentifierConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(originalPni)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberIdentifierConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .conditionExpression("attribute_not_exists(#pni)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .expressionAttributeNames(Map.of("#pni", ATTR_PNI_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, originalNumber));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(buildConstraintTablePut(phoneNumberConstraintTableName, uuidAttr, ATTR_ACCOUNT_E164, numberAttr));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, originalPni));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(buildConstraintTablePut(phoneNumberIdentifierConstraintTableName, uuidAttr, ATTR_PNI_UUID, pniAttr));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        writeItems.add(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .update(Update.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .tableName(accountsTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .updateExpression("SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .conditionExpression("attribute_exists(#number) AND #version = :version")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .key(Map.of(KEY_ACCOUNT_UUID, uuidAttr))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .updateExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "SET #data = :data, #number = :number, #pni = :pni, #cds = :cds ADD #version :version_increment")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .conditionExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "attribute_exists(#number) AND #version = :version")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeNames(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "#number", ATTR_ACCOUNT_E164,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "#data", ATTR_ACCOUNT_DATA,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "#cds", ATTR_CANONICALLY_DISCOVERABLE,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "#pni", ATTR_PNI_UUID,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        "#version", ATTR_VERSION))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .expressionAttributeValues(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":number", numberAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":number", AttributeValues.fromString(number),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":pni", AttributeValues.fromUUID(phoneNumberIdentifier),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":pni", pniAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":version", AttributeValues.fromInt(account.getVersion()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                        ":version_increment", AttributeValues.fromInt(1)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                    .build())
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -340,7 +280,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .transactItems(writeItems)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        account.setVersion(account.getVersion() + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        succeeded = true;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -354,21 +294,6 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public static byte[] reservedUsernameHash(final UUID accountId, final String reservedUsername) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final MessageDigest sha256;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      sha256 = MessageDigest.getInstance("SHA-256");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (NoSuchAlgorithmException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new AssertionError(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final ByteBuffer byteBuffer = ByteBuffer.allocate(32 + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    sha256.update(reservedUsername.getBytes(StandardCharsets.UTF_8));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    sha256.update(UUIDUtil.toBytes(accountId));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    byteBuffer.put(RESERVED_USERNAME_HASH_VERSION);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    byteBuffer.put(sha256.digest());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return byteBuffer.array();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  /**
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				   * Reserve a username under a token
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				   *
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -386,7 +311,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    boolean succeeded = false;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    long expirationTime = clock.instant().plus(ttl).getEpochSecond();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final long expirationTime = clock.instant().plus(ttl).getEpochSecond();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final UUID reservationToken = UUID.randomUUID();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -425,14 +350,14 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .transactItems(writeItems)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setVersion(account.getVersion() + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      succeeded = true;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new IllegalArgumentException(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final TransactionCanceledException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch("ConditionalCheckFailed"::equals)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        throw new ContestedOptimisticLockException();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw e;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -520,25 +445,21 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                  .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .tableName(usernamesConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .key(Map.of(ATTR_USERNAME, AttributeValues.fromString(originalUsername)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build()));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      maybeOriginalUsername.ifPresent(originalUsername -> writeItems.add(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          buildDelete(usernamesConstraintTableName, ATTR_USERNAME, originalUsername)));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .transactItems(writeItems)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setVersion(account.getVersion() + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      succeeded = true;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new IllegalArgumentException(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final TransactionCanceledException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch("ConditionalCheckFailed"::equals)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      if (e.cancellationReasons().stream().map(CancellationReason::code).anyMatch(CONDITIONAL_CHECK_FAILED::equals)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        throw new ContestedOptimisticLockException();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw e;
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -551,7 +472,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void clearUsername(Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void clearUsername(final Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    account.getUsername().ifPresent(username -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      CLEAR_USERNAME_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        account.setUsername(null);
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -578,25 +499,20 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                      .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                  .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          writeItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                  .tableName(usernamesConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                  .key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                  .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          writeItems.add(buildDelete(usernamesConstraintTableName, ATTR_USERNAME, username));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .transactItems(writeItems)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          account.setVersion(account.getVersion() + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          succeeded = true;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        } catch (final JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          throw new IllegalArgumentException(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        } catch (final TransactionCanceledException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if ("ConditionalCheckFailed".equals(e.cancellationReasons().get(0).code())) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          if (conditionalCheckFailed(e.cancellationReasons().get(0))) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            throw new ContestedOptimisticLockException();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -610,27 +526,18 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  /**
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				   * Extract the cause from a CompletionException
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				   */
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static Throwable unwrap(Throwable throwable) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    while (throwable instanceof CompletionException e && throwable.getCause() != null) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throwable = e.getCause();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return throwable;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public CompletionStage<Void> updateAsync(Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public CompletionStage<Void> updateAsync(final Account account) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return record(UPDATE_TIMER, () -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final UpdateItemRequest updateItemRequest;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        // username, e164, and pni cannot be modified through this method
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        Map<String, String> attrNames = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final Map<String, String> attrNames = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "#number", ATTR_ACCOUNT_E164,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "#data", ATTR_ACCOUNT_DATA,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "#cds", ATTR_CANONICALLY_DISCOVERABLE,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            "#version", ATTR_VERSION));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        Map<String, AttributeValue> attrValues = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final Map<String, AttributeValue> attrValues = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ":data", AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ":cds", AttributeValues.fromBool(account.shouldBeVisibleInDirectory()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            ":version", AttributeValues.fromInt(account.getVersion()),
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -654,7 +561,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeNames(attrNames)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeValues(attrValues)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      } catch (JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      } catch (final JsonProcessingException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        throw new IllegalArgumentException(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -664,7 +571,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            return (Void) null;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          })
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .exceptionally(throwable -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final Throwable unwrapped = unwrap(throwable);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            final Throwable unwrapped = ExceptionUtils.unwrap(throwable);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            if (unwrapped instanceof TransactionConflictException) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				              throw new ContestedOptimisticLockException();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            } else if (unwrapped instanceof ConditionalCheckFailedException e) {
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -679,12 +586,12 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void update(Account account) throws ContestedOptimisticLockException {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void update(final Account account) throws ContestedOptimisticLockException {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      this.updateAsync(account).toCompletableFuture().join();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (CompletionException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      updateAsync(account).toCompletableFuture().join();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final CompletionException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // unwrap CompletionExceptions, throw as long is it's unchecked
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      Throwables.throwIfUnchecked(unwrap(e));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      Throwables.throwIfUnchecked(ExceptionUtils.unwrap(e));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // if we otherwise somehow got a wrapped checked exception,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // rethrow the checked exception wrapped by the original CompletionException
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -698,15 +605,14 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public boolean usernameAvailable(final Optional<UUID> reservationToken, final String username) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final GetItemResponse response = client.getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .tableName(usernamesConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!response.hasItem()) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Optional<Map<String, AttributeValue>> usernameItem = itemByKey(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        usernamesConstraintTableName, ATTR_USERNAME, AttributeValues.fromString(username));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (usernameItem.isEmpty()) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // username is free
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return true;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Map<String, AttributeValue> item = response.item();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Map<String, AttributeValue> item = usernameItem.get();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (AttributeValues.getLong(item, ATTR_TTL, Long.MAX_VALUE) < clock.instant().getEpochSecond()) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      // username was reserved, but has expired
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -719,112 +625,56 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .orElse(false);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByE164(String number) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return GET_BY_NUMBER_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final GetItemResponse response = client.getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .tableName(phoneNumberConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(number)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return Optional.ofNullable(response.item())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(item -> item.get(KEY_ACCOUNT_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(this::accountByUuid)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(Accounts::fromItem);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByUsername(final String username) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return GET_BY_USERNAME_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final GetItemResponse response = client.getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .tableName(usernamesConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return Optional.ofNullable(response.item())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          // ignore items with a ttl (reservations)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .filter(item -> !item.containsKey(ATTR_TTL))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(item -> item.get(KEY_ACCOUNT_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(this::accountByUuid)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(Accounts::fromItem);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByE164(final String number) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return getByIndirectLookup(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        GET_BY_NUMBER_TIMER, phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, AttributeValues.fromString(number));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByPhoneNumberIdentifier(final UUID phoneNumberIdentifier) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return GET_BY_PNI_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final GetItemResponse response = client.getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .tableName(phoneNumberIdentifierConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return Optional.ofNullable(response.item())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(item -> item.get(KEY_ACCOUNT_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(this::accountByUuid)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(Accounts::fromItem);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return getByIndirectLookup(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        GET_BY_PNI_TIMER, phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, AttributeValues.fromUUID(phoneNumberIdentifier));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private Map<String, AttributeValue> accountByUuid(AttributeValue uuid) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    GetItemResponse r = client.getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .tableName(accountsTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .key(Map.of(KEY_ACCOUNT_UUID, uuid))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .consistentRead(true)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return r.item().isEmpty() ? null : r.item();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByUsername(final String username) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return getByIndirectLookup(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        GET_BY_USERNAME_TIMER,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        usernamesConstraintTableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_USERNAME,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        AttributeValues.fromString(username),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        item -> !item.containsKey(ATTR_TTL) // ignore items with a ttl (reservations)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    );
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByAccountIdentifier(UUID uuid) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return GET_BY_UUID_TIMER.record(() ->
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        Optional.ofNullable(accountByUuid(AttributeValues.fromUUID(uuid)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .map(Accounts::fromItem));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public Optional<Account> getByAccountIdentifier(final UUID uuid) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return requireNonNull(GET_BY_UUID_TIMER.record(() ->
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            itemByKey(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .map(Accounts::fromItem)));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void delete(UUID uuid) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    DELETE_TIMER.record(() -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public void delete(final UUID uuid) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    DELETE_TIMER.record(() -> getByAccountIdentifier(uuid).ifPresent(account -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      getByAccountIdentifier(uuid).ifPresent(account -> {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          buildDelete(phoneNumberConstraintTableName, ATTR_ACCOUNT_E164, account.getNumber()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          buildDelete(accountsTableName, KEY_ACCOUNT_UUID, uuid),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          buildDelete(phoneNumberIdentifierConstraintTableName, ATTR_PNI_UUID, account.getPhoneNumberIdentifier())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      ));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItem phoneNumberDelete = TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(ATTR_ACCOUNT_E164, AttributeValues.fromString(account.getNumber())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.getUsername().ifPresent(username -> transactWriteItems.add(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          buildDelete(usernamesConstraintTableName, ATTR_USERNAME, username)));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItem accountDelete = TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(accountsTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .transactItems(transactWriteItems).build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        final List<TransactWriteItem> transactWriteItems = new ArrayList<>(List.of(phoneNumberDelete, accountDelete));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        transactWriteItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(phoneNumberIdentifierConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(ATTR_PNI_UUID, AttributeValues.fromUUID(account.getPhoneNumberIdentifier())))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        account.getUsername().ifPresent(username -> transactWriteItems.add(TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .tableName(usernamesConstraintTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .key(Map.of(ATTR_USERNAME, AttributeValues.fromString(username)))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build()));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .transactItems(transactWriteItems).build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        client.transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    });
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      db().transactWriteItems(request);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public AccountCrawlChunk getAllFrom(final UUID from, final int maxCount) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .limit(scanPageSize)
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -833,6 +683,7 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_OFFSET_TIMER);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public AccountCrawlChunk getAllFromStart(final int maxCount) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .limit(scanPageSize);
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -840,34 +691,185 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return scanForChunk(scanRequestBuilder, maxCount, GET_ALL_FROM_START_TIMER);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static <T> CompletionStage<T> record(final Timer timer, Supplier<CompletionStage<T>> toRecord)  {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Instant start = Instant.now();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return toRecord.get().whenComplete((ignoreT, ignoreE) -> timer.record(Duration.between(start, Instant.now())));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private Optional<Account> getByIndirectLookup(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Timer timer,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String tableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String keyName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return getByIndirectLookup(timer, tableName, keyName, keyValue, i -> true);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private Optional<Account> getByIndirectLookup(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Timer timer,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String tableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String keyName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue keyValue,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Predicate<? super Map<String, AttributeValue>> predicate) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return requireNonNull(timer.record(() -> itemByKey(tableName, keyName, keyValue)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .filter(predicate)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .map(item -> item.get(KEY_ACCOUNT_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .flatMap(uuid -> itemByKey(accountsTableName, KEY_ACCOUNT_UUID, uuid))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .map(Accounts::fromItem)));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private Optional<Map<String, AttributeValue>> itemByKey(final String table, final String keyName, final AttributeValue keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final GetItemResponse response = db().getItem(GetItemRequest.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .tableName(table)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .key(Map.of(keyName, keyValue))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .consistentRead(true)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return Optional.ofNullable(response.item()).filter(m -> !m.isEmpty());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private TransactWriteItem buildAccountPut(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Account account,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue uuidAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue numberAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue pniUuidAttr) throws JsonProcessingException {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Map<String, AttributeValue> item = new HashMap<>(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        KEY_ACCOUNT_UUID, uuidAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_ACCOUNT_E164, numberAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_PNI_UUID, pniUuidAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_ACCOUNT_DATA, AttributeValues.fromByteArray(SystemMapper.getMapper().writeValueAsBytes(account)),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_VERSION, AttributeValues.fromInt(account.getVersion()),
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        ATTR_CANONICALLY_DISCOVERABLE, AttributeValues.fromBool(account.shouldBeVisibleInDirectory())));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    // Add the UAK if it's in the account
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    account.getUnidentifiedAccessKey()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .map(AttributeValues::fromByteArray)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .ifPresent(uak -> item.put(ATTR_UAK, uak));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .conditionExpression("attribute_not_exists(#number) OR #number = :number")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeNames(Map.of("#number", ATTR_ACCOUNT_E164))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeValues(Map.of(":number", numberAttr))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .tableName(accountsTableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .item(item)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static TransactWriteItem buildConstraintTablePutIfAbsent(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String tableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue uuidAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String keyName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue keyValue
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  ) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .tableName(tableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                keyName, keyValue,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                KEY_ACCOUNT_UUID, uuidAttr))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .conditionExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                "attribute_not_exists(#key) OR #uuid = :uuid")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeNames(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                "#key", keyName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                "#uuid", KEY_ACCOUNT_UUID))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeValues(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                ":uuid", uuidAttr))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static TransactWriteItem buildConstraintTablePut(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String tableName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue uuidAttr,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final String keyName,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final AttributeValue keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .put(Put.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .tableName(tableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .item(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                keyName, keyValue,
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                KEY_ACCOUNT_UUID, uuidAttr))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .conditionExpression(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                "attribute_not_exists(#key)")
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .expressionAttributeNames(Map.of(
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				                "#key", keyName))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static TransactWriteItem buildDelete(final String tableName, final String keyName, final String keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return buildDelete(tableName, keyName, AttributeValues.fromString(keyValue));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static TransactWriteItem buildDelete(final String tableName, final String keyName, final UUID keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return buildDelete(tableName, keyName, AttributeValues.fromUUID(keyValue));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static TransactWriteItem buildDelete(final String tableName, final String keyName, final AttributeValue keyValue) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return TransactWriteItem.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .delete(Delete.builder()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .tableName(tableName)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .key(Map.of(keyName, keyValue))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				            .build())
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .build();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static <T> CompletionStage<T> record(final Timer timer, final Supplier<CompletionStage<T>> toRecord)  {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final Timer.Sample sample = Timer.start();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return toRecord.get().whenComplete((ignoreT, ignoreE) -> sample.stop(timer));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private AccountCrawlChunk scanForChunk(final ScanRequest.Builder scanRequestBuilder, final int maxCount, final Timer timer) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    scanRequestBuilder.tableName(accountsTableName);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final List<Map<String, AttributeValue>> items = timer.record(() -> scan(scanRequestBuilder.build(), maxCount));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final List<Map<String, AttributeValue>> items = requireNonNull(timer.record(() -> scan(scanRequestBuilder.build(), maxCount)));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final List<Account> accounts = items.stream().map(Accounts::fromItem).toList();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return new AccountCrawlChunk(accounts, accounts.size() > 0 ? accounts.get(accounts.size() - 1).getUuid() : null);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static String extractCancellationReasonCodes(final TransactionCanceledException exception) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return exception.cancellationReasons().stream()
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .map(CancellationReason::code)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        .collect(Collectors.joining(", "));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  public static byte[] reservedUsernameHash(final UUID accountId, final String reservedUsername) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final MessageDigest sha256;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      sha256 = MessageDigest.getInstance("SHA-256");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final NoSuchAlgorithmException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new AssertionError(e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    final ByteBuffer byteBuffer = ByteBuffer.allocate(32 + 1);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    sha256.update(reservedUsername.getBytes(StandardCharsets.UTF_8));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    sha256.update(UUIDUtil.toBytes(accountId));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    byteBuffer.put(RESERVED_USERNAME_HASH_VERSION);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    byteBuffer.put(sha256.digest());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return byteBuffer.array();
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @VisibleForTesting
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  static Account fromItem(Map<String, AttributeValue> item) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!item.containsKey(ATTR_ACCOUNT_DATA) ||
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        !item.containsKey(ATTR_ACCOUNT_E164) ||
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        // TODO: eventually require ATTR_CANONICALLY_DISCOVERABLE
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        !item.containsKey(KEY_ACCOUNT_UUID)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  @Nonnull
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  static Account fromItem(final Map<String, AttributeValue> item) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    // TODO: eventually require ATTR_CANONICALLY_DISCOVERABLE
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    if (!item.containsKey(ATTR_ACCOUNT_DATA)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        || !item.containsKey(ATTR_ACCOUNT_E164)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				        || !item.containsKey(KEY_ACCOUNT_UUID)) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new RuntimeException("item missing values");
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    try {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final Account account = SystemMapper.getMapper().readValue(item.get(ATTR_ACCOUNT_DATA).b().asByteArray(), Account.class);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final UUID accountIdentifier = UUIDUtil.fromByteBuffer(item.get(KEY_ACCOUNT_UUID).b().asByteBuffer());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      final UUID phoneNumberIdentifierFromAttribute = AttributeValues.getUUID(item, ATTR_PNI_UUID, null);
 | 
			
		
		
	
	
		
			
				
					| 
						
					 | 
				
			
			 | 
			 | 
			
				@ -883,12 +885,18 @@ public class Accounts extends AbstractDynamoDbStore {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setUuid(accountIdentifier);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setUsername(AttributeValues.getString(item, ATTR_USERNAME, null));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE)).map(av -> av.bool()).orElse(false));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE))
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .map(AttributeValue::bool)
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				          .orElse(false));
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      return account;
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (IOException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    } catch (final IOException e) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				      throw new RuntimeException("Could not read stored account data", e);
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  private static boolean conditionalCheckFailed(final CancellationReason reason) {
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				    return CONDITIONAL_CHECK_FAILED.equals(reason.code());
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				  }
 | 
			
		
		
	
		
			
				 | 
				 | 
			
			 | 
			 | 
			
				}
 | 
			
		
		
	
	
		
			
				
					| 
						 
							
							
							
						 
					 | 
				
			
			 | 
			 | 
			
				
 
 |