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
.InactiveGroupLinkException
;
7 import org
.asamk
.signal
.manager
.api
.Pair
;
8 import org
.asamk
.signal
.manager
.api
.SendGroupMessageResults
;
9 import org
.asamk
.signal
.manager
.api
.SendMessageResult
;
10 import org
.asamk
.signal
.manager
.config
.ServiceConfig
;
11 import org
.asamk
.signal
.manager
.groups
.GroupId
;
12 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
13 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
14 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
15 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
16 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
17 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
18 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
19 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
20 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
21 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
22 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
23 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
24 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
25 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
26 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
27 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
28 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
29 import org
.asamk
.signal
.manager
.util
.IOUtils
;
30 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
31 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
32 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
33 import org
.signal
.zkgroup
.InvalidInputException
;
34 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
35 import org
.signal
.zkgroup
.groups
.GroupSecretParams
;
36 import org
.signal
.zkgroup
.profiles
.ProfileKey
;
37 import org
.slf4j
.Logger
;
38 import org
.slf4j
.LoggerFactory
;
39 import org
.whispersystems
.libsignal
.util
.guava
.Optional
;
40 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
41 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
42 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
43 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
44 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
45 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
46 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
47 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
48 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ConflictException
;
51 import java
.io
.IOException
;
52 import java
.io
.InputStream
;
53 import java
.io
.OutputStream
;
54 import java
.nio
.file
.Files
;
55 import java
.util
.Collection
;
56 import java
.util
.HashSet
;
57 import java
.util
.List
;
60 public class GroupHelper
{
62 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
64 private final SignalAccount account
;
65 private final SignalDependencies dependencies
;
66 private final AttachmentHelper attachmentHelper
;
67 private final SendHelper sendHelper
;
68 private final GroupV2Helper groupV2Helper
;
69 private final AvatarStore avatarStore
;
70 private final SignalServiceAddressResolver addressResolver
;
71 private final RecipientResolver recipientResolver
;
74 final SignalAccount account
,
75 final SignalDependencies dependencies
,
76 final AttachmentHelper attachmentHelper
,
77 final SendHelper sendHelper
,
78 final GroupV2Helper groupV2Helper
,
79 final AvatarStore avatarStore
,
80 final SignalServiceAddressResolver addressResolver
,
81 final RecipientResolver recipientResolver
83 this.account
= account
;
84 this.dependencies
= dependencies
;
85 this.attachmentHelper
= attachmentHelper
;
86 this.sendHelper
= sendHelper
;
87 this.groupV2Helper
= groupV2Helper
;
88 this.avatarStore
= avatarStore
;
89 this.addressResolver
= addressResolver
;
90 this.recipientResolver
= recipientResolver
;
93 public GroupInfo
getGroup(GroupId groupId
) {
94 return getGroup(groupId
, false);
97 public boolean isGroupBlocked(final GroupId groupId
) {
98 var group
= getGroup(groupId
);
99 return group
!= null && group
.isBlocked();
102 public void downloadGroupAvatar(GroupIdV1 groupId
, SignalServiceAttachment avatar
) {
104 avatarStore
.storeGroupAvatar(groupId
,
105 outputStream
-> attachmentHelper
.retrieveAttachment(avatar
, outputStream
));
106 } catch (IOException e
) {
107 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
111 public Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupIdV1 groupId
) throws IOException
{
112 final var streamDetails
= avatarStore
.retrieveGroupAvatar(groupId
);
113 if (streamDetails
== null) {
114 return Optional
.absent();
117 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.absent()));
120 public GroupInfoV2
getOrMigrateGroup(
121 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
123 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
125 var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
126 var groupInfo
= getGroup(groupId
);
127 final GroupInfoV2 groupInfoV2
;
128 if (groupInfo
instanceof GroupInfoV1
) {
129 // Received a v2 group message for a v1 group, we need to locally migrate the group
130 account
.getGroupStore().deleteGroupV1(((GroupInfoV1
) groupInfo
).getGroupId());
131 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
132 logger
.info("Locally migrated group {} to group v2, id: {}",
133 groupInfo
.getGroupId().toBase64(),
134 groupInfoV2
.getGroupId().toBase64());
135 } else if (groupInfo
instanceof GroupInfoV2
) {
136 groupInfoV2
= (GroupInfoV2
) groupInfo
;
138 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
141 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
142 DecryptedGroup group
= null;
143 if (signedGroupChange
!= null
144 && groupInfoV2
.getGroup() != null
145 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
146 group
= groupV2Helper
.getUpdatedDecryptedGroup(groupInfoV2
.getGroup(),
152 group
= groupV2Helper
.getDecryptedGroup(groupSecretParams
);
153 } catch (NotAGroupMemberException ignored
) {
157 storeProfileKeysFromMembers(group
);
158 final var avatar
= group
.getAvatar();
159 if (avatar
!= null && !avatar
.isEmpty()) {
160 downloadGroupAvatar(groupId
, groupSecretParams
, avatar
);
163 groupInfoV2
.setGroup(group
, recipientResolver
);
164 account
.getGroupStore().updateGroup(groupInfoV2
);
170 public Pair
<GroupId
, SendGroupMessageResults
> createGroup(
171 String name
, Set
<RecipientId
> members
, File avatarFile
172 ) throws IOException
, AttachmentInvalidException
{
173 final var selfRecipientId
= account
.getSelfRecipientId();
174 if (members
!= null && members
.contains(selfRecipientId
)) {
175 members
= new HashSet
<>(members
);
176 members
.remove(selfRecipientId
);
179 var gv2Pair
= groupV2Helper
.createGroup(name
== null ?
"" : name
,
180 members
== null ? Set
.of() : members
,
183 if (gv2Pair
== null) {
184 // Failed to create v2 group, creating v1 group instead
185 var gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
186 gv1
.addMembers(List
.of(selfRecipientId
));
187 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
188 return new Pair
<>(gv1
.getGroupId(), result
);
191 final var gv2
= gv2Pair
.first();
192 final var decryptedGroup
= gv2Pair
.second();
194 gv2
.setGroup(decryptedGroup
, recipientResolver
);
195 if (avatarFile
!= null) {
196 avatarStore
.storeGroupAvatar(gv2
.getGroupId(),
197 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
200 account
.getGroupStore().updateGroup(gv2
);
202 final var messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
204 final var result
= sendGroupMessage(messageBuilder
,
205 gv2
.getMembersIncludingPendingWithout(selfRecipientId
),
206 gv2
.getDistributionId());
207 return new Pair
<>(gv2
.getGroupId(), result
);
210 public SendGroupMessageResults
updateGroup(
211 final GroupId groupId
,
213 final String description
,
214 final Set
<RecipientId
> members
,
215 final Set
<RecipientId
> removeMembers
,
216 final Set
<RecipientId
> admins
,
217 final Set
<RecipientId
> removeAdmins
,
218 final boolean resetGroupLink
,
219 final GroupLinkState groupLinkState
,
220 final GroupPermission addMemberPermission
,
221 final GroupPermission editDetailsPermission
,
222 final File avatarFile
,
223 final Integer expirationTimer
,
224 final Boolean isAnnouncementGroup
225 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
, GroupSendingNotAllowedException
{
226 var group
= getGroupForUpdating(groupId
);
228 if (group
instanceof GroupInfoV2
) {
230 return updateGroupV2((GroupInfoV2
) group
,
240 editDetailsPermission
,
243 isAnnouncementGroup
);
244 } catch (ConflictException e
) {
245 // Detected conflicting update, refreshing group and trying again
246 group
= getGroup(groupId
, true);
247 return updateGroupV2((GroupInfoV2
) group
,
257 editDetailsPermission
,
260 isAnnouncementGroup
);
264 final var gv1
= (GroupInfoV1
) group
;
265 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
266 if (expirationTimer
!= null) {
267 setExpirationTimer(gv1
, expirationTimer
);
272 public Pair
<GroupId
, SendGroupMessageResults
> joinGroup(
273 GroupInviteLinkUrl inviteLinkUrl
274 ) throws IOException
, InactiveGroupLinkException
{
275 final DecryptedGroupJoinInfo groupJoinInfo
;
277 groupJoinInfo
= groupV2Helper
.getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(),
278 inviteLinkUrl
.getPassword());
279 } catch (GroupLinkNotActiveException e
) {
280 throw new InactiveGroupLinkException("Group link inactive", e
);
282 final var groupChange
= groupV2Helper
.joinGroup(inviteLinkUrl
.getGroupMasterKey(),
283 inviteLinkUrl
.getPassword(),
285 final var group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
286 groupJoinInfo
.getRevision() + 1,
287 groupChange
.toByteArray());
289 if (group
.getGroup() == null) {
290 // Only requested member, can't send update to group members
291 return new Pair
<>(group
.getGroupId(), new SendGroupMessageResults(0, List
.of()));
294 final var result
= sendUpdateGroupV2Message(group
, group
.getGroup(), groupChange
);
296 return new Pair
<>(group
.getGroupId(), result
);
299 public SendGroupMessageResults
quitGroup(
300 final GroupId groupId
, final Set
<RecipientId
> newAdmins
301 ) throws IOException
, LastGroupAdminException
, NotAGroupMemberException
, GroupNotFoundException
{
302 var group
= getGroupForUpdating(groupId
);
303 if (group
instanceof GroupInfoV1
) {
304 return quitGroupV1((GroupInfoV1
) group
);
308 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
309 } catch (ConflictException e
) {
310 // Detected conflicting update, refreshing group and trying again
311 group
= getGroup(groupId
, true);
312 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
316 public void deleteGroup(GroupId groupId
) throws IOException
{
317 account
.getGroupStore().deleteGroup(groupId
);
318 avatarStore
.deleteGroupAvatar(groupId
);
321 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
322 var group
= getGroup(groupId
);
324 throw new GroupNotFoundException(groupId
);
327 group
.setBlocked(blocked
);
328 account
.getGroupStore().updateGroup(group
);
331 public SendGroupMessageResults
sendGroupInfoRequest(
332 GroupIdV1 groupId
, RecipientId recipientId
333 ) throws IOException
{
334 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
).withId(groupId
.serialize());
336 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
.build());
338 // Send group info request message to the recipient who sent us a message with this groupId
339 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
342 public SendGroupMessageResults
sendGroupInfoMessage(
343 GroupIdV1 groupId
, RecipientId recipientId
344 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
346 var group
= getGroupForUpdating(groupId
);
347 if (!(group
instanceof GroupInfoV1
)) {
348 throw new IOException("Received an invalid group request for a v2 group!");
350 g
= (GroupInfoV1
) group
;
352 if (!g
.isMember(recipientId
)) {
353 throw new NotAGroupMemberException(groupId
, g
.name
);
356 var messageBuilder
= getGroupUpdateMessageBuilder(g
);
358 // Send group message only to the recipient who requested it
359 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
362 private GroupInfo
getGroup(GroupId groupId
, boolean forceUpdate
) {
363 final var group
= account
.getGroupStore().getGroup(groupId
);
364 if (group
instanceof GroupInfoV2 groupInfoV2
) {
365 if (forceUpdate
|| (!groupInfoV2
.isPermissionDenied() && groupInfoV2
.getGroup() == null)) {
366 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
367 DecryptedGroup decryptedGroup
;
369 decryptedGroup
= groupV2Helper
.getDecryptedGroup(groupSecretParams
);
370 } catch (NotAGroupMemberException e
) {
371 groupInfoV2
.setPermissionDenied(true);
372 decryptedGroup
= null;
374 groupInfoV2
.setGroup(decryptedGroup
, recipientResolver
);
375 account
.getGroupStore().updateGroup(group
);
381 private void downloadGroupAvatar(GroupIdV2 groupId
, GroupSecretParams groupSecretParams
, String cdnKey
) {
383 avatarStore
.storeGroupAvatar(groupId
,
384 outputStream
-> retrieveGroupV2Avatar(groupSecretParams
, cdnKey
, outputStream
));
385 } catch (IOException e
) {
386 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
390 private void retrieveGroupV2Avatar(
391 GroupSecretParams groupSecretParams
, String cdnKey
, OutputStream outputStream
392 ) throws IOException
{
393 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
395 var tmpFile
= IOUtils
.createTempFile();
396 try (InputStream input
= dependencies
.getMessageReceiver()
397 .retrieveGroupsV2ProfileAvatar(cdnKey
, tmpFile
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
398 var encryptedData
= IOUtils
.readFully(input
);
400 var decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
401 outputStream
.write(decryptedData
);
404 Files
.delete(tmpFile
.toPath());
405 } catch (IOException e
) {
406 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
413 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
414 for (var member
: group
.getMembersList()) {
415 final var aci
= ACI
.fromByteString(member
.getUuid());
416 final var recipientId
= account
.getRecipientStore().resolveRecipient(aci
);
418 account
.getProfileStore()
419 .storeProfileKey(recipientId
, new ProfileKey(member
.getProfileKey().toByteArray()));
420 } catch (InvalidInputException ignored
) {
425 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
426 var g
= getGroup(groupId
);
428 throw new GroupNotFoundException(groupId
);
430 if (!g
.isMember(account
.getSelfRecipientId()) && !g
.isPendingMember(account
.getSelfRecipientId())) {
431 throw new NotAGroupMemberException(groupId
, g
.getTitle());
436 private SendGroupMessageResults
updateGroupV1(
437 final GroupInfoV1 gv1
, final String name
, final Set
<RecipientId
> members
, final File avatarFile
438 ) throws IOException
, AttachmentInvalidException
{
439 updateGroupV1Details(gv1
, name
, members
, avatarFile
);
441 account
.getGroupStore().updateGroup(gv1
);
443 var messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
444 return sendGroupMessage(messageBuilder
,
445 gv1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
446 gv1
.getDistributionId());
449 private void updateGroupV1Details(
450 final GroupInfoV1 g
, final String name
, final Collection
<RecipientId
> members
, final File avatarFile
451 ) throws IOException
{
456 if (members
!= null) {
457 g
.addMembers(members
);
460 if (avatarFile
!= null) {
461 avatarStore
.storeGroupAvatar(g
.getGroupId(),
462 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
467 * Change the expiration timer for a group
469 private void setExpirationTimer(
470 GroupInfoV1 groupInfoV1
, int messageExpirationTimer
471 ) throws NotAGroupMemberException
, GroupNotFoundException
, IOException
, GroupSendingNotAllowedException
{
472 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
473 account
.getGroupStore().updateGroup(groupInfoV1
);
474 sendExpirationTimerUpdate(groupInfoV1
.getGroupId());
477 private void sendExpirationTimerUpdate(GroupIdV1 groupId
) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
478 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().asExpirationUpdate();
479 sendHelper
.sendAsGroupMessage(messageBuilder
, groupId
);
482 private SendGroupMessageResults
updateGroupV2(
483 final GroupInfoV2 group
,
485 final String description
,
486 final Set
<RecipientId
> members
,
487 final Set
<RecipientId
> removeMembers
,
488 final Set
<RecipientId
> admins
,
489 final Set
<RecipientId
> removeAdmins
,
490 final boolean resetGroupLink
,
491 final GroupLinkState groupLinkState
,
492 final GroupPermission addMemberPermission
,
493 final GroupPermission editDetailsPermission
,
494 final File avatarFile
,
495 final Integer expirationTimer
,
496 final Boolean isAnnouncementGroup
497 ) throws IOException
{
498 SendGroupMessageResults result
= null;
499 if (group
.isPendingMember(account
.getSelfRecipientId())) {
500 var groupGroupChangePair
= groupV2Helper
.acceptInvite(group
);
501 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
504 if (members
!= null) {
505 final var newMembers
= new HashSet
<>(members
);
506 newMembers
.removeAll(group
.getMembers());
507 if (newMembers
.size() > 0) {
508 var groupGroupChangePair
= groupV2Helper
.addMembers(group
, newMembers
);
509 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
513 if (removeMembers
!= null) {
514 var existingRemoveMembers
= new HashSet
<>(removeMembers
);
515 existingRemoveMembers
.retainAll(group
.getMembers());
516 existingRemoveMembers
.remove(account
.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
517 if (existingRemoveMembers
.size() > 0) {
518 var groupGroupChangePair
= groupV2Helper
.removeMembers(group
, existingRemoveMembers
);
519 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
522 var pendingRemoveMembers
= new HashSet
<>(removeMembers
);
523 pendingRemoveMembers
.retainAll(group
.getPendingMembers());
524 if (pendingRemoveMembers
.size() > 0) {
525 var groupGroupChangePair
= groupV2Helper
.revokeInvitedMembers(group
, pendingRemoveMembers
);
526 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
530 if (admins
!= null) {
531 final var newAdmins
= new HashSet
<>(admins
);
532 newAdmins
.retainAll(group
.getMembers());
533 newAdmins
.removeAll(group
.getAdminMembers());
534 if (newAdmins
.size() > 0) {
535 for (var admin
: newAdmins
) {
536 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, true);
537 result
= sendUpdateGroupV2Message(group
,
538 groupGroupChangePair
.first(),
539 groupGroupChangePair
.second());
544 if (removeAdmins
!= null) {
545 final var existingRemoveAdmins
= new HashSet
<>(removeAdmins
);
546 existingRemoveAdmins
.retainAll(group
.getAdminMembers());
547 if (existingRemoveAdmins
.size() > 0) {
548 for (var admin
: existingRemoveAdmins
) {
549 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, false);
550 result
= sendUpdateGroupV2Message(group
,
551 groupGroupChangePair
.first(),
552 groupGroupChangePair
.second());
557 if (resetGroupLink
) {
558 var groupGroupChangePair
= groupV2Helper
.resetGroupLinkPassword(group
);
559 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
562 if (groupLinkState
!= null) {
563 var groupGroupChangePair
= groupV2Helper
.setGroupLinkState(group
, groupLinkState
);
564 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
567 if (addMemberPermission
!= null) {
568 var groupGroupChangePair
= groupV2Helper
.setAddMemberPermission(group
, addMemberPermission
);
569 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
572 if (editDetailsPermission
!= null) {
573 var groupGroupChangePair
= groupV2Helper
.setEditDetailsPermission(group
, editDetailsPermission
);
574 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
577 if (expirationTimer
!= null) {
578 var groupGroupChangePair
= groupV2Helper
.setMessageExpirationTimer(group
, expirationTimer
);
579 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
582 if (isAnnouncementGroup
!= null) {
583 var groupGroupChangePair
= groupV2Helper
.setIsAnnouncementGroup(group
, isAnnouncementGroup
);
584 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
587 if (name
!= null || description
!= null || avatarFile
!= null) {
588 var groupGroupChangePair
= groupV2Helper
.updateGroup(group
, name
, description
, avatarFile
);
589 if (avatarFile
!= null) {
590 avatarStore
.storeGroupAvatar(group
.getGroupId(),
591 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
593 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
599 private SendGroupMessageResults
quitGroupV1(final GroupInfoV1 groupInfoV1
) throws IOException
{
600 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
601 .withId(groupInfoV1
.getGroupId().serialize())
604 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
605 groupInfoV1
.removeMember(account
.getSelfRecipientId());
606 account
.getGroupStore().updateGroup(groupInfoV1
);
607 return sendGroupMessage(messageBuilder
,
608 groupInfoV1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
609 groupInfoV1
.getDistributionId());
612 private SendGroupMessageResults
quitGroupV2(
613 final GroupInfoV2 groupInfoV2
, final Set
<RecipientId
> newAdmins
614 ) throws LastGroupAdminException
, IOException
{
615 final var currentAdmins
= groupInfoV2
.getAdminMembers();
616 newAdmins
.removeAll(currentAdmins
);
617 newAdmins
.retainAll(groupInfoV2
.getMembers());
618 if (currentAdmins
.contains(account
.getSelfRecipientId())
619 && currentAdmins
.size() == 1
620 && groupInfoV2
.getMembers().size() > 1
621 && newAdmins
.size() == 0) {
622 // Last admin can't leave the group, unless she's also the last member
623 throw new LastGroupAdminException(groupInfoV2
.getGroupId(), groupInfoV2
.getTitle());
625 final var groupGroupChangePair
= groupV2Helper
.leaveGroup(groupInfoV2
, newAdmins
);
626 groupInfoV2
.setGroup(groupGroupChangePair
.first(), recipientResolver
);
627 account
.getGroupStore().updateGroup(groupInfoV2
);
629 var messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
630 return sendGroupMessage(messageBuilder
,
631 groupInfoV2
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
632 groupInfoV2
.getDistributionId());
635 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
636 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
637 .withId(g
.getGroupId().serialize())
639 .withMembers(g
.getMembers().stream().map(addressResolver
::resolveSignalServiceAddress
).toList());
642 final var attachment
= createGroupAvatarAttachment(g
.getGroupId());
643 if (attachment
.isPresent()) {
644 group
.withAvatar(attachment
.get());
646 } catch (IOException e
) {
647 throw new AttachmentInvalidException(g
.getGroupId().toBase64(), e
);
650 return SignalServiceDataMessage
.newBuilder()
651 .asGroupMessage(group
.build())
652 .withExpiration(g
.getMessageExpirationTimer());
655 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
656 var group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
657 .withRevision(g
.getGroup().getRevision())
658 .withSignedGroupChange(signedGroupChange
);
659 return SignalServiceDataMessage
.newBuilder()
660 .asGroupMessage(group
.build())
661 .withExpiration(g
.getMessageExpirationTimer());
664 private SendGroupMessageResults
sendUpdateGroupV2Message(
665 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
666 ) throws IOException
{
667 final var selfRecipientId
= account
.getSelfRecipientId();
668 final var members
= group
.getMembersIncludingPendingWithout(selfRecipientId
);
669 group
.setGroup(newDecryptedGroup
, recipientResolver
);
670 members
.addAll(group
.getMembersIncludingPendingWithout(selfRecipientId
));
671 account
.getGroupStore().updateGroup(group
);
673 final var messageBuilder
= getGroupUpdateMessageBuilder(group
, groupChange
.toByteArray());
674 return sendGroupMessage(messageBuilder
, members
, group
.getDistributionId());
677 private SendGroupMessageResults
sendGroupMessage(
678 final SignalServiceDataMessage
.Builder messageBuilder
,
679 final Set
<RecipientId
> members
,
680 final DistributionId distributionId
681 ) throws IOException
{
682 final var timestamp
= System
.currentTimeMillis();
683 messageBuilder
.withTimestamp(timestamp
);
684 final var results
= sendHelper
.sendGroupMessage(messageBuilder
.build(), members
, distributionId
);
685 return new SendGroupMessageResults(timestamp
,
687 .map(sendMessageResult
-> SendMessageResult
.from(sendMessageResult
,
689 account
.getRecipientStore()::resolveRecipientAddress
))