mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-28 18:10:38 +00:00
Add PNI to recipients
This commit is contained in:
parent
e450f36e81
commit
b9eee539bd
10 changed files with 696 additions and 110 deletions
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
|
@ -54,6 +54,9 @@
|
|||
<option name="TERNARY_OPERATION_WRAP" value="5" />
|
||||
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
||||
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
|
||||
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
|
||||
<option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" />
|
||||
<option name="ENUM_CONSTANTS_WRAP" value="2" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
|
|
|
@ -21,6 +21,12 @@ dependencies {
|
|||
implementation("org.slf4j", "slf4j-api", "2.0.3")
|
||||
implementation("org.xerial", "sqlite-jdbc", "3.39.3.0")
|
||||
implementation("com.zaxxer", "HikariCP", "5.0.1")
|
||||
|
||||
testImplementation("org.junit.jupiter", "junit-jupiter", "5.9.0")
|
||||
}
|
||||
|
||||
tasks.named<Test>("test") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
|
|
@ -56,16 +56,19 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class IncomingMessageHandler {
|
||||
|
@ -194,7 +197,18 @@ public final class IncomingMessageHandler {
|
|||
if (content != null) {
|
||||
// Store uuid if we don't have it already
|
||||
// address/uuid is validated by unidentified sender certificate
|
||||
account.getRecipientTrustedResolver().resolveRecipientTrusted(content.getSender());
|
||||
|
||||
boolean handledPniSignature = false;
|
||||
if (content.getPniSignatureMessage().isPresent()) {
|
||||
final var message = content.getPniSignatureMessage().get();
|
||||
final var senderAddress = getSenderAddress(envelope, content);
|
||||
if (senderAddress != null) {
|
||||
handledPniSignature = handlePniSignatureMessage(message, senderAddress);
|
||||
}
|
||||
}
|
||||
if (!handledPniSignature) {
|
||||
account.getRecipientTrustedResolver().resolveRecipientTrusted(content.getSender());
|
||||
}
|
||||
}
|
||||
if (envelope.isReceipt()) {
|
||||
final var senderDeviceAddress = getSender(envelope, content);
|
||||
|
@ -215,8 +229,9 @@ public final class IncomingMessageHandler {
|
|||
logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
|
||||
return List.of();
|
||||
} else if (notAllowedToSendToGroup) {
|
||||
final var senderAddress = getSenderAddress(envelope, content);
|
||||
logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
|
||||
(envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(),
|
||||
senderAddress == null ? null : senderAddress.getIdentifier(),
|
||||
envelope.getTimestamp());
|
||||
return List.of();
|
||||
} else {
|
||||
|
@ -323,6 +338,32 @@ public final class IncomingMessageHandler {
|
|||
return actions;
|
||||
}
|
||||
|
||||
private boolean handlePniSignatureMessage(
|
||||
final SignalServicePniSignatureMessage message, final SignalServiceAddress senderAddress
|
||||
) {
|
||||
final var aci = ACI.from(senderAddress.getServiceId());
|
||||
final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci);
|
||||
final var pni = message.getPni();
|
||||
final var pniIdentity = account.getIdentityKeyStore().getIdentityInfo(pni);
|
||||
|
||||
if (aciIdentity == null || pniIdentity == null || aci.equals(pni)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final var verified = pniIdentity.getIdentityKey()
|
||||
.verifyAlternateIdentity(aciIdentity.getIdentityKey(), message.getSignature());
|
||||
|
||||
if (!verified) {
|
||||
logger.debug("Invalid PNI signature of ACI {} with PNI {}", aci, pni);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug("Verified association of ACI {} with PNI {}", aci, pni);
|
||||
account.getRecipientTrustedResolver()
|
||||
.resolveRecipientTrusted(Optional.of(aci), Optional.of(pni), senderAddress.getNumber());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleDecryptionErrorMessage(
|
||||
final List<HandleAction> actions,
|
||||
final RecipientId sender,
|
||||
|
@ -585,12 +626,8 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
SignalServiceAddress source;
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
|
||||
source = envelope.getSourceAddress();
|
||||
} else if (content != null) {
|
||||
source = content.getSender();
|
||||
} else {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(source);
|
||||
|
@ -608,12 +645,8 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
SignalServiceAddress source;
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
|
||||
source = envelope.getSourceAddress();
|
||||
} else if (content != null) {
|
||||
source = content.getSender();
|
||||
} else {
|
||||
SignalServiceAddress source = getSenderAddress(envelope, content);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -853,6 +886,16 @@ public final class IncomingMessageHandler {
|
|||
this.account.getProfileStore().storeProfileKey(source, profileKey);
|
||||
}
|
||||
|
||||
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
|
||||
return envelope.getSourceAddress();
|
||||
} else if (content != null) {
|
||||
return content.getSender();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
|
||||
if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) {
|
||||
return new DeviceAddress(context.getRecipientHelper().resolveRecipient(envelope.getSourceAddress()),
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.sql.SQLException;
|
|||
public class AccountDatabase extends Database {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||
private static final long DATABASE_VERSION = 10;
|
||||
private static final long DATABASE_VERSION = 11;
|
||||
|
||||
private AccountDatabase(final HikariDataSource dataSource) {
|
||||
super(logger, DATABASE_VERSION, dataSource);
|
||||
|
@ -288,5 +288,13 @@ public class AccountDatabase extends Database {
|
|||
""");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
logger.debug("Updating database: Adding pni field");
|
||||
try (final var statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
ALTER TABLE recipient ADD COLUMN pni BLOB;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ public class SignalAccount implements Closeable {
|
|||
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||
private static final int CURRENT_STORAGE_VERSION = 5;
|
||||
private static final int CURRENT_STORAGE_VERSION = 6;
|
||||
|
||||
private final Object LOCK = new Object();
|
||||
|
||||
|
@ -634,6 +634,9 @@ public class SignalAccount implements Closeable {
|
|||
migratedLegacyConfig = true;
|
||||
}
|
||||
}
|
||||
if (previousStorageVersion < 6) {
|
||||
getRecipientTrustedResolver().resolveSelfRecipientTrusted(getSelfRecipientAddress());
|
||||
}
|
||||
final var legacyAciPreKeysPath = getAciPreKeysPath(dataPath, accountPath);
|
||||
if (legacyAciPreKeysPath.exists()) {
|
||||
LegacyPreKeyStore.migrate(legacyAciPreKeysPath, getAciPreKeyStore());
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
package org.asamk.signal.manager.storage.recipients;
|
||||
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MergeRecipientHelper {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class);
|
||||
|
||||
static Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
|
||||
Store store, RecipientAddress address
|
||||
) throws SQLException {
|
||||
// address has serviceId and number, optionally also pni
|
||||
|
||||
final var recipients = store.findAllByAddress(address);
|
||||
|
||||
if (recipients.isEmpty()) {
|
||||
logger.debug("Got new recipient, serviceId, PNI and number are unknown");
|
||||
return new Pair<>(store.addNewRecipient(address), List.of());
|
||||
}
|
||||
|
||||
if (recipients.size() == 1) {
|
||||
final var recipient = recipients.stream().findFirst().get();
|
||||
if (recipient.address().hasIdentifiersOf(address)) {
|
||||
return new Pair<>(recipient.id(), List.of());
|
||||
}
|
||||
|
||||
if (recipient.address().serviceId().isEmpty() || (
|
||||
recipient.address().serviceId().equals(address.serviceId())
|
||||
) || (
|
||||
recipient.address().pni().isPresent() && recipient.address().pni().equals(address.serviceId())
|
||||
) || (
|
||||
recipient.address().serviceId().equals(address.pni())
|
||||
) || (
|
||||
address.pni().isPresent() && address.pni().equals(recipient.address().pni())
|
||||
)) {
|
||||
logger.debug("Got existing recipient {}, updating with high trust address", recipient.id());
|
||||
store.updateRecipientAddress(recipient.id(), recipient.address().withIdentifiersFrom(address));
|
||||
return new Pair<>(recipient.id(), List.of());
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"Got recipient {} existing with number/pni, but different serviceId, so stripping its number and adding new recipient",
|
||||
recipient.id());
|
||||
store.updateRecipientAddress(recipient.id(), recipient.address().removeIdentifiersFrom(address));
|
||||
|
||||
return new Pair<>(store.addNewRecipient(address), List.of());
|
||||
}
|
||||
|
||||
var resultingRecipient = recipients.stream()
|
||||
.filter(r -> r.address().serviceId().equals(address.serviceId()) || r.address()
|
||||
.pni()
|
||||
.equals(address.serviceId()))
|
||||
.findFirst();
|
||||
if (resultingRecipient.isEmpty() && address.pni().isPresent()) {
|
||||
resultingRecipient = recipients.stream().filter(r -> r.address().serviceId().equals(address.pni()) || (
|
||||
address.serviceId().equals(address.pni()) && r.address().pni().equals(address.pni())
|
||||
)).findFirst();
|
||||
}
|
||||
|
||||
final Set<RecipientWithAddress> remainingRecipients;
|
||||
if (resultingRecipient.isEmpty()) {
|
||||
remainingRecipients = recipients;
|
||||
} else {
|
||||
remainingRecipients = new HashSet<>(recipients);
|
||||
remainingRecipients.remove(resultingRecipient.get());
|
||||
}
|
||||
|
||||
final var recipientsToBeMerged = new HashSet<RecipientWithAddress>();
|
||||
final var recipientsToBeStripped = new HashSet<RecipientWithAddress>();
|
||||
for (final var recipient : remainingRecipients) {
|
||||
if (!recipient.address().hasAdditionalIdentifiersThan(address)) {
|
||||
recipientsToBeMerged.add(recipient);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipient.address().hasOnlyPniAndNumber()) {
|
||||
// PNI and phone number are linked by the server
|
||||
recipientsToBeMerged.add(recipient);
|
||||
continue;
|
||||
}
|
||||
|
||||
recipientsToBeStripped.add(recipient);
|
||||
}
|
||||
|
||||
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}) and strip ({})",
|
||||
address,
|
||||
recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")),
|
||||
recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")));
|
||||
|
||||
RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null);
|
||||
for (final var recipient : recipientsToBeMerged) {
|
||||
if (finalAddress == null) {
|
||||
finalAddress = recipient.address();
|
||||
} else {
|
||||
finalAddress = finalAddress.withIdentifiersFrom(recipient.address());
|
||||
}
|
||||
store.removeRecipientAddress(recipient.id());
|
||||
}
|
||||
if (finalAddress == null) {
|
||||
finalAddress = address;
|
||||
} else {
|
||||
finalAddress = finalAddress.withIdentifiersFrom(address);
|
||||
}
|
||||
|
||||
for (final var recipient : recipientsToBeStripped) {
|
||||
store.updateRecipientAddress(recipient.id(), recipient.address().removeIdentifiersFrom(address));
|
||||
}
|
||||
|
||||
// Create fixed RecipientIds that won't update its id after merged
|
||||
final var toBeMergedRecipientIds = recipientsToBeMerged.stream()
|
||||
.map(r -> new RecipientId(r.id().id(), null))
|
||||
.toList();
|
||||
|
||||
if (resultingRecipient.isPresent()) {
|
||||
store.updateRecipientAddress(resultingRecipient.get().id(), finalAddress);
|
||||
return new Pair<>(resultingRecipient.get().id(), toBeMergedRecipientIds);
|
||||
}
|
||||
|
||||
return new Pair<>(store.addNewRecipient(finalAddress), toBeMergedRecipientIds);
|
||||
}
|
||||
|
||||
public interface Store {
|
||||
|
||||
Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException;
|
||||
|
||||
RecipientId addNewRecipient(final RecipientAddress address) throws SQLException;
|
||||
|
||||
void updateRecipientAddress(RecipientId recipientId, final RecipientAddress address) throws SQLException;
|
||||
|
||||
void removeRecipientAddress(RecipientId recipientId) throws SQLException;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,14 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
if (serviceId.isEmpty() && pni.isPresent()) {
|
||||
serviceId = Optional.of(pni.get());
|
||||
}
|
||||
if (serviceId.isPresent() && serviceId.get() instanceof PNI sPNI) {
|
||||
if (pni.isPresent() && !sPNI.equals(pni.get())) {
|
||||
throw new AssertionError("Must not have two different PNIs!");
|
||||
}
|
||||
if (pni.isEmpty()) {
|
||||
pni = Optional.of(sPNI);
|
||||
}
|
||||
}
|
||||
if (serviceId.isEmpty() && number.isEmpty()) {
|
||||
throw new AssertionError("Must have either a ServiceId or E164 number!");
|
||||
}
|
||||
|
@ -49,6 +57,22 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
this(Optional.of(serviceId), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress withIdentifiersFrom(RecipientAddress address) {
|
||||
return new RecipientAddress((
|
||||
this.serviceId.isEmpty() || this.isServiceIdPNI() || this.serviceId.equals(address.pni)
|
||||
) && !address.isServiceIdPNI() ? address.serviceId : this.serviceId,
|
||||
address.pni.or(this::pni),
|
||||
address.number.or(this::number));
|
||||
}
|
||||
|
||||
public RecipientAddress removeIdentifiersFrom(RecipientAddress address) {
|
||||
return new RecipientAddress(address.serviceId.equals(this.serviceId) || address.pni.equals(this.serviceId)
|
||||
? Optional.empty()
|
||||
: this.serviceId,
|
||||
address.pni.equals(this.pni) || address.serviceId.equals(this.pni) ? Optional.empty() : this.pni,
|
||||
address.number.equals(this.number) ? Optional.empty() : this.number);
|
||||
}
|
||||
|
||||
public ServiceId getServiceId() {
|
||||
return serviceId.orElse(ServiceId.UNKNOWN);
|
||||
}
|
||||
|
@ -89,6 +113,42 @@ public record RecipientAddress(Optional<ServiceId> serviceId, Optional<PNI> pni,
|
|||
);
|
||||
}
|
||||
|
||||
public boolean hasSingleIdentifier() {
|
||||
return serviceId().isEmpty() || number.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasIdentifiersOf(RecipientAddress address) {
|
||||
return (address.serviceId.isEmpty() || address.serviceId.equals(serviceId) || address.serviceId.equals(pni))
|
||||
&& (address.pni.isEmpty() || address.pni.equals(pni))
|
||||
&& (address.number.isEmpty() || address.number.equals(number));
|
||||
}
|
||||
|
||||
public boolean hasAdditionalIdentifiersThan(RecipientAddress address) {
|
||||
return (
|
||||
serviceId.isPresent() && (
|
||||
address.serviceId.isEmpty() || (
|
||||
!address.serviceId.equals(serviceId) && !address.pni.equals(serviceId)
|
||||
)
|
||||
)
|
||||
) || (
|
||||
pni.isPresent() && !address.serviceId.equals(pni) && (
|
||||
address.pni.isEmpty() || !address.pni.equals(pni)
|
||||
)
|
||||
) || (
|
||||
number.isPresent() && (
|
||||
address.number.isEmpty() || !address.number.equals(number)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasOnlyPniAndNumber() {
|
||||
return pni.isPresent() && serviceId.equals(pni) && number.isPresent();
|
||||
}
|
||||
|
||||
public boolean isServiceIdPNI() {
|
||||
return serviceId.isPresent() && (pni.isPresent() && serviceId.equals(pni));
|
||||
}
|
||||
|
||||
public SignalServiceAddress toSignalServiceAddress() {
|
||||
return new SignalServiceAddress(getServiceId(), number);
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number TEXT UNIQUE,
|
||||
uuid BLOB UNIQUE,
|
||||
pni BLOB UNIQUE,
|
||||
profile_key BLOB,
|
||||
profile_key_credential BLOB,
|
||||
|
||||
|
@ -92,7 +93,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
public RecipientAddress resolveRecipientAddress(RecipientId recipientId) {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT r.number, r.uuid
|
||||
SELECT r.number, r.uuid, r.pni
|
||||
FROM %s r
|
||||
WHERE r._id = ?
|
||||
"""
|
||||
|
@ -246,7 +247,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
|
||||
) {
|
||||
final var serviceId = aci.map(a -> (ServiceId) a).or(() -> pni);
|
||||
return resolveRecipientTrusted(new RecipientAddress(serviceId, number), false);
|
||||
return resolveRecipientTrusted(new RecipientAddress(serviceId, pni, number), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -308,7 +309,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var sql = (
|
||||
"""
|
||||
SELECT r._id,
|
||||
r.number, r.uuid,
|
||||
r.number, r.uuid, r.pni,
|
||||
r.profile_key, r.profile_key_credential,
|
||||
r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived,
|
||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
|
||||
|
@ -601,21 +602,33 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private RecipientId resolveRecipientTrusted(RecipientAddress address, boolean isSelf) {
|
||||
final Pair<RecipientId, Optional<RecipientId>> pair;
|
||||
final Pair<RecipientId, List<RecipientId>> pair;
|
||||
synchronized (recipientsLock) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
pair = resolveRecipientTrustedLocked(connection, address, isSelf);
|
||||
if (address.hasSingleIdentifier() || (
|
||||
!isSelf && selfAddressProvider.getSelfAddress().matches(address)
|
||||
)) {
|
||||
pair = new Pair<>(resolveRecipientLocked(connection, address), List.of());
|
||||
} else {
|
||||
pair = MergeRecipientHelper.resolveRecipientTrustedLocked(new HelperStore(connection), address);
|
||||
|
||||
for (final var toBeMergedRecipientId : pair.second()) {
|
||||
mergeRecipientsLocked(connection, pair.first(), toBeMergedRecipientId);
|
||||
}
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update recipient store", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pair.second().isPresent()) {
|
||||
if (pair.second().size() > 0) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
recipientMergeHandler.mergeRecipients(connection, pair.first(), pair.second().get());
|
||||
deleteRecipient(connection, pair.second().get());
|
||||
for (final var toBeMergedRecipientId : pair.second()) {
|
||||
recipientMergeHandler.mergeRecipients(connection, pair.first(), toBeMergedRecipientId);
|
||||
deleteRecipient(connection, toBeMergedRecipientId);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update recipient store", e);
|
||||
}
|
||||
|
@ -623,82 +636,6 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
return pair.first();
|
||||
}
|
||||
|
||||
private Pair<RecipientId, Optional<RecipientId>> resolveRecipientTrustedLocked(
|
||||
Connection connection, RecipientAddress address, boolean isSelf
|
||||
) throws SQLException {
|
||||
if (!isSelf) {
|
||||
if (selfAddressProvider.getSelfAddress().matches(address)) {
|
||||
return new Pair<>(resolveRecipientLocked(connection, address), Optional.empty());
|
||||
}
|
||||
}
|
||||
final var byNumber = address.number().isEmpty()
|
||||
? Optional.<RecipientWithAddress>empty()
|
||||
: findByNumber(connection, address.number().get());
|
||||
final var byUuid = address.serviceId().isEmpty()
|
||||
? Optional.<RecipientWithAddress>empty()
|
||||
: findByServiceId(connection, address.serviceId().get());
|
||||
|
||||
if (byNumber.isEmpty() && byUuid.isEmpty()) {
|
||||
logger.debug("Got new recipient, both uuid and number are unknown");
|
||||
return new Pair<>(addNewRecipient(connection, address), Optional.empty());
|
||||
}
|
||||
|
||||
if (address.serviceId().isEmpty() || address.number().isEmpty() || byNumber.equals(byUuid)) {
|
||||
return new Pair<>(byUuid.or(() -> byNumber).map(RecipientWithAddress::id).get(), Optional.empty());
|
||||
}
|
||||
|
||||
if (byNumber.isEmpty()) {
|
||||
logger.debug("Got recipient {} existing with uuid, updating with high trust number", byUuid.get().id());
|
||||
updateRecipientAddress(connection, byUuid.get().id(), address);
|
||||
return new Pair<>(byUuid.get().id(), Optional.empty());
|
||||
}
|
||||
|
||||
final var byNumberRecipient = byNumber.get();
|
||||
|
||||
if (byUuid.isEmpty()) {
|
||||
if (byNumberRecipient.address().serviceId().isPresent()) {
|
||||
logger.debug(
|
||||
"Got recipient {} existing with number, but different uuid, so stripping its number and adding new recipient",
|
||||
byNumberRecipient.id());
|
||||
|
||||
updateRecipientAddress(connection,
|
||||
byNumberRecipient.id(),
|
||||
new RecipientAddress(byNumberRecipient.address().serviceId().get()));
|
||||
return new Pair<>(addNewRecipient(connection, address), Optional.empty());
|
||||
}
|
||||
|
||||
logger.debug("Got recipient {} existing with number and no uuid, updating with high trust uuid",
|
||||
byNumberRecipient.id());
|
||||
updateRecipientAddress(connection, byNumberRecipient.id(), address);
|
||||
return new Pair<>(byNumberRecipient.id(), Optional.empty());
|
||||
}
|
||||
|
||||
final var byUuidRecipient = byUuid.get();
|
||||
|
||||
if (byNumberRecipient.address().serviceId().isPresent()) {
|
||||
logger.debug(
|
||||
"Got separate recipients for high trust number {} and uuid {}, recipient for number has different uuid, so stripping its number",
|
||||
byNumberRecipient.id(),
|
||||
byUuidRecipient.id());
|
||||
|
||||
updateRecipientAddress(connection,
|
||||
byNumberRecipient.id(),
|
||||
new RecipientAddress(byNumberRecipient.address().serviceId().get()));
|
||||
updateRecipientAddress(connection, byUuidRecipient.id(), address);
|
||||
return new Pair<>(byUuidRecipient.id(), Optional.empty());
|
||||
}
|
||||
|
||||
logger.debug("Got separate recipients for high trust number {} and uuid {}, need to merge them",
|
||||
byNumberRecipient.id(),
|
||||
byUuidRecipient.id());
|
||||
// Create a fixed RecipientId that won't update its id after merge
|
||||
final var toBeMergedRecipientId = new RecipientId(byNumberRecipient.id().id(), null);
|
||||
mergeRecipientsLocked(connection, byUuidRecipient.id(), toBeMergedRecipientId);
|
||||
removeRecipientAddress(connection, toBeMergedRecipientId);
|
||||
updateRecipientAddress(connection, byUuidRecipient.id(), address);
|
||||
return new Pair<>(byUuidRecipient.id(), Optional.of(toBeMergedRecipientId));
|
||||
}
|
||||
|
||||
private RecipientId resolveRecipientLocked(
|
||||
Connection connection, RecipientAddress address
|
||||
) throws SQLException {
|
||||
|
@ -762,13 +699,14 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT INTO %s (number, uuid)
|
||||
VALUES (?, ?)
|
||||
INSERT INTO %s (number, uuid, pni)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, address.number().orElse(null));
|
||||
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setBytes(3, address.pni().map(PNI::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.executeUpdate();
|
||||
final var generatedKeys = statement.getGeneratedKeys();
|
||||
if (generatedKeys.next()) {
|
||||
|
@ -785,7 +723,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET number = NULL, uuid = NULL
|
||||
SET number = NULL, uuid = NULL, pni = NULL
|
||||
WHERE _id = ?
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
|
@ -801,14 +739,15 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET number = ?, uuid = ?
|
||||
SET number = ?, uuid = ?, pni = ?
|
||||
WHERE _id = ?
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, address.number().orElse(null));
|
||||
statement.setBytes(2, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setLong(3, recipientId.id());
|
||||
statement.setBytes(3, address.pni().map(PNI::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setLong(4, recipientId.id());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
@ -861,9 +800,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Connection connection, final String number
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
FROM %s r
|
||||
WHERE r.number = ?
|
||||
LIMIT 1
|
||||
""".formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, number);
|
||||
|
@ -875,9 +815,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
final Connection connection, final ServiceId serviceId
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
FROM %s r
|
||||
WHERE r.uuid = ?
|
||||
WHERE r.uuid = ? OR r.pni = ?
|
||||
LIMIT 1
|
||||
""".formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, UuidUtil.toByteArray(serviceId.uuid()));
|
||||
|
@ -885,6 +826,25 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
}
|
||||
|
||||
private Set<RecipientWithAddress> findAllByAddress(
|
||||
final Connection connection, final RecipientAddress address
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.uuid, r.pni
|
||||
FROM %s r
|
||||
WHERE r.uuid = ?1 OR r.pni = ?1 OR
|
||||
r.uuid = ?2 OR r.pni = ?2 OR
|
||||
r.number = ?3
|
||||
""".formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, address.serviceId().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setBytes(2, address.pni().map(ServiceId::uuid).map(UuidUtil::toByteArray).orElse(null));
|
||||
statement.setString(3, address.number().orElse(null));
|
||||
return Utils.executeQueryForStream(statement, this::getRecipientWithAddressFromResultSet)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -946,8 +906,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
|
||||
private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
final var serviceId = Optional.ofNullable(resultSet.getBytes("uuid")).map(ServiceId::parseOrNull);
|
||||
final var pni = Optional.ofNullable(resultSet.getBytes("pni")).map(PNI::parseOrNull);
|
||||
final var number = Optional.ofNullable(resultSet.getString("number"));
|
||||
return new RecipientAddress(serviceId, Optional.empty(), number);
|
||||
return new RecipientAddress(serviceId, pni, number);
|
||||
}
|
||||
|
||||
private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
|
@ -1032,5 +993,34 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
) throws SQLException;
|
||||
}
|
||||
|
||||
private record RecipientWithAddress(RecipientId id, RecipientAddress address) {}
|
||||
private class HelperStore implements MergeRecipientHelper.Store {
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
public HelperStore(final Connection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) throws SQLException {
|
||||
return RecipientStore.this.findAllByAddress(connection, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId addNewRecipient(final RecipientAddress address) throws SQLException {
|
||||
return RecipientStore.this.addNewRecipient(connection, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRecipientAddress(
|
||||
final RecipientId recipientId, final RecipientAddress address
|
||||
) throws SQLException {
|
||||
RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRecipientAddress(final RecipientId recipientId) throws SQLException {
|
||||
RecipientStore.this.removeRecipientAddress(connection, recipientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package org.asamk.signal.manager.storage.recipients;
|
||||
|
||||
record RecipientWithAddress(RecipientId id, RecipientAddress address) {}
|
|
@ -0,0 +1,330 @@
|
|||
package org.asamk.signal.manager.storage.recipients;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class MergeRecipientHelperTest {
|
||||
|
||||
static final ServiceId SERVICE_ID_A = ServiceId.from(UUID.randomUUID());
|
||||
static final ServiceId SERVICE_ID_B = ServiceId.from(UUID.randomUUID());
|
||||
static final ServiceId SERVICE_ID_C = ServiceId.from(UUID.randomUUID());
|
||||
static final PNI PNI_A = PNI.from(UUID.randomUUID());
|
||||
static final PNI PNI_B = PNI.from(UUID.randomUUID());
|
||||
static final PNI PNI_C = PNI.from(UUID.randomUUID());
|
||||
static final String NUMBER_A = "+AAA";
|
||||
static final String NUMBER_B = "+BBB";
|
||||
static final String NUMBER_C = "+CCC";
|
||||
|
||||
static final PartialAddresses ADDR_A = new PartialAddresses(SERVICE_ID_A, PNI_A, NUMBER_A);
|
||||
static final PartialAddresses ADDR_B = new PartialAddresses(SERVICE_ID_B, PNI_B, NUMBER_B);
|
||||
|
||||
static T[] testInstancesNone = new T[]{
|
||||
// 1
|
||||
new T(Set.of(), ADDR_A.FULL, Set.of(rec(1000000, ADDR_A.FULL))),
|
||||
new T(Set.of(), ADDR_A.ACI_NUM, Set.of(rec(1000000, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(), ADDR_A.ACI_PNI, Set.of(rec(1000000, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(), ADDR_A.PNI_S_NUM, Set.of(rec(1000000, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(), ADDR_A.PNI_NUM, Set.of(rec(1000000, ADDR_A.PNI_NUM))),
|
||||
};
|
||||
|
||||
static T[] testInstancesSingle = new T[]{
|
||||
// 1
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
|
||||
// 10
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.PNI), rec(1000000, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S)),
|
||||
ADDR_A.ACI_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_S), rec(1000000, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_NUM)),
|
||||
ADDR_A.ACI_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI), rec(1000000, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)),
|
||||
ADDR_A.ACI_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_S), rec(1000000, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
|
||||
// 19
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
|
||||
// 28
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(1000000, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
|
||||
// 37
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.NUM), rec(1000000, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI_PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
|
||||
new T(Set.of(rec(1, ADDR_A.FULL)), ADDR_B.FULL, Set.of(rec(1, ADDR_A.FULL), rec(1000000, ADDR_B.FULL))),
|
||||
};
|
||||
|
||||
static T[] testInstancesTwo = new T[]{
|
||||
// 1
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.FULL, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.FULL, Set.of(rec(2, ADDR_A.FULL))),
|
||||
|
||||
// 12
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.ACI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.ACI_NUM, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
|
||||
ADDR_A.ACI_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI_NUM), rec(2, ADDR_A.PNI_S))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.ACI_NUM, Set.of(rec(2, ADDR_A.FULL))),
|
||||
|
||||
// 16
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)),
|
||||
ADDR_A.PNI_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
|
||||
ADDR_A.PNI_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.PNI_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.PNI_NUM, Set.of(rec(2, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.PNI_NUM, Set.of(rec(2, ADDR_A.FULL))),
|
||||
|
||||
// 24
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_NUM), rec(2, ADDR_A.ACI))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.NUM)), ADDR_A.PNI_S_NUM, Set.of(rec(1, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)),
|
||||
ADDR_A.PNI_S_NUM,
|
||||
Set.of(rec(1, ADDR_A.PNI_S_NUM), rec(2, ADDR_A.ACI))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.PNI_S)), ADDR_A.PNI_S_NUM, Set.of(rec(2, ADDR_A.PNI_S_NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.NUM), rec(2, ADDR_A.ACI_PNI)), ADDR_A.PNI_S_NUM, Set.of(rec(2, ADDR_A.FULL))),
|
||||
|
||||
// 32
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.ACI_PNI))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI_S_NUM)),
|
||||
ADDR_A.ACI_PNI,
|
||||
Set.of(rec(1, ADDR_A.ACI_PNI), rec(2, ADDR_A.NUM))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI), rec(2, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(2, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.PNI_S), rec(2, ADDR_A.ACI_NUM)), ADDR_A.ACI_PNI, Set.of(rec(2, ADDR_A.FULL))),
|
||||
};
|
||||
|
||||
static T[] testInstancesThree = new T[]{
|
||||
// 1
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
|
||||
ADDR_A.FULL,
|
||||
Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.PNI)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
|
||||
ADDR_A.FULL,
|
||||
Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI.withIdentifiersFrom(ADDR_B.NUM)), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM)),
|
||||
ADDR_A.FULL,
|
||||
Set.of(rec(1, ADDR_A.FULL))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI), rec(3, ADDR_A.NUM.withIdentifiersFrom(ADDR_B.ACI))),
|
||||
ADDR_A.FULL,
|
||||
Set.of(rec(1, ADDR_A.FULL), rec(3, ADDR_B.ACI))),
|
||||
new T(Set.of(rec(1, ADDR_A.ACI), rec(2, ADDR_A.PNI.withIdentifiersFrom(ADDR_B.ACI)), rec(3, ADDR_A.NUM)),
|
||||
ADDR_A.FULL,
|
||||
Set.of(rec(1, ADDR_A.FULL), rec(2, ADDR_B.ACI))),
|
||||
};
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void resolveRecipientTrustedLocked_NoneExisting(T test) throws Exception {
|
||||
final var testStore = new TestStore(test.input);
|
||||
MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
|
||||
assertEquals(test.output, testStore.getRecipients());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> resolveRecipientTrustedLocked_NoneExisting() {
|
||||
return Arrays.stream(testInstancesNone).map(Arguments::of);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void resolveRecipientTrustedLocked_SingleExisting(T test) throws Exception {
|
||||
final var testStore = new TestStore(test.input);
|
||||
MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
|
||||
assertEquals(test.output, testStore.getRecipients());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> resolveRecipientTrustedLocked_SingleExisting() {
|
||||
return Arrays.stream(testInstancesSingle).map(Arguments::of);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void resolveRecipientTrustedLocked_TwoExisting(T test) throws Exception {
|
||||
final var testStore = new TestStore(test.input);
|
||||
MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
|
||||
assertEquals(test.output, testStore.getRecipients());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> resolveRecipientTrustedLocked_TwoExisting() {
|
||||
return Arrays.stream(testInstancesTwo).map(Arguments::of);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void resolveRecipientTrustedLocked_ThreeExisting(T test) throws Exception {
|
||||
final var testStore = new TestStore(test.input);
|
||||
MergeRecipientHelper.resolveRecipientTrustedLocked(testStore, test.request);
|
||||
assertEquals(test.output, testStore.getRecipients());
|
||||
}
|
||||
|
||||
private static Stream<Arguments> resolveRecipientTrustedLocked_ThreeExisting() {
|
||||
return Arrays.stream(testInstancesThree).map(Arguments::of);
|
||||
}
|
||||
|
||||
private static RecipientWithAddress rec(long recipientId, RecipientAddress address) {
|
||||
return new RecipientWithAddress(new RecipientId(recipientId, null), address);
|
||||
}
|
||||
|
||||
record T(
|
||||
Set<RecipientWithAddress> input, RecipientAddress request, Set<RecipientWithAddress> output
|
||||
) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "T{#input=%s, request=%s_%s_%s, #output=%s}".formatted(input.size(),
|
||||
request.serviceId().isPresent() ? "SVI" : "",
|
||||
request.pni().isPresent() ? "PNI" : "",
|
||||
request.number().isPresent() ? "NUM" : "",
|
||||
output.size());
|
||||
}
|
||||
}
|
||||
|
||||
static class TestStore implements MergeRecipientHelper.Store {
|
||||
|
||||
final Set<RecipientWithAddress> recipients;
|
||||
long nextRecipientId = 1000000;
|
||||
|
||||
TestStore(final Set<RecipientWithAddress> recipients) {
|
||||
this.recipients = new HashSet<>(recipients);
|
||||
}
|
||||
|
||||
public Set<RecipientWithAddress> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RecipientWithAddress> findAllByAddress(final RecipientAddress address) {
|
||||
return recipients.stream().filter(r -> r.address().matches(address)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientId addNewRecipient(final RecipientAddress address) {
|
||||
final var recipientId = new RecipientId(nextRecipientId++, null);
|
||||
recipients.add(new RecipientWithAddress(recipientId, address));
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRecipientAddress(
|
||||
final RecipientId recipientId, final RecipientAddress address
|
||||
) {
|
||||
recipients.removeIf(r -> r.id().equals(recipientId));
|
||||
recipients.add(new RecipientWithAddress(recipientId, address));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRecipientAddress(final RecipientId recipientId) {
|
||||
recipients.removeIf(r -> r.id().equals(recipientId));
|
||||
}
|
||||
}
|
||||
|
||||
private record PartialAddresses(
|
||||
RecipientAddress FULL,
|
||||
RecipientAddress ACI,
|
||||
RecipientAddress PNI,
|
||||
RecipientAddress PNI_S,
|
||||
RecipientAddress NUM,
|
||||
RecipientAddress ACI_NUM,
|
||||
RecipientAddress PNI_NUM,
|
||||
RecipientAddress PNI_S_NUM,
|
||||
RecipientAddress ACI_PNI
|
||||
) {
|
||||
|
||||
PartialAddresses(ServiceId serviceId, PNI pni, String number) {
|
||||
this(new RecipientAddress(serviceId, pni, number),
|
||||
new RecipientAddress(serviceId, null, null),
|
||||
new RecipientAddress(null, pni, null),
|
||||
new RecipientAddress(ServiceId.from(pni.uuid()), null, null),
|
||||
new RecipientAddress(null, null, number),
|
||||
new RecipientAddress(serviceId, null, number),
|
||||
new RecipientAddress(null, pni, number),
|
||||
new RecipientAddress(ServiceId.from(pni.uuid()), null, number),
|
||||
new RecipientAddress(serviceId, pni, null));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue