]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Refactor RecipientAddress
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / RecipientHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.SignalDependencies;
4 import org.asamk.signal.manager.api.RecipientIdentifier;
5 import org.asamk.signal.manager.api.UnregisteredRecipientException;
6 import org.asamk.signal.manager.config.ServiceConfig;
7 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
8 import org.asamk.signal.manager.storage.SignalAccount;
9 import org.asamk.signal.manager.storage.recipients.RecipientId;
10 import org.signal.libsignal.protocol.InvalidKeyException;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 import org.whispersystems.signalservice.api.push.ACI;
14 import org.whispersystems.signalservice.api.push.ServiceId;
15 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
16 import org.whispersystems.signalservice.api.services.CdsiV2Service;
17 import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
18 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
19 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
20
21 import java.io.IOException;
22 import java.security.SignatureException;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.Set;
29
30 public class RecipientHelper {
31
32 private final static Logger logger = LoggerFactory.getLogger(RecipientHelper.class);
33
34 private final SignalAccount account;
35 private final SignalDependencies dependencies;
36 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
37
38 public RecipientHelper(final Context context) {
39 this.account = context.getAccount();
40 this.dependencies = context.getDependencies();
41 this.serviceEnvironmentConfig = dependencies.getServiceEnvironmentConfig();
42 }
43
44 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
45 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
46 if (address.number().isEmpty() || address.serviceId().isPresent()) {
47 return address.toSignalServiceAddress();
48 }
49
50 // Address in recipient store doesn't have a uuid, this shouldn't happen
51 // Try to retrieve the uuid from the server
52 final var number = address.number().get();
53 final ACI aci;
54 try {
55 aci = getRegisteredUser(number);
56 } catch (UnregisteredRecipientException e) {
57 logger.warn("Failed to get uuid for e164 number: {}", number);
58 // Return SignalServiceAddress with unknown UUID
59 return address.toSignalServiceAddress();
60 } catch (IOException e) {
61 logger.warn("Failed to get uuid for e164 number: {}", number, e);
62 // Return SignalServiceAddress with unknown UUID
63 return address.toSignalServiceAddress();
64 }
65 return account.getRecipientAddressResolver()
66 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(aci))
67 .toSignalServiceAddress();
68 }
69
70 public RecipientId resolveRecipient(final SignalServiceAddress address) {
71 return account.getRecipientResolver().resolveRecipient(address);
72 }
73
74 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
75 final var recipientIds = new HashSet<RecipientId>(recipients.size());
76 for (var number : recipients) {
77 final var recipientId = resolveRecipient(number);
78 recipientIds.add(recipientId);
79 }
80 return recipientIds;
81 }
82
83 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
84 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
85 return account.getRecipientResolver().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
86 } else {
87 final var number = ((RecipientIdentifier.Number) recipient).number();
88 return account.getRecipientStore().resolveRecipient(number, () -> {
89 try {
90 return getRegisteredUser(number);
91 } catch (Exception e) {
92 return null;
93 }
94 });
95 }
96 }
97
98 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
99 final var address = resolveSignalServiceAddress(recipientId);
100 if (address.getNumber().isEmpty()) {
101 return recipientId;
102 }
103 final var number = address.getNumber().get();
104 final var uuid = getRegisteredUser(number);
105 return account.getRecipientTrustedResolver().resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
106 }
107
108 public Map<String, ACI> getRegisteredUsers(final Set<String> numbers) throws IOException {
109 Map<String, ACI> registeredUsers;
110 try {
111 registeredUsers = getRegisteredUsersV2(numbers, true);
112 } catch (IOException e) {
113 logger.warn("CDSI request failed, trying fallback to CDS", e);
114 registeredUsers = getRegisteredUsersV1(numbers);
115 }
116
117 // Store numbers as recipients, so we have the number/uuid association
118 registeredUsers.forEach((number, aci) -> account.getRecipientTrustedResolver()
119 .resolveRecipientTrusted(new SignalServiceAddress(aci, number)));
120
121 return registeredUsers;
122 }
123
124 private ACI getRegisteredUser(final String number) throws IOException, UnregisteredRecipientException {
125 final Map<String, ACI> aciMap;
126 try {
127 aciMap = getRegisteredUsers(Set.of(number));
128 } catch (NumberFormatException e) {
129 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
130 }
131 final var uuid = aciMap.get(number);
132 if (uuid == null) {
133 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
134 }
135 return uuid;
136 }
137
138 private Map<String, ACI> getRegisteredUsersV1(final Set<String> numbers) throws IOException {
139 final Map<String, ACI> registeredUsers;
140 try {
141 registeredUsers = dependencies.getAccountManager()
142 .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
143 numbers,
144 serviceEnvironmentConfig.getCdsMrenclave());
145 } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException |
146 UnauthenticatedResponseException | InvalidKeyException | NumberFormatException e) {
147 throw new IOException(e);
148 }
149 return registeredUsers;
150 }
151
152 private Map<String, ACI> getRegisteredUsersV2(final Set<String> numbers, boolean useCompat) throws IOException {
153 // Only partial refresh is implemented here
154 final CdsiV2Service.Response response;
155 try {
156 response = dependencies.getAccountManager()
157 .getRegisteredUsersWithCdsi(Set.of(),
158 numbers,
159 account.getRecipientStore().getServiceIdToProfileKeyMap(),
160 useCompat,
161 Optional.empty(),
162 serviceEnvironmentConfig.getCdsiMrenclave(),
163 token -> {
164 // Not storing for partial refresh
165 });
166 } catch (NumberFormatException e) {
167 throw new IOException(e);
168 }
169 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
170
171 final var registeredUsers = new HashMap<String, ACI>();
172 response.getResults().forEach((key, value) -> {
173 if (value.getAci().isPresent()) {
174 registeredUsers.put(key, value.getAci().get());
175 }
176 });
177 return registeredUsers;
178 }
179
180 private ACI getRegisteredUserByUsername(String username) throws IOException {
181 return dependencies.getAccountManager().getAciByUsername(username);
182 }
183 }