]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Improve source serviceId handling
[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
20 import java.io.IOException;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.Set;
27
28 import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
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 ServiceId serviceId;
54 try {
55 serviceId = getRegisteredUserByNumber(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(serviceId))
67 .toSignalServiceAddress();
68 }
69
70 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
71 final var recipientIds = new HashSet<RecipientId>(recipients.size());
72 for (var number : recipients) {
73 final var recipientId = resolveRecipient(number);
74 recipientIds.add(recipientId);
75 }
76 return recipientIds;
77 }
78
79 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
80 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
81 return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
82 } else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
83 final var number = numberRecipient.number();
84 return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
85 try {
86 return getRegisteredUserByNumber(number);
87 } catch (Exception e) {
88 return null;
89 }
90 });
91 } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
92 final var username = usernameRecipient.username();
93 return account.getRecipientStore().resolveRecipientByUsername(username, () -> {
94 try {
95 return getRegisteredUserByUsername(username);
96 } catch (Exception e) {
97 return null;
98 }
99 });
100 }
101 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
102 }
103
104 public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
105 try {
106 return Optional.of(resolveRecipient(recipient));
107 } catch (UnregisteredRecipientException e) {
108 if (recipient instanceof RecipientIdentifier.Number r) {
109 return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
110 } else {
111 return Optional.empty();
112 }
113 }
114 }
115
116 public void refreshUsers() throws IOException {
117 getRegisteredUsers(account.getRecipientStore().getAllNumbers(), false);
118 }
119
120 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
121 final var address = resolveSignalServiceAddress(recipientId);
122 if (address.getNumber().isEmpty()) {
123 return recipientId;
124 }
125 final var number = address.getNumber().get();
126 final var serviceId = getRegisteredUserByNumber(number);
127 return account.getRecipientTrustedResolver()
128 .resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
129 }
130
131 public Map<String, RegisteredUser> getRegisteredUsers(
132 final Set<String> numbers
133 ) throws IOException {
134 if (numbers.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
135 final var allNumbers = new HashSet<>(account.getRecipientStore().getAllNumbers()) {{
136 addAll(numbers);
137 }};
138 return getRegisteredUsers(allNumbers, false);
139 } else {
140 return getRegisteredUsers(numbers, true);
141 }
142 }
143
144 private Map<String, RegisteredUser> getRegisteredUsers(
145 final Set<String> numbers, final boolean isPartialRefresh
146 ) throws IOException {
147 Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh, true);
148
149 // Store numbers as recipients, so we have the number/uuid association
150 registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver()
151 .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number)));
152
153 return registeredUsers;
154 }
155
156 private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
157 final Map<String, RegisteredUser> aciMap;
158 try {
159 aciMap = getRegisteredUsers(Set.of(number), true);
160 } catch (NumberFormatException e) {
161 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
162 }
163 final var user = aciMap.get(number);
164 if (user == null) {
165 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
166 }
167 return user.getServiceId();
168 }
169
170 private Map<String, RegisteredUser> getRegisteredUsersV2(
171 final Set<String> numbers, boolean isPartialRefresh, boolean useCompat
172 ) throws IOException {
173 final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
174 final var newNumbers = new HashSet<>(numbers) {{
175 removeAll(previousNumbers);
176 }};
177 if (newNumbers.isEmpty() && previousNumbers.isEmpty()) {
178 logger.debug("No new numbers to query.");
179 return Map.of();
180 }
181 logger.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
182 newNumbers.size(),
183 previousNumbers.size(),
184 isPartialRefresh);
185 final var token = previousNumbers.isEmpty()
186 ? Optional.<byte[]>empty()
187 : Optional.ofNullable(account.getCdsiToken());
188
189 final CdsiV2Service.Response response;
190 try {
191 response = dependencies.getAccountManager()
192 .getRegisteredUsersWithCdsi(previousNumbers,
193 newNumbers,
194 account.getRecipientStore().getServiceIdToProfileKeyMap(),
195 useCompat,
196 token,
197 serviceEnvironmentConfig.cdsiMrenclave(),
198 null,
199 newToken -> {
200 if (isPartialRefresh) {
201 account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
202 // Not storing newToken for partial refresh
203 } else {
204 final var fullNumbers = new HashSet<>(previousNumbers) {{
205 addAll(newNumbers);
206 }};
207 final var seenNumbers = new HashSet<>(numbers) {{
208 addAll(newNumbers);
209 }};
210 account.getCdsiStore().updateAfterFullCdsQuery(fullNumbers, seenNumbers);
211 account.setCdsiToken(newToken);
212 account.setLastRecipientsRefresh(System.currentTimeMillis());
213 }
214 });
215 } catch (CdsiInvalidTokenException e) {
216 account.setCdsiToken(null);
217 account.getCdsiStore().clearAll();
218 throw e;
219 } catch (NumberFormatException e) {
220 throw new IOException(e);
221 }
222 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
223
224 final var registeredUsers = new HashMap<String, RegisteredUser>();
225 response.getResults()
226 .forEach((key, value) -> registeredUsers.put(key,
227 new RegisteredUser(value.getAci(), Optional.of(value.getPni()))));
228 return registeredUsers;
229 }
230
231 private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
232 return dependencies.getAccountManager().getAciByUsername(new Username(username));
233 }
234
235 public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
236
237 public RegisteredUser {
238 aci = aci.isPresent() && aci.get().isUnknown() ? Optional.empty() : aci;
239 pni = pni.isPresent() && pni.get().isUnknown() ? Optional.empty() : pni;
240 if (aci.isEmpty() && pni.isEmpty()) {
241 throw new AssertionError("Must have either a ACI or PNI!");
242 }
243 }
244
245 public ServiceId getServiceId() {
246 return aci.map(a -> (ServiceId) a).or(this::pni).orElse(null);
247 }
248 }
249 }