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