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