1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
4 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
5 import org
.asamk
.signal
.manager
.api
.UsernameLinkUrl
;
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
.cds
.CdsiV2Service
;
14 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
15 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
16 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
17 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
18 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.CdsiInvalidArgumentException
;
19 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.CdsiInvalidTokenException
;
20 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
22 import java
.io
.IOException
;
23 import java
.util
.Collection
;
24 import java
.util
.HashMap
;
25 import java
.util
.HashSet
;
27 import java
.util
.Optional
;
29 import java
.util
.UUID
;
31 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.MAXIMUM_ONE_OFF_REQUEST_SIZE
;
32 import static org
.asamk
.signal
.manager
.util
.Utils
.handleResponseException
;
34 public class RecipientHelper
{
36 private static final Logger logger
= LoggerFactory
.getLogger(RecipientHelper
.class);
38 private final SignalAccount account
;
39 private final SignalDependencies dependencies
;
41 public RecipientHelper(final Context context
) {
42 this.account
= context
.getAccount();
43 this.dependencies
= context
.getDependencies();
46 public SignalServiceAddress
resolveSignalServiceAddress(RecipientId recipientId
) {
47 final var address
= account
.getRecipientAddressResolver().resolveRecipientAddress(recipientId
);
48 if (address
.number().isEmpty() || address
.serviceId().isPresent()) {
49 return address
.toSignalServiceAddress();
52 // Address in recipient store doesn't have a uuid, this shouldn't happen
53 // Try to retrieve the uuid from the server
54 final var number
= address
.number().get();
55 final ServiceId serviceId
;
57 serviceId
= getRegisteredUserByNumber(number
);
58 } catch (UnregisteredRecipientException e
) {
59 logger
.warn("Failed to get uuid for e164 number: {}", number
);
60 // Return SignalServiceAddress with unknown UUID
61 return address
.toSignalServiceAddress();
62 } catch (IOException e
) {
63 logger
.warn("Failed to get uuid for e164 number: {}", number
, e
);
64 // Return SignalServiceAddress with unknown UUID
65 return address
.toSignalServiceAddress();
67 return account
.getRecipientAddressResolver()
68 .resolveRecipientAddress(account
.getRecipientResolver().resolveRecipient(serviceId
))
69 .toSignalServiceAddress();
72 public Set
<RecipientId
> resolveRecipients(Collection
<RecipientIdentifier
.Single
> recipients
) throws UnregisteredRecipientException
, IOException
{
73 final var recipientIds
= new HashSet
<RecipientId
>(recipients
.size());
74 for (var number
: recipients
) {
75 final var recipientId
= resolveRecipient(number
);
76 recipientIds
.add(recipientId
);
81 public RecipientId
resolveRecipient(final RecipientIdentifier
.Single recipient
) throws UnregisteredRecipientException
{
82 if (recipient
instanceof RecipientIdentifier
.Uuid(UUID uuid
)) {
83 return account
.getRecipientResolver().resolveRecipient(ACI
.from(uuid
));
84 } else if (recipient
instanceof RecipientIdentifier
.Pni(UUID pni
)) {
85 return account
.getRecipientResolver().resolveRecipient(PNI
.from(pni
));
86 } else if (recipient
instanceof RecipientIdentifier
.Number(String number
)) {
87 return account
.getRecipientStore().resolveRecipientByNumber(number
, () -> {
89 return getRegisteredUserByNumber(number
);
90 } catch (Exception e
) {
94 } else if (recipient
instanceof RecipientIdentifier
.Username(String username
)) {
96 return resolveRecipientByUsernameOrLink(username
, false);
97 } catch (Exception e
) {
101 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient
);
104 public RecipientId
resolveRecipientByUsernameOrLink(
107 ) throws UnregisteredRecipientException
, IOException
{
108 final Username finalUsername
;
110 finalUsername
= getUsernameFromUsernameOrLink(username
);
111 } catch (IOException
| BaseUsernameException e
) {
112 throw new RuntimeException(e
);
116 final var aci
= handleResponseException(dependencies
.getUsernameApi().getAciByUsername(finalUsername
));
117 return account
.getRecipientStore().resolveRecipientTrusted(aci
, finalUsername
.getUsername());
118 } catch (NonSuccessfulResponseCodeException e
) {
120 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null,
125 logger
.debug("Failed to get uuid for username: {}", username
, e
);
129 return account
.getRecipientStore().resolveRecipientByUsername(finalUsername
.getUsername(), () -> {
131 return handleResponseException(dependencies
.getUsernameApi().getAciByUsername(finalUsername
));
132 } catch (Exception e
) {
138 private Username
getUsernameFromUsernameOrLink(String username
) throws BaseUsernameException
, IOException
{
140 final var usernameLinkUrl
= UsernameLinkUrl
.fromUri(username
);
141 final var components
= usernameLinkUrl
.getComponents();
142 final var encryptedUsername
= handleResponseException(dependencies
.getUsernameApi()
143 .getEncryptedUsernameFromLinkServerId(components
.getServerId()));
144 final var link
= new Username
.UsernameLink(components
.getEntropy(), encryptedUsername
);
146 return Username
.fromLink(link
);
147 } catch (UsernameLinkUrl
.InvalidUsernameLinkException e
) {
148 return new Username(username
);
152 public Optional
<RecipientId
> resolveRecipientOptional(final RecipientIdentifier
.Single recipient
) {
154 return Optional
.of(resolveRecipient(recipient
));
155 } catch (UnregisteredRecipientException e
) {
156 if (recipient
instanceof RecipientIdentifier
.Number(String number
)) {
157 return account
.getRecipientStore().resolveRecipientByNumberOptional(number
);
159 return Optional
.empty();
164 public void refreshUsers() throws IOException
{
165 getRegisteredUsers(account
.getRecipientStore().getAllNumbers(), false);
168 public RecipientId
refreshRegisteredUser(RecipientId recipientId
) throws IOException
, UnregisteredRecipientException
{
169 final var address
= resolveSignalServiceAddress(recipientId
);
170 if (address
.getNumber().isEmpty()) {
173 final var number
= address
.getNumber().get();
174 final var serviceId
= getRegisteredUserByNumber(number
);
175 return account
.getRecipientTrustedResolver()
176 .resolveRecipientTrusted(new SignalServiceAddress(serviceId
, number
));
179 public Map
<String
, RegisteredUser
> getRegisteredUsers(
180 final Set
<String
> numbers
181 ) throws IOException
{
182 if (numbers
.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE
) {
183 final var allNumbers
= new HashSet
<>(account
.getRecipientStore().getAllNumbers()) {{
186 return getRegisteredUsers(allNumbers
, false);
188 return getRegisteredUsers(numbers
, true);
192 private Map
<String
, RegisteredUser
> getRegisteredUsers(
193 final Set
<String
> numbers
,
194 final boolean isPartialRefresh
195 ) throws IOException
{
196 Map
<String
, RegisteredUser
> registeredUsers
= getRegisteredUsersV2(numbers
, isPartialRefresh
);
198 // Store numbers as recipients, so we have the number/uuid association
199 registeredUsers
.forEach((number
, u
) -> account
.getRecipientTrustedResolver()
200 .resolveRecipientTrusted(u
.aci
, u
.pni
, Optional
.of(number
)));
202 final var unregisteredUsers
= new HashSet
<>(numbers
);
203 unregisteredUsers
.removeAll(registeredUsers
.keySet());
204 account
.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers
);
205 account
.getRecipientStore().markDiscoverable(registeredUsers
.keySet());
207 return registeredUsers
;
210 private ServiceId
getRegisteredUserByNumber(final String number
) throws IOException
, UnregisteredRecipientException
{
211 final Map
<String
, RegisteredUser
> aciMap
;
213 aciMap
= getRegisteredUsers(Set
.of(number
), true);
214 } catch (NumberFormatException e
) {
215 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(number
));
217 final var user
= aciMap
.get(number
);
219 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(number
));
221 return user
.getServiceId();
224 private Map
<String
, RegisteredUser
> getRegisteredUsersV2(
225 final Set
<String
> numbers
,
226 boolean isPartialRefresh
227 ) throws IOException
{
228 final var previousNumbers
= isPartialRefresh ? Set
.<String
>of() : account
.getCdsiStore().getAllNumbers();
229 final var newNumbers
= new HashSet
<>(numbers
) {{
230 removeAll(previousNumbers
);
232 if (newNumbers
.isEmpty() && previousNumbers
.isEmpty()) {
233 logger
.debug("No new numbers to query.");
236 logger
.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
238 previousNumbers
.size(),
240 final var token
= previousNumbers
.isEmpty()
241 ? Optional
.<byte[]>empty()
242 : Optional
.ofNullable(account
.getCdsiToken());
244 final CdsiV2Service
.Response response
;
246 response
= handleResponseException(dependencies
.getCdsApi()
247 .getRegisteredUsers(token
.isEmpty() ? Set
.of() : previousNumbers
,
249 account
.getRecipientStore().getServiceIdToProfileKeyMap(),
252 dependencies
.getLibSignalNetwork(),
254 if (isPartialRefresh
) {
255 account
.getCdsiStore().updateAfterPartialCdsQuery(newNumbers
);
256 // Not storing newToken for partial refresh
258 final var fullNumbers
= new HashSet
<>(previousNumbers
) {{
261 final var seenNumbers
= new HashSet
<>(numbers
) {{
264 account
.getCdsiStore().updateAfterFullCdsQuery(fullNumbers
, seenNumbers
);
265 account
.setCdsiToken(newToken
);
266 account
.setLastRecipientsRefresh(System
.currentTimeMillis());
269 } catch (CdsiInvalidTokenException
| CdsiInvalidArgumentException e
) {
270 account
.setCdsiToken(null);
271 account
.getCdsiStore().clearAll();
273 } catch (NumberFormatException e
) {
274 throw new IOException(e
);
276 logger
.debug("CDSI request successful, quota used by this request: {}", response
.getQuotaUsedDebugOnly());
278 final var registeredUsers
= new HashMap
<String
, RegisteredUser
>();
279 response
.getResults()
280 .forEach((key
, value
) -> registeredUsers
.put(key
,
281 new RegisteredUser(value
.getAci(), Optional
.of(value
.getPni()))));
282 return registeredUsers
;
285 public record RegisteredUser(Optional
<ACI
> aci
, Optional
<PNI
> pni
) {
287 public RegisteredUser
{
288 aci
= aci
.isPresent() && aci
.get().isUnknown() ? Optional
.empty() : aci
;
289 pni
= pni
.isPresent() && pni
.get().isUnknown() ? Optional
.empty() : pni
;
290 if (aci
.isEmpty() && pni
.isEmpty()) {
291 throw new AssertionError("Must have either a ACI or PNI!");
295 public ServiceId
getServiceId() {
296 return aci
.map(a
-> (ServiceId
) a
).or(this::pni
).orElse(null);