1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.AttachmentInvalidException
;
4 import org
.asamk
.signal
.manager
.AvatarStore
;
5 import org
.asamk
.signal
.manager
.SignalDependencies
;
6 import org
.asamk
.signal
.manager
.api
.SendGroupMessageResults
;
7 import org
.asamk
.signal
.manager
.config
.ServiceConfig
;
8 import org
.asamk
.signal
.manager
.groups
.GroupId
;
9 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
10 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
11 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
12 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
13 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
14 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
15 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
16 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
17 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
18 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
19 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
20 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
21 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
22 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
23 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
24 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
25 import org
.asamk
.signal
.manager
.util
.IOUtils
;
26 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
27 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
28 import org
.signal
.zkgroup
.InvalidInputException
;
29 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
30 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
31 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
32 import org
.slf4j
.Logger
;
33 import org
.slf4j
.LoggerFactory
;
34 import org
.whispersystems
.libsignal
.util
.Pair
;
35 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
36 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
37 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
38 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
39 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
40 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
41 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ConflictException
;
42 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
45 import java
.io
.IOException
;
46 import java
.io
.InputStream
;
47 import java
.io
.OutputStream
;
48 import java
.nio
.file
.Files
;
49 import java
.util
.Collection
;
50 import java
.util
.HashSet
;
51 import java
.util
.List
;
53 import java
.util
.stream
.Collectors
;
55 public class GroupHelper
{
57 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
59 private final SignalAccount account
;
60 private final SignalDependencies dependencies
;
61 private final SendHelper sendHelper
;
62 private final GroupV2Helper groupV2Helper
;
63 private final AvatarStore avatarStore
;
64 private final SignalServiceAddressResolver addressResolver
;
65 private final RecipientResolver recipientResolver
;
68 final SignalAccount account
,
69 final SignalDependencies dependencies
,
70 final SendHelper sendHelper
,
71 final GroupV2Helper groupV2Helper
,
72 final AvatarStore avatarStore
,
73 final SignalServiceAddressResolver addressResolver
,
74 final RecipientResolver recipientResolver
76 this.account
= account
;
77 this.dependencies
= dependencies
;
78 this.sendHelper
= sendHelper
;
79 this.groupV2Helper
= groupV2Helper
;
80 this.avatarStore
= avatarStore
;
81 this.addressResolver
= addressResolver
;
82 this.recipientResolver
= recipientResolver
;
85 public GroupInfo
getGroup(GroupId groupId
) {
86 return getGroup(groupId
, false);
89 public Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupIdV1 groupId
) throws IOException
{
90 final var streamDetails
= avatarStore
.retrieveGroupAvatar(groupId
);
91 if (streamDetails
== null) {
92 return Optional
.absent();
95 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.absent()));
98 public GroupInfoV2
getOrMigrateGroup(
99 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
101 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
103 var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
104 var groupInfo
= getGroup(groupId
);
105 final GroupInfoV2 groupInfoV2
;
106 if (groupInfo
instanceof GroupInfoV1
) {
107 // Received a v2 group message for a v1 group, we need to locally migrate the group
108 account
.getGroupStore().deleteGroupV1(((GroupInfoV1
) groupInfo
).getGroupId());
109 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
110 logger
.info("Locally migrated group {} to group v2, id: {}",
111 groupInfo
.getGroupId().toBase64(),
112 groupInfoV2
.getGroupId().toBase64());
113 } else if (groupInfo
instanceof GroupInfoV2
) {
114 groupInfoV2
= (GroupInfoV2
) groupInfo
;
116 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
119 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
120 DecryptedGroup group
= null;
121 if (signedGroupChange
!= null
122 && groupInfoV2
.getGroup() != null
123 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
124 group
= groupV2Helper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(),
129 group
= groupV2Helper
.getDecryptedGroup(groupSecretParams
);
132 storeProfileKeysFromMembers(group
);
133 final var avatar
= group
.getAvatar();
134 if (avatar
!= null && !avatar
.isEmpty()) {
135 downloadGroupAvatar(groupId
, groupSecretParams
, avatar
);
138 groupInfoV2
.setGroup(group
, recipientResolver
);
139 account
.getGroupStore().updateGroup(groupInfoV2
);
145 public Pair
<GroupId
, SendGroupMessageResults
> createGroup(
146 String name
, Set
<RecipientId
> members
, File avatarFile
147 ) throws IOException
, AttachmentInvalidException
{
148 final var selfRecipientId
= account
.getSelfRecipientId();
149 if (members
!= null && members
.contains(selfRecipientId
)) {
150 members
= new HashSet
<>(members
);
151 members
.remove(selfRecipientId
);
154 var gv2Pair
= groupV2Helper
.createGroup(name
== null ?
"" : name
,
155 members
== null ? Set
.of() : members
,
158 if (gv2Pair
== null) {
159 // Failed to create v2 group, creating v1 group instead
160 var gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
161 gv1
.addMembers(List
.of(selfRecipientId
));
162 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
163 return new Pair
<>(gv1
.getGroupId(), result
);
166 final var gv2
= gv2Pair
.first();
167 final var decryptedGroup
= gv2Pair
.second();
169 gv2
.setGroup(decryptedGroup
, recipientResolver
);
170 if (avatarFile
!= null) {
171 avatarStore
.storeGroupAvatar(gv2
.getGroupId(),
172 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
175 account
.getGroupStore().updateGroup(gv2
);
177 final var messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
179 final var result
= sendGroupMessage(messageBuilder
, gv2
.getMembersIncludingPendingWithout(selfRecipientId
));
180 return new Pair
<>(gv2
.getGroupId(), result
);
183 public SendGroupMessageResults
updateGroup(
184 final GroupId groupId
,
186 final String description
,
187 final Set
<RecipientId
> members
,
188 final Set
<RecipientId
> removeMembers
,
189 final Set
<RecipientId
> admins
,
190 final Set
<RecipientId
> removeAdmins
,
191 final boolean resetGroupLink
,
192 final GroupLinkState groupLinkState
,
193 final GroupPermission addMemberPermission
,
194 final GroupPermission editDetailsPermission
,
195 final File avatarFile
,
196 final Integer expirationTimer
,
197 final Boolean isAnnouncementGroup
198 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
{
199 var group
= getGroupForUpdating(groupId
);
201 if (group
instanceof GroupInfoV2
) {
203 return updateGroupV2((GroupInfoV2
) group
,
213 editDetailsPermission
,
216 isAnnouncementGroup
);
217 } catch (ConflictException e
) {
218 // Detected conflicting update, refreshing group and trying again
219 group
= getGroup(groupId
, true);
220 return updateGroupV2((GroupInfoV2
) group
,
230 editDetailsPermission
,
233 isAnnouncementGroup
);
237 final var gv1
= (GroupInfoV1
) group
;
238 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
239 if (expirationTimer
!= null) {
240 setExpirationTimer(gv1
, expirationTimer
);
245 public Pair
<GroupId
, SendGroupMessageResults
> joinGroup(
246 GroupInviteLinkUrl inviteLinkUrl
247 ) throws IOException
, GroupLinkNotActiveException
{
248 final var groupJoinInfo
= groupV2Helper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
249 inviteLinkUrl
.getPassword());
250 final var groupChange
= groupV2Helper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
251 inviteLinkUrl
.getPassword(),
253 final var group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
254 groupJoinInfo
.getRevision() + 1,
255 groupChange
.toByteArray());
257 if (group
.getGroup() == null) {
258 // Only requested member, can't send update to group members
259 return new Pair
<>(group
.getGroupId(), new SendGroupMessageResults(0, List
.of()));
262 final var result
= sendUpdateGroupV2Message(group
, group
.getGroup(), groupChange
);
264 return new Pair
<>(group
.getGroupId(), result
);
267 public SendGroupMessageResults
quitGroup(
268 final GroupId groupId
, final Set
<RecipientId
> newAdmins
269 ) throws IOException
, LastGroupAdminException
, NotAGroupMemberException
, GroupNotFoundException
{
270 var group
= getGroupForUpdating(groupId
);
271 if (group
instanceof GroupInfoV1
) {
272 return quitGroupV1((GroupInfoV1
) group
);
276 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
277 } catch (ConflictException e
) {
278 // Detected conflicting update, refreshing group and trying again
279 group
= getGroup(groupId
, true);
280 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
284 public SendGroupMessageResults
sendGroupInfoRequest(
285 GroupIdV1 groupId
, RecipientId recipientId
286 ) throws IOException
{
287 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
).withId(groupId
.serialize());
289 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
.build());
291 // Send group info request message to the recipient who sent us a message with this groupId
292 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
));
295 public SendGroupMessageResults
sendGroupInfoMessage(
296 GroupIdV1 groupId
, RecipientId recipientId
297 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
299 var group
= getGroupForUpdating(groupId
);
300 if (!(group
instanceof GroupInfoV1
)) {
301 throw new IOException("Received an invalid group request for a v2 group!");
303 g
= (GroupInfoV1
) group
;
305 if (!g
.isMember(recipientId
)) {
306 throw new NotAGroupMemberException(groupId
, g
.name
);
309 var messageBuilder
= getGroupUpdateMessageBuilder(g
);
311 // Send group message only to the recipient who requested it
312 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
));
315 private GroupInfo
getGroup(GroupId groupId
, boolean forceUpdate
) {
316 final var group
= account
.getGroupStore().getGroup(groupId
);
317 if (group
instanceof GroupInfoV2
&& (forceUpdate
|| ((GroupInfoV2
) group
).getGroup() == null)) {
318 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(((GroupInfoV2
) group
).getMasterKey());
319 ((GroupInfoV2
) group
).setGroup(groupV2Helper
.getDecryptedGroup(groupSecretParams
), recipientResolver
);
320 account
.getGroupStore().updateGroup(group
);
325 private void downloadGroupAvatar(GroupIdV2 groupId
, GroupSecretParams groupSecretParams
, String cdnKey
) {
327 avatarStore
.storeGroupAvatar(groupId
,
328 outputStream
-> retrieveGroupV2Avatar(groupSecretParams
, cdnKey
, outputStream
));
329 } catch (IOException e
) {
330 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
334 private void retrieveGroupV2Avatar(
335 GroupSecretParams groupSecretParams
, String cdnKey
, OutputStream outputStream
336 ) throws IOException
{
337 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
339 var tmpFile
= IOUtils
.createTempFile();
340 try (InputStream input
= dependencies
.getMessageReceiver()
341 .retrieveGroupsV2ProfileAvatar(cdnKey
, tmpFile
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
342 var encryptedData
= IOUtils
.readFully(input
);
344 var decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
345 outputStream
.write(decryptedData
);
348 Files
.delete(tmpFile
.toPath());
349 } catch (IOException e
) {
350 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
357 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
358 for (var member
: group
.getMembersList()) {
359 final var uuid
= UuidUtil
.parseOrThrow(member
.getUuid().toByteArray());
360 final var recipientId
= account
.getRecipientStore().resolveRecipient(uuid
);
362 account
.getProfileStore()
363 .storeProfileKey(recipientId
, new ProfileKey(member
.getProfileKey().toByteArray()));
364 } catch (InvalidInputException ignored
) {
369 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
370 var g
= getGroup(groupId
);
372 throw new GroupNotFoundException(groupId
);
374 if (!g
.isMember(account
.getSelfRecipientId()) && !g
.isPendingMember(account
.getSelfRecipientId())) {
375 throw new NotAGroupMemberException(groupId
, g
.getTitle());
380 private SendGroupMessageResults
updateGroupV1(
381 final GroupInfoV1 gv1
, final String name
, final Set
<RecipientId
> members
, final File avatarFile
382 ) throws IOException
, AttachmentInvalidException
{
383 updateGroupV1Details(gv1
, name
, members
, avatarFile
);
385 account
.getGroupStore().updateGroup(gv1
);
387 var messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
388 return sendGroupMessage(messageBuilder
, gv1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()));
391 private void updateGroupV1Details(
392 final GroupInfoV1 g
, final String name
, final Collection
<RecipientId
> members
, final File avatarFile
393 ) throws IOException
{
398 if (members
!= null) {
399 g
.addMembers(members
);
402 if (avatarFile
!= null) {
403 avatarStore
.storeGroupAvatar(g
.getGroupId(),
404 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
409 * Change the expiration timer for a group
411 private void setExpirationTimer(
412 GroupInfoV1 groupInfoV1
, int messageExpirationTimer
413 ) throws NotAGroupMemberException
, GroupNotFoundException
, IOException
{
414 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
415 account
.getGroupStore().updateGroup(groupInfoV1
);
416 sendExpirationTimerUpdate(groupInfoV1
.getGroupId());
419 private void sendExpirationTimerUpdate(GroupIdV1 groupId
) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
{
420 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().asExpirationUpdate();
421 sendHelper
.sendAsGroupMessage(messageBuilder
, groupId
);
424 private SendGroupMessageResults
updateGroupV2(
425 final GroupInfoV2 group
,
427 final String description
,
428 final Set
<RecipientId
> members
,
429 final Set
<RecipientId
> removeMembers
,
430 final Set
<RecipientId
> admins
,
431 final Set
<RecipientId
> removeAdmins
,
432 final boolean resetGroupLink
,
433 final GroupLinkState groupLinkState
,
434 final GroupPermission addMemberPermission
,
435 final GroupPermission editDetailsPermission
,
436 final File avatarFile
,
437 final Integer expirationTimer
,
438 final Boolean isAnnouncementGroup
439 ) throws IOException
{
440 SendGroupMessageResults result
= null;
441 if (group
.isPendingMember(account
.getSelfRecipientId())) {
442 var groupGroupChangePair
= groupV2Helper
.acceptInvite(group
);
443 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
446 if (members
!= null) {
447 final var newMembers
= new HashSet
<>(members
);
448 newMembers
.removeAll(group
.getMembers());
449 if (newMembers
.size() > 0) {
450 var groupGroupChangePair
= groupV2Helper
.addMembers(group
, newMembers
);
451 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
455 if (removeMembers
!= null) {
456 var existingRemoveMembers
= new HashSet
<>(removeMembers
);
457 existingRemoveMembers
.retainAll(group
.getMembers());
458 existingRemoveMembers
.remove(account
.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
459 if (existingRemoveMembers
.size() > 0) {
460 var groupGroupChangePair
= groupV2Helper
.removeMembers(group
, existingRemoveMembers
);
461 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
464 var pendingRemoveMembers
= new HashSet
<>(removeMembers
);
465 pendingRemoveMembers
.retainAll(group
.getPendingMembers());
466 if (pendingRemoveMembers
.size() > 0) {
467 var groupGroupChangePair
= groupV2Helper
.revokeInvitedMembers(group
, pendingRemoveMembers
);
468 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
472 if (admins
!= null) {
473 final var newAdmins
= new HashSet
<>(admins
);
474 newAdmins
.retainAll(group
.getMembers());
475 newAdmins
.removeAll(group
.getAdminMembers());
476 if (newAdmins
.size() > 0) {
477 for (var admin
: newAdmins
) {
478 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, true);
479 result
= sendUpdateGroupV2Message(group
,
480 groupGroupChangePair
.first(),
481 groupGroupChangePair
.second());
486 if (removeAdmins
!= null) {
487 final var existingRemoveAdmins
= new HashSet
<>(removeAdmins
);
488 existingRemoveAdmins
.retainAll(group
.getAdminMembers());
489 if (existingRemoveAdmins
.size() > 0) {
490 for (var admin
: existingRemoveAdmins
) {
491 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, false);
492 result
= sendUpdateGroupV2Message(group
,
493 groupGroupChangePair
.first(),
494 groupGroupChangePair
.second());
499 if (resetGroupLink
) {
500 var groupGroupChangePair
= groupV2Helper
.resetGroupLinkPassword(group
);
501 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
504 if (groupLinkState
!= null) {
505 var groupGroupChangePair
= groupV2Helper
.setGroupLinkState(group
, groupLinkState
);
506 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
509 if (addMemberPermission
!= null) {
510 var groupGroupChangePair
= groupV2Helper
.setAddMemberPermission(group
, addMemberPermission
);
511 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
514 if (editDetailsPermission
!= null) {
515 var groupGroupChangePair
= groupV2Helper
.setEditDetailsPermission(group
, editDetailsPermission
);
516 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
519 if (expirationTimer
!= null) {
520 var groupGroupChangePair
= groupV2Helper
.setMessageExpirationTimer(group
, expirationTimer
);
521 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
524 if (isAnnouncementGroup
!= null) {
525 var groupGroupChangePair
= groupV2Helper
.setIsAnnouncementGroup(group
, isAnnouncementGroup
);
526 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
529 if (name
!= null || description
!= null || avatarFile
!= null) {
530 var groupGroupChangePair
= groupV2Helper
.updateGroup(group
, name
, description
, avatarFile
);
531 if (avatarFile
!= null) {
532 avatarStore
.storeGroupAvatar(group
.getGroupId(),
533 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
535 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
541 private SendGroupMessageResults
quitGroupV1(final GroupInfoV1 groupInfoV1
) throws IOException
{
542 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
543 .withId(groupInfoV1
.getGroupId().serialize())
546 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
547 groupInfoV1
.removeMember(account
.getSelfRecipientId());
548 account
.getGroupStore().updateGroup(groupInfoV1
);
549 return sendGroupMessage(messageBuilder
,
550 groupInfoV1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()));
553 private SendGroupMessageResults
quitGroupV2(
554 final GroupInfoV2 groupInfoV2
, final Set
<RecipientId
> newAdmins
555 ) throws LastGroupAdminException
, IOException
{
556 final var currentAdmins
= groupInfoV2
.getAdminMembers();
557 newAdmins
.removeAll(currentAdmins
);
558 newAdmins
.retainAll(groupInfoV2
.getMembers());
559 if (currentAdmins
.contains(account
.getSelfRecipientId())
560 && currentAdmins
.size() == 1
561 && groupInfoV2
.getMembers().size() > 1
562 && newAdmins
.size() == 0) {
563 // Last admin can't leave the group, unless she's also the last member
564 throw new LastGroupAdminException(groupInfoV2
.getGroupId(), groupInfoV2
.getTitle());
566 final var groupGroupChangePair
= groupV2Helper
.leaveGroup(groupInfoV2
, newAdmins
);
567 groupInfoV2
.setGroup(groupGroupChangePair
.first(), recipientResolver
);
568 account
.getGroupStore().updateGroup(groupInfoV2
);
570 var messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
571 return sendGroupMessage(messageBuilder
,
572 groupInfoV2
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()));
575 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
576 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
577 .withId(g
.getGroupId().serialize())
579 .withMembers(g
.getMembers()
581 .map(addressResolver
::resolveSignalServiceAddress
)
582 .collect(Collectors
.toList()));
585 final var attachment
= createGroupAvatarAttachment(g
.getGroupId());
586 if (attachment
.isPresent()) {
587 group
.withAvatar(attachment
.get());
589 } catch (IOException e
) {
590 throw new AttachmentInvalidException(g
.getGroupId().toBase64(), e
);
593 return SignalServiceDataMessage
.newBuilder()
594 .asGroupMessage(group
.build())
595 .withExpiration(g
.getMessageExpirationTime());
598 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
599 var group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
600 .withRevision(g
.getGroup().getRevision())
601 .withSignedGroupChange(signedGroupChange
);
602 return SignalServiceDataMessage
.newBuilder()
603 .asGroupMessage(group
.build())
604 .withExpiration(g
.getMessageExpirationTime());
607 private SendGroupMessageResults
sendUpdateGroupV2Message(
608 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
609 ) throws IOException
{
610 final var selfRecipientId
= account
.getSelfRecipientId();
611 final var members
= group
.getMembersIncludingPendingWithout(selfRecipientId
);
612 group
.setGroup(newDecryptedGroup
, recipientResolver
);
613 members
.addAll(group
.getMembersIncludingPendingWithout(selfRecipientId
));
614 account
.getGroupStore().updateGroup(group
);
616 final var messageBuilder
= getGroupUpdateMessageBuilder(group
, groupChange
.toByteArray());
617 return sendGroupMessage(messageBuilder
, members
);
620 private SendGroupMessageResults
sendGroupMessage(
621 final SignalServiceDataMessage
.Builder messageBuilder
, final Set
<RecipientId
> members
622 ) throws IOException
{
623 final var timestamp
= System
.currentTimeMillis();
624 messageBuilder
.withTimestamp(timestamp
);
625 final var results
= sendHelper
.sendGroupMessage(messageBuilder
.build(), members
);
626 return new SendGroupMessageResults(timestamp
, results
);