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
.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
;
20 import java
.io
.IOException
;
21 import java
.util
.Collection
;
22 import java
.util
.HashMap
;
23 import java
.util
.HashSet
;
25 import java
.util
.Optional
;
28 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.MAXIMUM_ONE_OFF_REQUEST_SIZE
;
30 public class RecipientHelper
{
32 private static final Logger logger
= LoggerFactory
.getLogger(RecipientHelper
.class);
34 private final SignalAccount account
;
35 private final SignalDependencies dependencies
;
36 private final ServiceEnvironmentConfig serviceEnvironmentConfig
;
38 public RecipientHelper(final Context context
) {
39 this.account
= context
.getAccount();
40 this.dependencies
= context
.getDependencies();
41 this.serviceEnvironmentConfig
= dependencies
.getServiceEnvironmentConfig();
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();
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
;
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();
65 return account
.getRecipientAddressResolver()
66 .resolveRecipientAddress(account
.getRecipientResolver().resolveRecipient(serviceId
))
67 .toSignalServiceAddress();
70 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
71 return account
.getRecipientResolver().resolveRecipient(address
);
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
);
83 public RecipientId
resolveRecipient(final RecipientIdentifier
.Single recipient
) throws UnregisteredRecipientException
{
84 if (recipient
instanceof RecipientIdentifier
.Uuid uuidRecipient
) {
85 return account
.getRecipientResolver().resolveRecipient(ACI
.from(uuidRecipient
.uuid()));
86 } else if (recipient
instanceof RecipientIdentifier
.Number numberRecipient
) {
87 final var number
= numberRecipient
.number();
88 return account
.getRecipientStore().resolveRecipientByNumber(number
, () -> {
90 return getRegisteredUserByNumber(number
);
91 } catch (Exception e
) {
95 } else if (recipient
instanceof RecipientIdentifier
.Username usernameRecipient
) {
96 final var username
= usernameRecipient
.username();
97 return account
.getRecipientStore().resolveRecipientByUsername(username
, () -> {
99 return getRegisteredUserByUsername(username
);
100 } catch (Exception e
) {
105 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient
);
108 public Optional
<RecipientId
> resolveRecipientOptional(final RecipientIdentifier
.Single recipient
) {
110 return Optional
.of(resolveRecipient(recipient
));
111 } catch (UnregisteredRecipientException e
) {
112 if (recipient
instanceof RecipientIdentifier
.Number r
) {
113 return account
.getRecipientStore().resolveRecipientByNumberOptional(r
.number());
115 return Optional
.empty();
120 public void refreshUsers() throws IOException
{
121 getRegisteredUsers(account
.getRecipientStore().getAllNumbers(), false);
124 public RecipientId
refreshRegisteredUser(RecipientId recipientId
) throws IOException
, UnregisteredRecipientException
{
125 final var address
= resolveSignalServiceAddress(recipientId
);
126 if (address
.getNumber().isEmpty()) {
129 final var number
= address
.getNumber().get();
130 final var serviceId
= getRegisteredUserByNumber(number
);
131 return account
.getRecipientTrustedResolver()
132 .resolveRecipientTrusted(new SignalServiceAddress(serviceId
, number
));
135 public Map
<String
, RegisteredUser
> getRegisteredUsers(
136 final Set
<String
> numbers
137 ) throws IOException
{
138 if (numbers
.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE
) {
139 final var allNumbers
= new HashSet
<>(account
.getRecipientStore().getAllNumbers()) {{
142 return getRegisteredUsers(allNumbers
, false);
144 return getRegisteredUsers(numbers
, true);
148 private Map
<String
, RegisteredUser
> getRegisteredUsers(
149 final Set
<String
> numbers
, final boolean isPartialRefresh
150 ) throws IOException
{
151 Map
<String
, RegisteredUser
> registeredUsers
= getRegisteredUsersV2(numbers
, isPartialRefresh
, true);
153 // Store numbers as recipients, so we have the number/uuid association
154 registeredUsers
.forEach((number
, u
) -> account
.getRecipientTrustedResolver()
155 .resolveRecipientTrusted(u
.aci
, u
.pni
, Optional
.of(number
)));
157 return registeredUsers
;
160 private ServiceId
getRegisteredUserByNumber(final String number
) throws IOException
, UnregisteredRecipientException
{
161 final Map
<String
, RegisteredUser
> aciMap
;
163 aciMap
= getRegisteredUsers(Set
.of(number
), true);
164 } catch (NumberFormatException e
) {
165 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null, number
));
167 final var user
= aciMap
.get(number
);
169 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null, number
));
171 return user
.getServiceId();
174 private Map
<String
, RegisteredUser
> getRegisteredUsersV2(
175 final Set
<String
> numbers
, boolean isPartialRefresh
, boolean useCompat
176 ) throws IOException
{
177 final var previousNumbers
= isPartialRefresh ? Set
.<String
>of() : account
.getCdsiStore().getAllNumbers();
178 final var newNumbers
= new HashSet
<>(numbers
) {{
179 removeAll(previousNumbers
);
181 if (newNumbers
.isEmpty() && previousNumbers
.isEmpty()) {
182 logger
.debug("No new numbers to query.");
185 logger
.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
187 previousNumbers
.size(),
189 final var token
= previousNumbers
.isEmpty()
190 ? Optional
.<byte[]>empty()
191 : Optional
.ofNullable(account
.getCdsiToken());
193 final CdsiV2Service
.Response response
;
195 response
= dependencies
.getAccountManager()
196 .getRegisteredUsersWithCdsi(previousNumbers
,
198 account
.getRecipientStore().getServiceIdToProfileKeyMap(),
201 serviceEnvironmentConfig
.cdsiMrenclave(),
204 if (isPartialRefresh
) {
205 account
.getCdsiStore().updateAfterPartialCdsQuery(newNumbers
);
206 // Not storing newToken for partial refresh
208 final var fullNumbers
= new HashSet
<>(previousNumbers
) {{
211 final var seenNumbers
= new HashSet
<>(numbers
) {{
214 account
.getCdsiStore().updateAfterFullCdsQuery(fullNumbers
, seenNumbers
);
215 account
.setCdsiToken(newToken
);
216 account
.setLastRecipientsRefresh(System
.currentTimeMillis());
219 } catch (CdsiInvalidTokenException e
) {
220 account
.setCdsiToken(null);
221 account
.getCdsiStore().clearAll();
223 } catch (NumberFormatException e
) {
224 throw new IOException(e
);
226 logger
.debug("CDSI request successful, quota used by this request: {}", response
.getQuotaUsedDebugOnly());
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
;
235 private ACI
getRegisteredUserByUsername(String username
) throws IOException
, BaseUsernameException
{
236 return dependencies
.getAccountManager().getAciByUsername(new Username(username
));
239 public record RegisteredUser(Optional
<ACI
> aci
, Optional
<PNI
> pni
) {
241 public RegisteredUser
{
242 aci
= aci
.isPresent() && aci
.get().isUnknown() ? Optional
.empty() : aci
;
243 pni
= pni
.isPresent() && pni
.get().isUnknown() ? Optional
.empty() : pni
;
244 if (aci
.isEmpty() && pni
.isEmpty()) {
245 throw new AssertionError("Must have either a ACI or PNI!");
249 public ServiceId
getServiceId() {
250 return aci
.map(a
-> (ServiceId
) a
).or(this::pni
).orElse(null);