1 package org
.asamk
.signal
.manager
.helper
;
3 import com
.google
.protobuf
.InvalidProtocolBufferException
;
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
.RecipientId
;
14 import org
.asamk
.signal
.manager
.util
.IOUtils
;
15 import org
.asamk
.signal
.manager
.util
.Utils
;
16 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
17 import org
.signal
.libsignal
.zkgroup
.VerificationFailedException
;
18 import org
.signal
.libsignal
.zkgroup
.auth
.AuthCredentialResponse
;
19 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
20 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
21 import org
.signal
.libsignal
.zkgroup
.groups
.UuidCiphertext
;
22 import org
.signal
.storageservice
.protos
.groups
.AccessControl
;
23 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
24 import org
.signal
.storageservice
.protos
.groups
.Member
;
25 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
26 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupChange
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
28 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
29 import org
.slf4j
.Logger
;
30 import org
.slf4j
.LoggerFactory
;
31 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
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
.ACI
;
39 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
40 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
41 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
44 import java
.io
.FileInputStream
;
45 import java
.io
.IOException
;
46 import java
.io
.InputStream
;
47 import java
.util
.ArrayList
;
48 import java
.util
.HashMap
;
49 import java
.util
.List
;
50 import java
.util
.Optional
;
52 import java
.util
.UUID
;
53 import java
.util
.concurrent
.TimeUnit
;
54 import java
.util
.stream
.Collectors
;
58 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
60 private final SignalDependencies dependencies
;
61 private final Context context
;
63 private HashMap
<Integer
, AuthCredentialResponse
> groupApiCredentials
;
65 GroupV2Helper(final Context context
) {
66 this.dependencies
= context
.getDependencies();
67 this.context
= context
;
70 DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
72 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
73 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
74 } catch (NonSuccessfulResponseCodeException e
) {
75 if (e
.getCode() == 403) {
76 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
78 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
80 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
81 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
86 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
87 GroupMasterKey groupMasterKey
, GroupLinkPassword password
88 ) throws IOException
, GroupLinkNotActiveException
{
89 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
91 return dependencies
.getGroupsV2Api()
92 .getGroupJoinInfo(groupSecretParams
,
93 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
94 getGroupAuthForToday(groupSecretParams
));
97 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
98 String name
, Set
<RecipientId
> members
, File avatarFile
99 ) throws IOException
{
100 final var avatarBytes
= readAvatarBytes(avatarFile
);
101 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
102 if (newGroup
== null) {
106 final var groupSecretParams
= newGroup
.getGroupSecretParams();
108 final GroupsV2AuthorizationString groupAuthForToday
;
109 final DecryptedGroup decryptedGroup
;
111 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
112 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
113 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
114 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
115 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
118 if (decryptedGroup
== null) {
119 logger
.warn("Failed to create V2 group, unknown error!");
123 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
124 final var masterKey
= groupSecretParams
.getMasterKey();
125 var g
= new GroupInfoV2(groupId
, masterKey
);
127 return new Pair
<>(g
, decryptedGroup
);
130 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
131 final byte[] avatarBytes
;
132 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
133 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
138 private GroupsV2Operations
.NewGroup
buildNewGroup(
139 String name
, Set
<RecipientId
> members
, byte[] avatar
141 final var profileKeyCredential
= context
.getProfileHelper()
142 .getRecipientProfileKeyCredential(context
.getAccount().getSelfRecipientId());
143 if (profileKeyCredential
== null) {
144 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
148 final var self
= new GroupCandidate(getSelfAci().uuid(), Optional
.of(profileKeyCredential
));
149 final var memberList
= new ArrayList
<>(members
);
150 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
151 final var uuids
= memberList
.stream()
152 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
153 var candidates
= Utils
.zip(uuids
,
155 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
156 .collect(Collectors
.toSet());
158 final var groupSecretParams
= GroupSecretParams
.generate();
159 return dependencies
.getGroupsV2Operations()
160 .createNewGroup(groupSecretParams
,
162 Optional
.ofNullable(avatar
),
169 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
170 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
171 ) throws IOException
{
172 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
173 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
175 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
177 if (description
!= null) {
178 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
181 if (avatarFile
!= null) {
182 final var avatarBytes
= readAvatarBytes(avatarFile
);
183 var avatarCdnKey
= dependencies
.getGroupsV2Api()
184 .uploadAvatar(avatarBytes
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
185 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
188 change
.setSourceUuid(getSelfAci().toByteString());
190 return commitChange(groupInfoV2
, change
);
193 Pair
<DecryptedGroup
, GroupChange
> addMembers(
194 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
195 ) throws IOException
{
196 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
198 final var memberList
= new ArrayList
<>(newMembers
);
199 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
200 final var uuids
= memberList
.stream()
201 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
202 var candidates
= Utils
.zip(uuids
,
204 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
205 .collect(Collectors
.toSet());
206 final var bannedUuids
= groupInfoV2
.getBannedMembers()
208 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
209 .collect(Collectors
.toSet());
211 final var aci
= getSelfAci();
212 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
.uuid());
214 change
.setSourceUuid(getSelfAci().toByteString());
216 return commitChange(groupInfoV2
, change
);
219 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
220 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
221 ) throws IOException
{
222 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
223 final var selfAci
= getSelfAci();
224 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfAci
.uuid());
226 if (selfPendingMember
.isPresent()) {
227 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
230 final var adminUuids
= membersToMakeAdmin
.stream()
231 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
232 .map(SignalServiceAddress
::getServiceId
)
233 .map(ServiceId
::uuid
)
235 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
236 return commitChange(groupInfoV2
,
237 groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
.uuid(), adminUuids
));
240 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
241 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
242 ) throws IOException
{
243 final var memberUuids
= members
.stream()
244 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
245 .map(SignalServiceAddress
::getServiceId
)
246 .map(ServiceId
::uuid
)
247 .collect(Collectors
.toSet());
248 return ejectMembers(groupInfoV2
, memberUuids
);
251 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
252 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
253 ) throws IOException
{
254 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
255 final var memberUuids
= members
.stream()
256 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
257 .map(SignalServiceAddress
::getServiceId
)
258 .map(ServiceId
::uuid
)
259 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
260 .filter(Optional
::isPresent
)
262 .collect(Collectors
.toSet());
263 return revokeInvites(groupInfoV2
, memberUuids
);
266 Pair
<DecryptedGroup
, GroupChange
> banMembers(
267 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
268 ) throws IOException
{
269 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
271 final var uuids
= block
.stream()
272 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
273 .collect(Collectors
.toSet());
275 final var change
= groupOperations
.createBanUuidsChange(uuids
,
277 groupInfoV2
.getGroup().getBannedMembersList());
279 change
.setSourceUuid(getSelfAci().toByteString());
281 return commitChange(groupInfoV2
, change
);
284 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
285 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
286 ) throws IOException
{
287 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
289 final var uuids
= block
.stream()
290 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
291 .collect(Collectors
.toSet());
293 final var change
= groupOperations
.createUnbanUuidsChange(uuids
);
295 change
.setSourceUuid(getSelfAci().toByteString());
297 return commitChange(groupInfoV2
, change
);
300 Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
301 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
302 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
303 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
304 return commitChange(groupInfoV2
, change
);
307 Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
308 GroupInfoV2 groupInfoV2
, GroupLinkState state
309 ) throws IOException
{
310 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
312 final var accessRequired
= toAccessControl(state
);
313 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
314 .getInviteLinkPassword()
317 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
318 GroupLinkPassword
.createNew().serialize(),
319 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
320 return commitChange(groupInfoV2
, change
);
323 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
324 GroupInfoV2 groupInfoV2
, GroupPermission permission
325 ) throws IOException
{
326 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
328 final var accessRequired
= toAccessControl(permission
);
329 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
330 return commitChange(groupInfoV2
, change
);
333 Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
334 GroupInfoV2 groupInfoV2
, GroupPermission permission
335 ) throws IOException
{
336 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
338 final var accessRequired
= toAccessControl(permission
);
339 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
340 return commitChange(groupInfoV2
, change
);
343 GroupChange
joinGroup(
344 GroupMasterKey groupMasterKey
,
345 GroupLinkPassword groupLinkPassword
,
346 DecryptedGroupJoinInfo decryptedGroupJoinInfo
347 ) throws IOException
{
348 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
349 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
351 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
352 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
353 if (profileKeyCredential
== null) {
354 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
357 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
358 var change
= requestToJoin
359 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
360 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
362 change
.setSourceUuid(context
.getRecipientHelper()
363 .resolveSignalServiceAddress(selfRecipientId
)
367 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
370 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
371 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
373 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
374 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
375 if (profileKeyCredential
== null) {
376 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
379 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
381 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
382 change
.setSourceUuid(aci
.toByteString());
384 return commitChange(groupInfoV2
, change
);
387 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
388 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
389 ) throws IOException
{
390 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
391 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
392 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
393 final var change
= groupOperations
.createChangeMemberRole(address
.getServiceId().uuid(), newRole
);
394 return commitChange(groupInfoV2
, change
);
397 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
398 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
399 ) throws IOException
{
400 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
401 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
402 return commitChange(groupInfoV2
, change
);
405 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
406 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
407 ) throws IOException
{
408 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
409 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
410 return commitChange(groupInfoV2
, change
);
413 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
414 return switch (state
) {
415 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
416 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
417 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
421 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
422 return switch (permission
) {
423 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
424 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
428 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
429 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
430 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
433 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
434 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
435 ) throws IOException
{
436 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
437 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
439 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
440 } catch (InvalidInputException e
) {
441 throw new AssertionError(e
);
443 }).collect(Collectors
.toSet());
444 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
447 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
448 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
449 ) throws IOException
{
450 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
451 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
, false, List
.of()));
454 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
455 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
456 ) throws IOException
{
457 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
458 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
459 final var previousGroupState
= groupInfoV2
.getGroup();
460 final var nextRevision
= previousGroupState
.getRevision() + 1;
461 final var changeActions
= change
.setRevision(nextRevision
).build();
462 final DecryptedGroupChange decryptedChange
;
463 final DecryptedGroup decryptedGroupState
;
466 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci().uuid());
467 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
468 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
469 throw new IOException(e
);
472 var signedGroupChange
= dependencies
.getGroupsV2Api()
473 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
475 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
478 private GroupChange
commitChange(
479 GroupSecretParams groupSecretParams
,
481 GroupChange
.Actions
.Builder change
,
482 GroupLinkPassword password
483 ) throws IOException
{
484 final var nextRevision
= currentRevision
+ 1;
485 final var changeActions
= change
.setRevision(nextRevision
).build();
487 return dependencies
.getGroupsV2Api()
488 .patchGroup(changeActions
,
489 getGroupAuthForToday(groupSecretParams
),
490 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
493 DecryptedGroup
getUpdatedDecryptedGroup(
494 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
497 final var decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
498 if (decryptedGroupChange
== null) {
501 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
502 } catch (NotAbleToApplyGroupV2ChangeException e
) {
507 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
508 if (signedGroupChange
!= null) {
509 var groupOperations
= dependencies
.getGroupsV2Operations()
510 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
513 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orElse(null);
514 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
522 private static int currentTimeDays() {
523 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
526 private GroupsV2AuthorizationString
getGroupAuthForToday(
527 final GroupSecretParams groupSecretParams
528 ) throws IOException
{
529 final var today
= currentTimeDays();
530 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(today
)) {
531 // Returns credentials for the next 7 days
532 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(today
);
533 // TODO cache credentials on disk until they expire
535 var authCredentialResponse
= groupApiCredentials
.get(today
);
536 final var aci
= getSelfAci();
538 return dependencies
.getGroupsV2Api()
539 .getGroupsV2AuthorizationString(aci
, today
, groupSecretParams
, authCredentialResponse
);
540 } catch (VerificationFailedException e
) {
541 throw new IOException(e
);
545 private ACI
getSelfAci() {
546 return context
.getAccount().getAci();