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
.DecryptedMember
;
29 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedPendingMember
;
30 import org
.slf4j
.Logger
;
31 import org
.slf4j
.LoggerFactory
;
32 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupUtil
;
33 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupCandidate
;
34 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
35 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2AuthorizationString
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupsV2Operations
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.InvalidGroupStateException
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.NotAbleToApplyGroupV2ChangeException
;
39 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
40 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
41 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
42 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.NonSuccessfulResponseCodeException
;
45 import java
.io
.FileInputStream
;
46 import java
.io
.IOException
;
47 import java
.io
.InputStream
;
48 import java
.util
.ArrayList
;
49 import java
.util
.Arrays
;
50 import java
.util
.HashMap
;
51 import java
.util
.List
;
52 import java
.util
.Optional
;
54 import java
.util
.UUID
;
55 import java
.util
.concurrent
.TimeUnit
;
56 import java
.util
.stream
.Collectors
;
60 private final static Logger logger
= LoggerFactory
.getLogger(GroupV2Helper
.class);
62 private final SignalDependencies dependencies
;
63 private final Context context
;
65 private HashMap
<Integer
, AuthCredentialResponse
> groupApiCredentials
;
67 GroupV2Helper(final Context context
) {
68 this.dependencies
= context
.getDependencies();
69 this.context
= context
;
72 DecryptedGroup
getDecryptedGroup(final GroupSecretParams groupSecretParams
) throws NotAGroupMemberException
{
74 final var groupsV2AuthorizationString
= getGroupAuthForToday(groupSecretParams
);
75 return dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupsV2AuthorizationString
);
76 } catch (NonSuccessfulResponseCodeException e
) {
77 if (e
.getCode() == 403) {
78 throw new NotAGroupMemberException(GroupUtils
.getGroupIdV2(groupSecretParams
), null);
80 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
82 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
83 logger
.warn("Failed to retrieve Group V2 info, ignoring: {}", e
.getMessage());
88 DecryptedGroupJoinInfo
getDecryptedGroupJoinInfo(
89 GroupMasterKey groupMasterKey
, GroupLinkPassword password
90 ) throws IOException
, GroupLinkNotActiveException
{
91 var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
93 return dependencies
.getGroupsV2Api()
94 .getGroupJoinInfo(groupSecretParams
,
95 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
),
96 getGroupAuthForToday(groupSecretParams
));
99 Pair
<GroupInfoV2
, DecryptedGroup
> createGroup(
100 String name
, Set
<RecipientId
> members
, File avatarFile
101 ) throws IOException
{
102 final var avatarBytes
= readAvatarBytes(avatarFile
);
103 final var newGroup
= buildNewGroup(name
, members
, avatarBytes
);
104 if (newGroup
== null) {
108 final var groupSecretParams
= newGroup
.getGroupSecretParams();
110 final GroupsV2AuthorizationString groupAuthForToday
;
111 final DecryptedGroup decryptedGroup
;
113 groupAuthForToday
= getGroupAuthForToday(groupSecretParams
);
114 dependencies
.getGroupsV2Api().putNewGroup(newGroup
, groupAuthForToday
);
115 decryptedGroup
= dependencies
.getGroupsV2Api().getGroup(groupSecretParams
, groupAuthForToday
);
116 } catch (IOException
| VerificationFailedException
| InvalidGroupStateException e
) {
117 logger
.warn("Failed to create V2 group: {}", e
.getMessage());
120 if (decryptedGroup
== null) {
121 logger
.warn("Failed to create V2 group, unknown error!");
125 final var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
126 final var masterKey
= groupSecretParams
.getMasterKey();
127 var g
= new GroupInfoV2(groupId
, masterKey
);
129 return new Pair
<>(g
, decryptedGroup
);
132 private byte[] readAvatarBytes(final File avatarFile
) throws IOException
{
133 final byte[] avatarBytes
;
134 try (InputStream avatar
= avatarFile
== null ?
null : new FileInputStream(avatarFile
)) {
135 avatarBytes
= avatar
== null ?
null : IOUtils
.readFully(avatar
);
140 private GroupsV2Operations
.NewGroup
buildNewGroup(
141 String name
, Set
<RecipientId
> members
, byte[] avatar
143 final var profileKeyCredential
= context
.getProfileHelper()
144 .getRecipientProfileKeyCredential(context
.getAccount().getSelfRecipientId());
145 if (profileKeyCredential
== null) {
146 logger
.warn("Cannot create a V2 group as self does not have a versioned profile");
150 final var self
= new GroupCandidate(getSelfAci().uuid(), Optional
.of(profileKeyCredential
));
151 final var memberList
= new ArrayList
<>(members
);
152 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
153 final var uuids
= memberList
.stream()
154 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
155 var candidates
= Utils
.zip(uuids
,
157 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
158 .collect(Collectors
.toSet());
160 final var groupSecretParams
= GroupSecretParams
.generate();
161 return dependencies
.getGroupsV2Operations()
162 .createNewGroup(groupSecretParams
,
164 Optional
.ofNullable(avatar
),
171 Pair
<DecryptedGroup
, GroupChange
> updateGroup(
172 GroupInfoV2 groupInfoV2
, String name
, String description
, File avatarFile
173 ) throws IOException
{
174 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
175 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
177 var change
= name
!= null ? groupOperations
.createModifyGroupTitle(name
) : GroupChange
.Actions
.newBuilder();
179 if (description
!= null) {
180 change
.setModifyDescription(groupOperations
.createModifyGroupDescriptionAction(description
));
183 if (avatarFile
!= null) {
184 final var avatarBytes
= readAvatarBytes(avatarFile
);
185 var avatarCdnKey
= dependencies
.getGroupsV2Api()
186 .uploadAvatar(avatarBytes
, groupSecretParams
, getGroupAuthForToday(groupSecretParams
));
187 change
.setModifyAvatar(GroupChange
.Actions
.ModifyAvatarAction
.newBuilder().setAvatar(avatarCdnKey
));
190 change
.setSourceUuid(getSelfAci().toByteString());
192 return commitChange(groupInfoV2
, change
);
195 Pair
<DecryptedGroup
, GroupChange
> addMembers(
196 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> newMembers
197 ) throws IOException
{
198 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
200 final var memberList
= new ArrayList
<>(newMembers
);
201 final var credentials
= context
.getProfileHelper().getRecipientProfileKeyCredential(memberList
).stream();
202 final var uuids
= memberList
.stream()
203 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid());
204 var candidates
= Utils
.zip(uuids
,
206 (uuid
, credential
) -> new GroupCandidate(uuid
, Optional
.ofNullable(credential
)))
207 .collect(Collectors
.toSet());
208 final var bannedUuids
= groupInfoV2
.getBannedMembers()
210 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
211 .collect(Collectors
.toSet());
213 final var aci
= getSelfAci();
214 final var change
= groupOperations
.createModifyGroupMembershipChange(candidates
, bannedUuids
, aci
.uuid());
216 change
.setSourceUuid(getSelfAci().toByteString());
218 return commitChange(groupInfoV2
, change
);
221 Pair
<DecryptedGroup
, GroupChange
> leaveGroup(
222 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> membersToMakeAdmin
223 ) throws IOException
{
224 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
225 final var selfAci
= getSelfAci();
226 var selfPendingMember
= DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, selfAci
.uuid());
228 if (selfPendingMember
.isPresent()) {
229 return revokeInvites(groupInfoV2
, Set
.of(selfPendingMember
.get()));
232 final var adminUuids
= membersToMakeAdmin
.stream()
233 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
234 .map(SignalServiceAddress
::getServiceId
)
235 .map(ServiceId
::uuid
)
237 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
238 return commitChange(groupInfoV2
,
239 groupOperations
.createLeaveAndPromoteMembersToAdmin(selfAci
.uuid(), adminUuids
));
242 Pair
<DecryptedGroup
, GroupChange
> removeMembers(
243 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
244 ) throws IOException
{
245 final var memberUuids
= members
.stream()
246 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
247 .map(SignalServiceAddress
::getServiceId
)
248 .map(ServiceId
::uuid
)
249 .collect(Collectors
.toSet());
250 return ejectMembers(groupInfoV2
, memberUuids
);
253 Pair
<DecryptedGroup
, GroupChange
> revokeInvitedMembers(
254 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> members
255 ) throws IOException
{
256 var pendingMembersList
= groupInfoV2
.getGroup().getPendingMembersList();
257 final var memberUuids
= members
.stream()
258 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
259 .map(SignalServiceAddress
::getServiceId
)
260 .map(ServiceId
::uuid
)
261 .map(uuid
-> DecryptedGroupUtil
.findPendingByUuid(pendingMembersList
, uuid
))
262 .filter(Optional
::isPresent
)
264 .collect(Collectors
.toSet());
265 return revokeInvites(groupInfoV2
, memberUuids
);
268 Pair
<DecryptedGroup
, GroupChange
> banMembers(
269 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
270 ) throws IOException
{
271 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
273 final var uuids
= block
.stream()
274 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
275 .collect(Collectors
.toSet());
277 final var change
= groupOperations
.createBanUuidsChange(uuids
,
279 groupInfoV2
.getGroup().getBannedMembersList());
281 change
.setSourceUuid(getSelfAci().toByteString());
283 return commitChange(groupInfoV2
, change
);
286 Pair
<DecryptedGroup
, GroupChange
> unbanMembers(
287 GroupInfoV2 groupInfoV2
, Set
<RecipientId
> block
288 ) throws IOException
{
289 GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
291 final var uuids
= block
.stream()
292 .map(member
-> context
.getRecipientHelper().resolveSignalServiceAddress(member
).getServiceId().uuid())
293 .collect(Collectors
.toSet());
295 final var change
= groupOperations
.createUnbanUuidsChange(uuids
);
297 change
.setSourceUuid(getSelfAci().toByteString());
299 return commitChange(groupInfoV2
, change
);
302 Pair
<DecryptedGroup
, GroupChange
> resetGroupLinkPassword(GroupInfoV2 groupInfoV2
) throws IOException
{
303 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
304 final var newGroupLinkPassword
= GroupLinkPassword
.createNew().serialize();
305 final var change
= groupOperations
.createModifyGroupLinkPasswordChange(newGroupLinkPassword
);
306 return commitChange(groupInfoV2
, change
);
309 Pair
<DecryptedGroup
, GroupChange
> setGroupLinkState(
310 GroupInfoV2 groupInfoV2
, GroupLinkState state
311 ) throws IOException
{
312 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
314 final var accessRequired
= toAccessControl(state
);
315 final var requiresNewPassword
= state
!= GroupLinkState
.DISABLED
&& groupInfoV2
.getGroup()
316 .getInviteLinkPassword()
319 final var change
= requiresNewPassword ? groupOperations
.createModifyGroupLinkPasswordAndRightsChange(
320 GroupLinkPassword
.createNew().serialize(),
321 accessRequired
) : groupOperations
.createChangeJoinByLinkRights(accessRequired
);
322 return commitChange(groupInfoV2
, change
);
325 Pair
<DecryptedGroup
, GroupChange
> setEditDetailsPermission(
326 GroupInfoV2 groupInfoV2
, GroupPermission permission
327 ) throws IOException
{
328 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
330 final var accessRequired
= toAccessControl(permission
);
331 final var change
= groupOperations
.createChangeAttributesRights(accessRequired
);
332 return commitChange(groupInfoV2
, change
);
335 Pair
<DecryptedGroup
, GroupChange
> setAddMemberPermission(
336 GroupInfoV2 groupInfoV2
, GroupPermission permission
337 ) throws IOException
{
338 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
340 final var accessRequired
= toAccessControl(permission
);
341 final var change
= groupOperations
.createChangeMembershipRights(accessRequired
);
342 return commitChange(groupInfoV2
, change
);
345 Pair
<DecryptedGroup
, GroupChange
> updateSelfProfileKey(GroupInfoV2 groupInfoV2
) throws IOException
{
346 Optional
<DecryptedMember
> selfInGroup
= groupInfoV2
.getGroup() == null
348 : DecryptedGroupUtil
.findMemberByUuid(groupInfoV2
.getGroup().getMembersList(), getSelfAci().uuid());
349 if (selfInGroup
.isEmpty()) {
350 logger
.trace("Not updating group, self not in group " + groupInfoV2
.getGroupId().toBase64());
354 final var profileKey
= context
.getAccount().getProfileKey();
355 if (Arrays
.equals(profileKey
.serialize(), selfInGroup
.get().getProfileKey().toByteArray())) {
356 logger
.trace("Not updating group, own Profile Key is already up to date in group "
357 + groupInfoV2
.getGroupId().toBase64());
360 logger
.debug("Updating own profile key in group " + groupInfoV2
.getGroupId().toBase64());
362 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
363 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
364 if (profileKeyCredential
== null) {
365 logger
.trace("Cannot update profile key as self does not have a versioned profile");
369 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
370 final var change
= groupOperations
.createUpdateProfileKeyCredentialChange(profileKeyCredential
);
371 change
.setSourceUuid(getSelfAci().toByteString());
372 return commitChange(groupInfoV2
, change
);
375 GroupChange
joinGroup(
376 GroupMasterKey groupMasterKey
,
377 GroupLinkPassword groupLinkPassword
,
378 DecryptedGroupJoinInfo decryptedGroupJoinInfo
379 ) throws IOException
{
380 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
381 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
383 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
384 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
385 if (profileKeyCredential
== null) {
386 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
389 var requestToJoin
= decryptedGroupJoinInfo
.getAddFromInviteLink() == AccessControl
.AccessRequired
.ADMINISTRATOR
;
390 var change
= requestToJoin
391 ? groupOperations
.createGroupJoinRequest(profileKeyCredential
)
392 : groupOperations
.createGroupJoinDirect(profileKeyCredential
);
394 change
.setSourceUuid(context
.getRecipientHelper()
395 .resolveSignalServiceAddress(selfRecipientId
)
399 return commitChange(groupSecretParams
, decryptedGroupJoinInfo
.getRevision(), change
, groupLinkPassword
);
402 Pair
<DecryptedGroup
, GroupChange
> acceptInvite(GroupInfoV2 groupInfoV2
) throws IOException
{
403 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
405 final var selfRecipientId
= context
.getAccount().getSelfRecipientId();
406 final var profileKeyCredential
= context
.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId
);
407 if (profileKeyCredential
== null) {
408 throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
411 final var change
= groupOperations
.createAcceptInviteChange(profileKeyCredential
);
413 final var aci
= context
.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId
).getServiceId();
414 change
.setSourceUuid(aci
.toByteString());
416 return commitChange(groupInfoV2
, change
);
419 Pair
<DecryptedGroup
, GroupChange
> setMemberAdmin(
420 GroupInfoV2 groupInfoV2
, RecipientId recipientId
, boolean admin
421 ) throws IOException
{
422 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
423 final var address
= context
.getRecipientHelper().resolveSignalServiceAddress(recipientId
);
424 final var newRole
= admin ? Member
.Role
.ADMINISTRATOR
: Member
.Role
.DEFAULT
;
425 final var change
= groupOperations
.createChangeMemberRole(address
.getServiceId().uuid(), newRole
);
426 return commitChange(groupInfoV2
, change
);
429 Pair
<DecryptedGroup
, GroupChange
> setMessageExpirationTimer(
430 GroupInfoV2 groupInfoV2
, int messageExpirationTimer
431 ) throws IOException
{
432 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
433 final var change
= groupOperations
.createModifyGroupTimerChange(messageExpirationTimer
);
434 return commitChange(groupInfoV2
, change
);
437 Pair
<DecryptedGroup
, GroupChange
> setIsAnnouncementGroup(
438 GroupInfoV2 groupInfoV2
, boolean isAnnouncementGroup
439 ) throws IOException
{
440 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
441 final var change
= groupOperations
.createAnnouncementGroupChange(isAnnouncementGroup
);
442 return commitChange(groupInfoV2
, change
);
445 private AccessControl
.AccessRequired
toAccessControl(final GroupLinkState state
) {
446 return switch (state
) {
447 case DISABLED
-> AccessControl
.AccessRequired
.UNSATISFIABLE
;
448 case ENABLED
-> AccessControl
.AccessRequired
.ANY
;
449 case ENABLED_WITH_APPROVAL
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
453 private AccessControl
.AccessRequired
toAccessControl(final GroupPermission permission
) {
454 return switch (permission
) {
455 case EVERY_MEMBER
-> AccessControl
.AccessRequired
.MEMBER
;
456 case ONLY_ADMINS
-> AccessControl
.AccessRequired
.ADMINISTRATOR
;
460 private GroupsV2Operations
.GroupOperations
getGroupOperations(final GroupInfoV2 groupInfoV2
) {
461 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
462 return dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
465 private Pair
<DecryptedGroup
, GroupChange
> revokeInvites(
466 GroupInfoV2 groupInfoV2
, Set
<DecryptedPendingMember
> pendingMembers
467 ) throws IOException
{
468 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
469 final var uuidCipherTexts
= pendingMembers
.stream().map(member
-> {
471 return new UuidCiphertext(member
.getUuidCipherText().toByteArray());
472 } catch (InvalidInputException e
) {
473 throw new AssertionError(e
);
475 }).collect(Collectors
.toSet());
476 return commitChange(groupInfoV2
, groupOperations
.createRemoveInvitationChange(uuidCipherTexts
));
479 private Pair
<DecryptedGroup
, GroupChange
> ejectMembers(
480 GroupInfoV2 groupInfoV2
, Set
<UUID
> uuids
481 ) throws IOException
{
482 final GroupsV2Operations
.GroupOperations groupOperations
= getGroupOperations(groupInfoV2
);
483 return commitChange(groupInfoV2
, groupOperations
.createRemoveMembersChange(uuids
, false, List
.of()));
486 private Pair
<DecryptedGroup
, GroupChange
> commitChange(
487 GroupInfoV2 groupInfoV2
, GroupChange
.Actions
.Builder change
488 ) throws IOException
{
489 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
490 final var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
491 final var previousGroupState
= groupInfoV2
.getGroup();
492 final var nextRevision
= previousGroupState
.getRevision() + 1;
493 final var changeActions
= change
.setRevision(nextRevision
).build();
494 final DecryptedGroupChange decryptedChange
;
495 final DecryptedGroup decryptedGroupState
;
498 decryptedChange
= groupOperations
.decryptChange(changeActions
, getSelfAci().uuid());
499 decryptedGroupState
= DecryptedGroupUtil
.apply(previousGroupState
, decryptedChange
);
500 } catch (VerificationFailedException
| InvalidGroupStateException
| NotAbleToApplyGroupV2ChangeException e
) {
501 throw new IOException(e
);
504 var signedGroupChange
= dependencies
.getGroupsV2Api()
505 .patchGroup(changeActions
, getGroupAuthForToday(groupSecretParams
), Optional
.empty());
507 return new Pair
<>(decryptedGroupState
, signedGroupChange
);
510 private GroupChange
commitChange(
511 GroupSecretParams groupSecretParams
,
513 GroupChange
.Actions
.Builder change
,
514 GroupLinkPassword password
515 ) throws IOException
{
516 final var nextRevision
= currentRevision
+ 1;
517 final var changeActions
= change
.setRevision(nextRevision
).build();
519 return dependencies
.getGroupsV2Api()
520 .patchGroup(changeActions
,
521 getGroupAuthForToday(groupSecretParams
),
522 Optional
.ofNullable(password
).map(GroupLinkPassword
::serialize
));
525 DecryptedGroup
getUpdatedDecryptedGroup(
526 DecryptedGroup group
, byte[] signedGroupChange
, GroupMasterKey groupMasterKey
529 final var decryptedGroupChange
= getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
530 if (decryptedGroupChange
== null) {
533 return DecryptedGroupUtil
.apply(group
, decryptedGroupChange
);
534 } catch (NotAbleToApplyGroupV2ChangeException e
) {
539 private DecryptedGroupChange
getDecryptedGroupChange(byte[] signedGroupChange
, GroupMasterKey groupMasterKey
) {
540 if (signedGroupChange
!= null) {
541 var groupOperations
= dependencies
.getGroupsV2Operations()
542 .forGroup(GroupSecretParams
.deriveFromMasterKey(groupMasterKey
));
545 return groupOperations
.decryptChange(GroupChange
.parseFrom(signedGroupChange
), true).orElse(null);
546 } catch (VerificationFailedException
| InvalidGroupStateException
| InvalidProtocolBufferException e
) {
554 private static int currentTimeDays() {
555 return (int) TimeUnit
.MILLISECONDS
.toDays(System
.currentTimeMillis());
558 private GroupsV2AuthorizationString
getGroupAuthForToday(
559 final GroupSecretParams groupSecretParams
560 ) throws IOException
{
561 final var today
= currentTimeDays();
562 if (groupApiCredentials
== null || !groupApiCredentials
.containsKey(today
)) {
563 // Returns credentials for the next 7 days
564 final var isAci
= true; // TODO enable group handling with PNI
565 groupApiCredentials
= dependencies
.getGroupsV2Api().getCredentials(today
, isAci
);
566 // TODO cache credentials on disk until they expire
568 var authCredentialResponse
= groupApiCredentials
.get(today
);
569 final var aci
= getSelfAci();
571 return dependencies
.getGroupsV2Api()
572 .getGroupsV2AuthorizationString(aci
, today
, groupSecretParams
, authCredentialResponse
);
573 } catch (VerificationFailedException e
) {
574 throw new IOException(e
);
578 private ACI
getSelfAci() {
579 return context
.getAccount().getAci();