]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
133aab5bd83ae0e28de2983cc41f18a8c7ccd522
[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.api.RecipientIdentifier;
4 import org.asamk.signal.manager.api.UnregisteredRecipientException;
5 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
6 import org.asamk.signal.manager.internal.SignalDependencies;
7 import org.asamk.signal.manager.storage.SignalAccount;
8 import org.asamk.signal.manager.storage.recipients.RecipientId;
9 import org.signal.libsignal.usernames.BaseUsernameException;
10 import org.signal.libsignal.usernames.Username;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 import org.whispersystems.signalservice.api.push.ServiceId;
14 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
15 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
16 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
17 import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
18 import org.whispersystems.signalservice.api.services.CdsiV2Service;
19 import org.whispersystems.util.Base64UrlSafe;
20
21 import java.io.IOException;
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.Set;
28
29 import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
30
31 public class RecipientHelper {
32
33 private final static Logger logger = LoggerFactory.getLogger(RecipientHelper.class);
34
35 private final SignalAccount account;
36 private final SignalDependencies dependencies;
37 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
38
39 public RecipientHelper(final Context context) {
40 this.account = context.getAccount();
41 this.dependencies = context.getDependencies();
42 this.serviceEnvironmentConfig = dependencies.getServiceEnvironmentConfig();
43 }
44
45 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
46 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
47 if (address.number().isEmpty() || address.serviceId().isPresent()) {
48 return address.toSignalServiceAddress();
49 }
50
51 // Address in recipient store doesn't have a uuid, this shouldn't happen
52 // Try to retrieve the uuid from the server
53 final var number = address.number().get();
54 final ServiceId serviceId;
55 try {
56 serviceId = getRegisteredUserByNumber(number);
57 } catch (UnregisteredRecipientException e) {
58 logger.warn("Failed to get uuid for e164 number: {}", number);
59 // Return SignalServiceAddress with unknown UUID
60 return address.toSignalServiceAddress();
61 } catch (IOException e) {
62 logger.warn("Failed to get uuid for e164 number: {}", number, e);
63 // Return SignalServiceAddress with unknown UUID
64 return address.toSignalServiceAddress();
65 }
66 return account.getRecipientAddressResolver()
67 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId))
68 .toSignalServiceAddress();
69 }
70
71 public RecipientId resolveRecipient(final SignalServiceAddress address) {
72 return account.getRecipientResolver().resolveRecipient(address);
73 }
74
75 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
76 final var recipientIds = new HashSet<RecipientId>(recipients.size());
77 for (var number : recipients) {
78 final var recipientId = resolveRecipient(number);
79 recipientIds.add(recipientId);
80 }
81 return recipientIds;
82 }
83
84 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
85 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
86 return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
87 } else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
88 final var number = numberRecipient.number();
89 return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
90 try {
91 return getRegisteredUserByNumber(number);
92 } catch (Exception e) {
93 return null;
94 }
95 });
96 } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
97 final var username = usernameRecipient.username();
98 return account.getRecipientStore().resolveRecipientByUsername(username, () -> {
99 try {
100 return getRegisteredUserByUsername(username);
101 } catch (Exception e) {
102 return null;
103 }
104 });
105 }
106 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
107 }
108
109 public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
110 try {
111 return Optional.of(resolveRecipient(recipient));
112 } catch (UnregisteredRecipientException e) {
113 if (recipient instanceof RecipientIdentifier.Number r) {
114 return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
115 } else {
116 return Optional.empty();
117 }
118 }
119 }
120
121 public void refreshUsers() throws IOException {
122 getRegisteredUsers(account.getRecipientStore().getAllNumbers(), false);
123 }
124
125 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
126 final var address = resolveSignalServiceAddress(recipientId);
127 if (address.getNumber().isEmpty()) {
128 return recipientId;
129 }
130 final var number = address.getNumber().get();
131 final var serviceId = getRegisteredUserByNumber(number);
132 return account.getRecipientTrustedResolver()
133 .resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
134 }
135
136 public Map<String, RegisteredUser> getRegisteredUsers(
137 final Set<String> numbers
138 ) throws IOException {
139 if (numbers.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
140 final var allNumbers = new HashSet<>(account.getRecipientStore().getAllNumbers()) {{
141 addAll(numbers);
142 }};
143 return getRegisteredUsers(allNumbers, false);
144 } else {
145 return getRegisteredUsers(numbers, true);
146 }
147 }
148
149 private Map<String, RegisteredUser> getRegisteredUsers(
150 final Set<String> numbers, final boolean isPartialRefresh
151 ) throws IOException {
152 Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh, true);
153
154 // Store numbers as recipients, so we have the number/uuid association
155 registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver()
156 .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number)));
157
158 return registeredUsers;
159 }
160
161 private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
162 final Map<String, RegisteredUser> aciMap;
163 try {
164 aciMap = getRegisteredUsers(Set.of(number), true);
165 } catch (NumberFormatException e) {
166 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
167 }
168 final var user = aciMap.get(number);
169 if (user == null) {
170 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
171 }
172 return user.getServiceId();
173 }
174
175 private Map<String, RegisteredUser> getRegisteredUsersV2(
176 final Set<String> numbers, boolean isPartialRefresh, boolean useCompat
177 ) throws IOException {
178 final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
179 final var newNumbers = new HashSet<>(numbers) {{
180 removeAll(previousNumbers);
181 }};
182 if (newNumbers.isEmpty() && previousNumbers.isEmpty()) {
183 logger.debug("No new numbers to query.");
184 return Map.of();
185 }
186 logger.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
187 newNumbers.size(),
188 previousNumbers.size(),
189 isPartialRefresh);
190 final var token = previousNumbers.isEmpty()
191 ? Optional.<byte[]>empty()
192 : Optional.ofNullable(account.getCdsiToken());
193
194 final CdsiV2Service.Response response;
195 try {
196 response = dependencies.getAccountManager()
197 .getRegisteredUsersWithCdsi(previousNumbers,
198 newNumbers,
199 account.getRecipientStore().getServiceIdToProfileKeyMap(),
200 useCompat,
201 token,
202 serviceEnvironmentConfig.cdsiMrenclave(),
203 null,
204 newToken -> {
205 if (isPartialRefresh) {
206 account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
207 // Not storing newToken for partial refresh
208 } else {
209 final var fullNumbers = new HashSet<>(previousNumbers) {{
210 addAll(newNumbers);
211 }};
212 final var seenNumbers = new HashSet<>(numbers) {{
213 addAll(newNumbers);
214 }};
215 account.getCdsiStore().updateAfterFullCdsQuery(fullNumbers, seenNumbers);
216 account.setCdsiToken(newToken);
217 }
218 });
219 } catch (CdsiInvalidTokenException e) {
220 account.setCdsiToken(null);
221 account.getCdsiStore().clearAll();
222 throw e;
223 } catch (NumberFormatException e) {
224 throw new IOException(e);
225 }
226 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
227
228 final var registeredUsers = new HashMap<String, RegisteredUser>();
229 response.getResults()
230 .forEach((key, value) -> registeredUsers.put(key,
231 new RegisteredUser(value.getAci(), Optional.of(value.getPni()))));
232 return registeredUsers;
233 }
234
235 private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
236 return dependencies.getAccountManager()
237 .getAciByUsernameHash(Base64UrlSafe.encodeBytesWithoutPadding(new Username(username).getHash()));
238 }
239
240 public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
241
242 public RegisteredUser {
243 aci = aci.isPresent() && aci.get().isUnknown() ? Optional.empty() : aci;
244 pni = pni.isPresent() && pni.get().isUnknown() ? Optional.empty() : pni;
245 if (aci.isEmpty() && pni.isEmpty()) {
246 throw new AssertionError("Must have either a ACI or PNI!");
247 }
248 }
249
250 public ServiceId getServiceId() {
251 return aci.map(a -> (ServiceId) a).or(this::pni).orElse(null);
252 }
253 }
254 }