]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
Refactor Context to create helpers lazily
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / GroupV2Helper.java
1 package org.asamk.signal.manager.helper;
2
3 import com.google.protobuf.InvalidProtocolBufferException;
4
5 import org.asamk.signal.manager.SignalDependencies;
6 import org.asamk.signal.manager.api.Pair;
7 import org.asamk.signal.manager.groups.GroupLinkPassword;
8 import org.asamk.signal.manager.groups.GroupLinkState;
9 import org.asamk.signal.manager.groups.GroupPermission;
10 import org.asamk.signal.manager.groups.GroupUtils;
11 import org.asamk.signal.manager.groups.NotAGroupMemberException;
12 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
13 import org.asamk.signal.manager.storage.recipients.Profile;
14 import org.asamk.signal.manager.storage.recipients.RecipientId;
15 import org.asamk.signal.manager.util.IOUtils;
16 import org.asamk.signal.manager.util.Utils;
17 import org.signal.storageservice.protos.groups.AccessControl;
18 import org.signal.storageservice.protos.groups.GroupChange;
19 import org.signal.storageservice.protos.groups.Member;
20 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
21 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
22 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
23 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
24 import org.signal.zkgroup.InvalidInputException;
25 import org.signal.zkgroup.VerificationFailedException;
26 import org.signal.zkgroup.auth.AuthCredentialResponse;
27 import org.signal.zkgroup.groups.GroupMasterKey;
28 import org.signal.zkgroup.groups.GroupSecretParams;
29 import org.signal.zkgroup.groups.UuidCiphertext;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.whispersystems.libsignal.util.guava.Optional;
33 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
34 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
35 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
36 import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
37 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
38 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
39 import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
40 import org.whispersystems.signalservice.api.push.ACI;
41 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
42 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
43
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.Set;
51 import java.util.UUID;
52 import java.util.concurrent.TimeUnit;
53 import java.util.stream.Collectors;
54
55 public class GroupV2Helper {
56
57 private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class);
58
59 private final SignalDependencies dependencies;
60 private final Context context;
61
62 private HashMap<Integer, AuthCredentialResponse> groupApiCredentials;
63
64 public GroupV2Helper(final Context context) {
65 this.dependencies = context.getDependencies();
66 this.context = context;
67 }
68
69 public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
70 try {
71 final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
72 return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
73 } catch (NonSuccessfulResponseCodeException e) {
74 if (e.getCode() == 403) {
75 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
76 }
77 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
78 return null;
79 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
80 logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
81 return null;
82 }
83 }
84
85 public DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
86 GroupMasterKey groupMasterKey, GroupLinkPassword password
87 ) throws IOException, GroupLinkNotActiveException {
88 var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
89
90 return dependencies.getGroupsV2Api()
91 .getGroupJoinInfo(groupSecretParams,
92 Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
93 getGroupAuthForToday(groupSecretParams));
94 }
95
96 public Pair<GroupInfoV2, DecryptedGroup> createGroup(
97 String name, Set<RecipientId> members, File avatarFile
98 ) throws IOException {
99 final var avatarBytes = readAvatarBytes(avatarFile);
100 final var newGroup = buildNewGroup(name, members, avatarBytes);
101 if (newGroup == null) {
102 return null;
103 }
104
105 final var groupSecretParams = newGroup.getGroupSecretParams();
106
107 final GroupsV2AuthorizationString groupAuthForToday;
108 final DecryptedGroup decryptedGroup;
109 try {
110 groupAuthForToday = getGroupAuthForToday(groupSecretParams);
111 dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday);
112 decryptedGroup = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
113 } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
114 logger.warn("Failed to create V2 group: {}", e.getMessage());
115 return null;
116 }
117 if (decryptedGroup == null) {
118 logger.warn("Failed to create V2 group, unknown error!");
119 return null;
120 }
121
122 final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
123 final var masterKey = groupSecretParams.getMasterKey();
124 var g = new GroupInfoV2(groupId, masterKey);
125
126 return new Pair<>(g, decryptedGroup);
127 }
128
129 private byte[] readAvatarBytes(final File avatarFile) throws IOException {
130 final byte[] avatarBytes;
131 try (InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile)) {
132 avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
133 }
134 return avatarBytes;
135 }
136
137 private GroupsV2Operations.NewGroup buildNewGroup(
138 String name, Set<RecipientId> members, byte[] avatar
139 ) {
140 final var profileKeyCredential = context.getProfileHelper()
141 .getRecipientProfileKeyCredential(context.getAccount().getSelfRecipientId());
142 if (profileKeyCredential == null) {
143 logger.warn("Cannot create a V2 group as self does not have a versioned profile");
144 return null;
145 }
146
147 if (!areMembersValid(members)) return null;
148
149 final var self = new GroupCandidate(getSelfAci().uuid(), Optional.fromNullable(profileKeyCredential));
150 final var memberList = new ArrayList<>(members);
151 final var credentials = context.getProfileHelper().getRecipientProfileKeyCredential(memberList).stream();
152 final var uuids = memberList.stream()
153 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getAci().uuid());
154 var candidates = Utils.zip(uuids,
155 credentials,
156 (uuid, credential) -> new GroupCandidate(uuid, Optional.fromNullable(credential)))
157 .collect(Collectors.toSet());
158
159 final var groupSecretParams = GroupSecretParams.generate();
160 return dependencies.getGroupsV2Operations()
161 .createNewGroup(groupSecretParams,
162 name,
163 Optional.fromNullable(avatar),
164 self,
165 candidates,
166 Member.Role.DEFAULT,
167 0);
168 }
169
170 private boolean areMembersValid(final Set<RecipientId> members) {
171 final var noGv2Capability = context.getProfileHelper()
172 .getRecipientProfile(new ArrayList<>(members))
173 .stream()
174 .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2))
175 .collect(Collectors.toSet());
176 if (noGv2Capability.size() > 0) {
177 logger.warn("Cannot create a V2 group as some members don't support Groups V2: {}",
178 noGv2Capability.stream().map(Profile::getDisplayName).collect(Collectors.joining(", ")));
179 return false;
180 }
181
182 return true;
183 }
184
185 public Pair<DecryptedGroup, GroupChange> updateGroup(
186 GroupInfoV2 groupInfoV2, String name, String description, File avatarFile
187 ) throws IOException {
188 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
189 var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
190
191 var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
192
193 if (description != null) {
194 change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description));
195 }
196
197 if (avatarFile != null) {
198 final var avatarBytes = readAvatarBytes(avatarFile);
199 var avatarCdnKey = dependencies.getGroupsV2Api()
200 .uploadAvatar(avatarBytes, groupSecretParams, getGroupAuthForToday(groupSecretParams));
201 change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
202 }
203
204 change.setSourceUuid(getSelfAci().toByteString());
205
206 return commitChange(groupInfoV2, change);
207 }
208
209 public Pair<DecryptedGroup, GroupChange> addMembers(
210 GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
211 ) throws IOException {
212 GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
213
214 if (!areMembersValid(newMembers)) {
215 throw new IOException("Failed to update group");
216 }
217
218 final var memberList = new ArrayList<>(newMembers);
219 final var credentials = context.getProfileHelper().getRecipientProfileKeyCredential(memberList).stream();
220 final var uuids = memberList.stream()
221 .map(member -> context.getRecipientHelper().resolveSignalServiceAddress(member).getAci().uuid());
222 var candidates = Utils.zip(uuids,
223 credentials,
224 (uuid, credential) -> new GroupCandidate(uuid, Optional.fromNullable(credential)))
225 .collect(Collectors.toSet());
226
227 final var aci = getSelfAci();
228 final var change = groupOperations.createModifyGroupMembershipChange(candidates, aci.uuid());
229
230 change.setSourceUuid(getSelfAci().toByteString());
231
232 return commitChange(groupInfoV2, change);
233 }
234
235 public Pair<DecryptedGroup, GroupChange> leaveGroup(
236 GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
237 ) throws IOException {
238 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
239 final var selfAci = getSelfAci();
240 var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfAci.uuid());
241
242 if (selfPendingMember.isPresent()) {
243 return revokeInvites(groupInfoV2, Set.of(selfPendingMember.get()));
244 }
245
246 final var adminUuids = membersToMakeAdmin.stream()
247 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
248 .map(SignalServiceAddress::getAci)
249 .map(ACI::uuid)
250 .toList();
251 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
252 return commitChange(groupInfoV2,
253 groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci.uuid(), adminUuids));
254 }
255
256 public Pair<DecryptedGroup, GroupChange> removeMembers(
257 GroupInfoV2 groupInfoV2, Set<RecipientId> members
258 ) throws IOException {
259 final var memberUuids = members.stream()
260 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
261 .map(SignalServiceAddress::getAci)
262 .map(ACI::uuid)
263 .collect(Collectors.toSet());
264 return ejectMembers(groupInfoV2, memberUuids);
265 }
266
267 public Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
268 GroupInfoV2 groupInfoV2, Set<RecipientId> members
269 ) throws IOException {
270 var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
271 final var memberUuids = members.stream()
272 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
273 .map(SignalServiceAddress::getAci)
274 .map(ACI::uuid)
275 .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid))
276 .filter(Optional::isPresent)
277 .map(Optional::get)
278 .collect(Collectors.toSet());
279 return revokeInvites(groupInfoV2, memberUuids);
280 }
281
282 public Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
283 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
284 final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
285 final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
286 return commitChange(groupInfoV2, change);
287 }
288
289 public Pair<DecryptedGroup, GroupChange> setGroupLinkState(
290 GroupInfoV2 groupInfoV2, GroupLinkState state
291 ) throws IOException {
292 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
293
294 final var accessRequired = toAccessControl(state);
295 final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup()
296 .getInviteLinkPassword()
297 .isEmpty();
298
299 final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
300 GroupLinkPassword.createNew().serialize(),
301 accessRequired) : groupOperations.createChangeJoinByLinkRights(accessRequired);
302 return commitChange(groupInfoV2, change);
303 }
304
305 public Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
306 GroupInfoV2 groupInfoV2, GroupPermission permission
307 ) throws IOException {
308 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
309
310 final var accessRequired = toAccessControl(permission);
311 final var change = groupOperations.createChangeAttributesRights(accessRequired);
312 return commitChange(groupInfoV2, change);
313 }
314
315 public Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
316 GroupInfoV2 groupInfoV2, GroupPermission permission
317 ) throws IOException {
318 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
319
320 final var accessRequired = toAccessControl(permission);
321 final var change = groupOperations.createChangeMembershipRights(accessRequired);
322 return commitChange(groupInfoV2, change);
323 }
324
325 public GroupChange joinGroup(
326 GroupMasterKey groupMasterKey,
327 GroupLinkPassword groupLinkPassword,
328 DecryptedGroupJoinInfo decryptedGroupJoinInfo
329 ) throws IOException {
330 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
331 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
332
333 final var selfRecipientId = context.getAccount().getSelfRecipientId();
334 final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId);
335 if (profileKeyCredential == null) {
336 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
337 }
338
339 var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
340 var change = requestToJoin
341 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
342 : groupOperations.createGroupJoinDirect(profileKeyCredential);
343
344 change.setSourceUuid(context.getRecipientHelper()
345 .resolveSignalServiceAddress(selfRecipientId)
346 .getAci()
347 .toByteString());
348
349 return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
350 }
351
352 public Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
353 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
354
355 final var selfRecipientId = context.getAccount().getSelfRecipientId();
356 final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId);
357 if (profileKeyCredential == null) {
358 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
359 }
360
361 final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
362
363 final var aci = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId).getAci();
364 change.setSourceUuid(aci.toByteString());
365
366 return commitChange(groupInfoV2, change);
367 }
368
369 public Pair<DecryptedGroup, GroupChange> setMemberAdmin(
370 GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
371 ) throws IOException {
372 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
373 final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
374 final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT;
375 final var change = groupOperations.createChangeMemberRole(address.getAci().uuid(), newRole);
376 return commitChange(groupInfoV2, change);
377 }
378
379 public Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
380 GroupInfoV2 groupInfoV2, int messageExpirationTimer
381 ) throws IOException {
382 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
383 final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
384 return commitChange(groupInfoV2, change);
385 }
386
387 public Pair<DecryptedGroup, GroupChange> setIsAnnouncementGroup(
388 GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
389 ) throws IOException {
390 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
391 final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
392 return commitChange(groupInfoV2, change);
393 }
394
395 private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) {
396 return switch (state) {
397 case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE;
398 case ENABLED -> AccessControl.AccessRequired.ANY;
399 case ENABLED_WITH_APPROVAL -> AccessControl.AccessRequired.ADMINISTRATOR;
400 };
401 }
402
403 private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) {
404 return switch (permission) {
405 case EVERY_MEMBER -> AccessControl.AccessRequired.MEMBER;
406 case ONLY_ADMINS -> AccessControl.AccessRequired.ADMINISTRATOR;
407 };
408 }
409
410 private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) {
411 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
412 return dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
413 }
414
415 private Pair<DecryptedGroup, GroupChange> revokeInvites(
416 GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
417 ) throws IOException {
418 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
419 final var uuidCipherTexts = pendingMembers.stream().map(member -> {
420 try {
421 return new UuidCiphertext(member.getUuidCipherText().toByteArray());
422 } catch (InvalidInputException e) {
423 throw new AssertionError(e);
424 }
425 }).collect(Collectors.toSet());
426 return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
427 }
428
429 private Pair<DecryptedGroup, GroupChange> ejectMembers(
430 GroupInfoV2 groupInfoV2, Set<UUID> uuids
431 ) throws IOException {
432 final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
433 return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(uuids));
434 }
435
436 private Pair<DecryptedGroup, GroupChange> commitChange(
437 GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
438 ) throws IOException {
439 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
440 final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
441 final var previousGroupState = groupInfoV2.getGroup();
442 final var nextRevision = previousGroupState.getRevision() + 1;
443 final var changeActions = change.setRevision(nextRevision).build();
444 final DecryptedGroupChange decryptedChange;
445 final DecryptedGroup decryptedGroupState;
446
447 try {
448 decryptedChange = groupOperations.decryptChange(changeActions, getSelfAci().uuid());
449 decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange);
450 } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
451 throw new IOException(e);
452 }
453
454 var signedGroupChange = dependencies.getGroupsV2Api()
455 .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.absent());
456
457 return new Pair<>(decryptedGroupState, signedGroupChange);
458 }
459
460 private GroupChange commitChange(
461 GroupSecretParams groupSecretParams,
462 int currentRevision,
463 GroupChange.Actions.Builder change,
464 GroupLinkPassword password
465 ) throws IOException {
466 final var nextRevision = currentRevision + 1;
467 final var changeActions = change.setRevision(nextRevision).build();
468
469 return dependencies.getGroupsV2Api()
470 .patchGroup(changeActions,
471 getGroupAuthForToday(groupSecretParams),
472 Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
473 }
474
475 public DecryptedGroup getUpdatedDecryptedGroup(
476 DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
477 ) {
478 try {
479 final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
480 if (decryptedGroupChange == null) {
481 return null;
482 }
483 return DecryptedGroupUtil.apply(group, decryptedGroupChange);
484 } catch (NotAbleToApplyGroupV2ChangeException e) {
485 return null;
486 }
487 }
488
489 private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
490 if (signedGroupChange != null) {
491 var groupOperations = dependencies.getGroupsV2Operations()
492 .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
493
494 try {
495 return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orNull();
496 } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
497 return null;
498 }
499 }
500
501 return null;
502 }
503
504 private static int currentTimeDays() {
505 return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
506 }
507
508 private GroupsV2AuthorizationString getGroupAuthForToday(
509 final GroupSecretParams groupSecretParams
510 ) throws IOException {
511 final var today = currentTimeDays();
512 if (groupApiCredentials == null || !groupApiCredentials.containsKey(today)) {
513 // Returns credentials for the next 7 days
514 groupApiCredentials = dependencies.getGroupsV2Api().getCredentials(today);
515 // TODO cache credentials on disk until they expire
516 }
517 var authCredentialResponse = groupApiCredentials.get(today);
518 final var aci = getSelfAci();
519 try {
520 return dependencies.getGroupsV2Api()
521 .getGroupsV2AuthorizationString(aci, today, groupSecretParams, authCredentialResponse);
522 } catch (VerificationFailedException e) {
523 throw new IOException(e);
524 }
525 }
526
527 private ACI getSelfAci() {
528 return context.getAccount().getAci();
529 }
530 }