1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.SignalDependencies
;
4 import org
.asamk
.signal
.manager
.api
.AttachmentInvalidException
;
5 import org
.asamk
.signal
.manager
.api
.InactiveGroupLinkException
;
6 import org
.asamk
.signal
.manager
.api
.Pair
;
7 import org
.asamk
.signal
.manager
.api
.SendGroupMessageResults
;
8 import org
.asamk
.signal
.manager
.api
.SendMessageResult
;
9 import org
.asamk
.signal
.manager
.config
.ServiceConfig
;
10 import org
.asamk
.signal
.manager
.groups
.GroupId
;
11 import org
.asamk
.signal
.manager
.groups
.GroupIdV1
;
12 import org
.asamk
.signal
.manager
.groups
.GroupIdV2
;
13 import org
.asamk
.signal
.manager
.groups
.GroupInviteLinkUrl
;
14 import org
.asamk
.signal
.manager
.groups
.GroupLinkState
;
15 import org
.asamk
.signal
.manager
.groups
.GroupNotFoundException
;
16 import org
.asamk
.signal
.manager
.groups
.GroupPermission
;
17 import org
.asamk
.signal
.manager
.groups
.GroupSendingNotAllowedException
;
18 import org
.asamk
.signal
.manager
.groups
.GroupUtils
;
19 import org
.asamk
.signal
.manager
.groups
.LastGroupAdminException
;
20 import org
.asamk
.signal
.manager
.groups
.NotAGroupMemberException
;
21 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
22 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfo
;
23 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV1
;
24 import org
.asamk
.signal
.manager
.storage
.groups
.GroupInfoV2
;
25 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
26 import org
.asamk
.signal
.manager
.util
.AttachmentUtils
;
27 import org
.asamk
.signal
.manager
.util
.IOUtils
;
28 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
29 import org
.signal
.libsignal
.zkgroup
.groups
.GroupMasterKey
;
30 import org
.signal
.libsignal
.zkgroup
.groups
.GroupSecretParams
;
31 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
32 import org
.signal
.storageservice
.protos
.groups
.GroupChange
;
33 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
34 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
35 import org
.slf4j
.Logger
;
36 import org
.slf4j
.LoggerFactory
;
37 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
38 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
39 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
40 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
41 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
42 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
43 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
44 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
45 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ConflictException
;
48 import java
.io
.IOException
;
49 import java
.io
.InputStream
;
50 import java
.io
.OutputStream
;
51 import java
.nio
.file
.Files
;
52 import java
.util
.Collection
;
53 import java
.util
.HashSet
;
54 import java
.util
.List
;
55 import java
.util
.Optional
;
58 public class GroupHelper
{
60 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
62 private final SignalAccount account
;
63 private final SignalDependencies dependencies
;
64 private final Context context
;
66 public GroupHelper(final Context context
) {
67 this.account
= context
.getAccount();
68 this.dependencies
= context
.getDependencies();
69 this.context
= context
;
72 public GroupInfo
getGroup(GroupId groupId
) {
73 return getGroup(groupId
, false);
76 public boolean isGroupBlocked(final GroupId groupId
) {
77 var group
= getGroup(groupId
);
78 return group
!= null && group
.isBlocked();
81 public void downloadGroupAvatar(GroupIdV1 groupId
, SignalServiceAttachment avatar
) {
83 context
.getAvatarStore()
84 .storeGroupAvatar(groupId
,
85 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
86 } catch (IOException e
) {
87 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
91 public Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupIdV1 groupId
) throws IOException
{
92 final var streamDetails
= context
.getAvatarStore().retrieveGroupAvatar(groupId
);
93 if (streamDetails
== null) {
94 return Optional
.empty();
97 return Optional
.of(AttachmentUtils
.createAttachment(streamDetails
, Optional
.empty()));
100 public GroupInfoV2
getOrMigrateGroup(
101 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
103 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
105 var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
106 var groupInfo
= getGroup(groupId
);
107 final GroupInfoV2 groupInfoV2
;
108 if (groupInfo
instanceof GroupInfoV1
) {
109 // Received a v2 group message for a v1 group, we need to locally migrate the group
110 account
.getGroupStore().deleteGroupV1(((GroupInfoV1
) groupInfo
).getGroupId());
111 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
112 logger
.info("Locally migrated group {} to group v2, id: {}",
113 groupInfo
.getGroupId().toBase64(),
114 groupInfoV2
.getGroupId().toBase64());
115 } else if (groupInfo
instanceof GroupInfoV2
) {
116 groupInfoV2
= (GroupInfoV2
) groupInfo
;
118 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
);
121 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
122 DecryptedGroup group
= null;
123 if (signedGroupChange
!= null
124 && groupInfoV2
.getGroup() != null
125 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
126 group
= context
.getGroupV2Helper()
127 .getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), signedGroupChange
, groupMasterKey
);
131 group
= context
.getGroupV2Helper().getDecryptedGroup(groupSecretParams
);
132 } catch (NotAGroupMemberException ignored
) {
136 storeProfileKeysFromMembers(group
);
137 final var avatar
= group
.getAvatar();
138 if (avatar
!= null && !avatar
.isEmpty()) {
139 downloadGroupAvatar(groupId
, groupSecretParams
, avatar
);
142 groupInfoV2
.setGroup(group
, account
.getRecipientStore());
143 account
.getGroupStore().updateGroup(groupInfoV2
);
149 public Pair
<GroupId
, SendGroupMessageResults
> createGroup(
150 String name
, Set
<RecipientId
> members
, File avatarFile
151 ) throws IOException
, AttachmentInvalidException
{
152 final var selfRecipientId
= account
.getSelfRecipientId();
153 if (members
!= null && members
.contains(selfRecipientId
)) {
154 members
= new HashSet
<>(members
);
155 members
.remove(selfRecipientId
);
158 var gv2Pair
= context
.getGroupV2Helper()
159 .createGroup(name
== null ?
"" : name
, members
== null ? Set
.of() : members
, avatarFile
);
161 if (gv2Pair
== null) {
162 // Failed to create v2 group, creating v1 group instead
163 var gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
164 gv1
.addMembers(List
.of(selfRecipientId
));
165 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
166 return new Pair
<>(gv1
.getGroupId(), result
);
169 final var gv2
= gv2Pair
.first();
170 final var decryptedGroup
= gv2Pair
.second();
172 gv2
.setGroup(decryptedGroup
, account
.getRecipientStore());
173 if (avatarFile
!= null) {
174 context
.getAvatarStore()
175 .storeGroupAvatar(gv2
.getGroupId(),
176 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
179 account
.getGroupStore().updateGroup(gv2
);
181 final var messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
183 final var result
= sendGroupMessage(messageBuilder
,
184 gv2
.getMembersIncludingPendingWithout(selfRecipientId
),
185 gv2
.getDistributionId());
186 return new Pair
<>(gv2
.getGroupId(), result
);
189 public SendGroupMessageResults
updateGroup(
190 final GroupId groupId
,
192 final String description
,
193 final Set
<RecipientId
> members
,
194 final Set
<RecipientId
> removeMembers
,
195 final Set
<RecipientId
> admins
,
196 final Set
<RecipientId
> removeAdmins
,
197 final Set
<RecipientId
> banMembers
,
198 final Set
<RecipientId
> unbanMembers
,
199 final boolean resetGroupLink
,
200 final GroupLinkState groupLinkState
,
201 final GroupPermission addMemberPermission
,
202 final GroupPermission editDetailsPermission
,
203 final File avatarFile
,
204 final Integer expirationTimer
,
205 final Boolean isAnnouncementGroup
206 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
, GroupSendingNotAllowedException
{
207 var group
= getGroupForUpdating(groupId
);
209 if (group
instanceof GroupInfoV2
) {
211 return updateGroupV2((GroupInfoV2
) group
,
223 editDetailsPermission
,
226 isAnnouncementGroup
);
227 } catch (ConflictException e
) {
228 // Detected conflicting update, refreshing group and trying again
229 group
= getGroup(groupId
, true);
230 return updateGroupV2((GroupInfoV2
) group
,
242 editDetailsPermission
,
245 isAnnouncementGroup
);
249 final var gv1
= (GroupInfoV1
) group
;
250 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
251 if (expirationTimer
!= null) {
252 setExpirationTimer(gv1
, expirationTimer
);
257 public void updateGroupProfileKey(GroupIdV2 groupId
) throws GroupNotFoundException
, NotAGroupMemberException
, IOException
{
258 var group
= getGroupForUpdating(groupId
);
260 if (group
instanceof GroupInfoV2 groupInfoV2
) {
261 Pair
<DecryptedGroup
, GroupChange
> groupChangePair
;
263 groupChangePair
= context
.getGroupV2Helper().updateSelfProfileKey(groupInfoV2
);
264 } catch (ConflictException e
) {
265 // Detected conflicting update, refreshing group and trying again
266 groupInfoV2
= (GroupInfoV2
) getGroup(groupId
, true);
267 groupChangePair
= context
.getGroupV2Helper().updateSelfProfileKey(groupInfoV2
);
269 if (groupChangePair
!= null) {
270 sendUpdateGroupV2Message(groupInfoV2
, groupChangePair
.first(), groupChangePair
.second());
275 public Pair
<GroupId
, SendGroupMessageResults
> joinGroup(
276 GroupInviteLinkUrl inviteLinkUrl
277 ) throws IOException
, InactiveGroupLinkException
{
278 final DecryptedGroupJoinInfo groupJoinInfo
;
280 groupJoinInfo
= context
.getGroupV2Helper()
281 .getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(), inviteLinkUrl
.getPassword());
282 } catch (GroupLinkNotActiveException e
) {
283 throw new InactiveGroupLinkException("Group link inactive (reason: " + e
.getReason() + ")", e
);
285 final var groupChange
= context
.getGroupV2Helper()
286 .joinGroup(inviteLinkUrl
.getGroupMasterKey(), inviteLinkUrl
.getPassword(), groupJoinInfo
);
287 final var group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
288 groupJoinInfo
.getRevision() + 1,
289 groupChange
.toByteArray());
291 if (group
.getGroup() == null) {
292 // Only requested member, can't send update to group members
293 return new Pair
<>(group
.getGroupId(), new SendGroupMessageResults(0, List
.of()));
296 final var result
= sendUpdateGroupV2Message(group
, group
.getGroup(), groupChange
);
298 return new Pair
<>(group
.getGroupId(), result
);
301 public SendGroupMessageResults
quitGroup(
302 final GroupId groupId
, final Set
<RecipientId
> newAdmins
303 ) throws IOException
, LastGroupAdminException
, NotAGroupMemberException
, GroupNotFoundException
{
304 var group
= getGroupForUpdating(groupId
);
305 if (group
instanceof GroupInfoV1
) {
306 return quitGroupV1((GroupInfoV1
) group
);
310 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
311 } catch (ConflictException e
) {
312 // Detected conflicting update, refreshing group and trying again
313 group
= getGroup(groupId
, true);
314 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
318 public void deleteGroup(GroupId groupId
) throws IOException
{
319 account
.getGroupStore().deleteGroup(groupId
);
320 context
.getAvatarStore().deleteGroupAvatar(groupId
);
323 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
324 var group
= getGroup(groupId
);
326 throw new GroupNotFoundException(groupId
);
329 group
.setBlocked(blocked
);
330 account
.getGroupStore().updateGroup(group
);
333 public SendGroupMessageResults
sendGroupInfoRequest(
334 GroupIdV1 groupId
, RecipientId recipientId
335 ) throws IOException
{
336 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
).withId(groupId
.serialize());
338 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
.build());
340 // Send group info request message to the recipient who sent us a message with this groupId
341 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
344 public SendGroupMessageResults
sendGroupInfoMessage(
345 GroupIdV1 groupId
, RecipientId recipientId
346 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
348 var group
= getGroupForUpdating(groupId
);
349 if (!(group
instanceof GroupInfoV1
)) {
350 throw new IOException("Received an invalid group request for a v2 group!");
352 g
= (GroupInfoV1
) group
;
354 if (!g
.isMember(recipientId
)) {
355 throw new NotAGroupMemberException(groupId
, g
.name
);
358 var messageBuilder
= getGroupUpdateMessageBuilder(g
);
360 // Send group message only to the recipient who requested it
361 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
364 private GroupInfo
getGroup(GroupId groupId
, boolean forceUpdate
) {
365 final var group
= account
.getGroupStore().getGroup(groupId
);
366 if (group
instanceof GroupInfoV2 groupInfoV2
) {
367 if (forceUpdate
|| (!groupInfoV2
.isPermissionDenied() && groupInfoV2
.getGroup() == null)) {
368 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
369 DecryptedGroup decryptedGroup
;
371 decryptedGroup
= context
.getGroupV2Helper().getDecryptedGroup(groupSecretParams
);
372 } catch (NotAGroupMemberException e
) {
373 groupInfoV2
.setPermissionDenied(true);
374 decryptedGroup
= null;
376 groupInfoV2
.setGroup(decryptedGroup
, account
.getRecipientStore());
377 account
.getGroupStore().updateGroup(group
);
383 private void downloadGroupAvatar(GroupIdV2 groupId
, GroupSecretParams groupSecretParams
, String cdnKey
) {
385 context
.getAvatarStore()
386 .storeGroupAvatar(groupId
,
387 outputStream
-> retrieveGroupV2Avatar(groupSecretParams
, cdnKey
, outputStream
));
388 } catch (IOException e
) {
389 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
393 private void retrieveGroupV2Avatar(
394 GroupSecretParams groupSecretParams
, String cdnKey
, OutputStream outputStream
395 ) throws IOException
{
396 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
398 var tmpFile
= IOUtils
.createTempFile();
399 try (InputStream input
= dependencies
.getMessageReceiver()
400 .retrieveGroupsV2ProfileAvatar(cdnKey
, tmpFile
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
401 var encryptedData
= IOUtils
.readFully(input
);
403 var decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
404 outputStream
.write(decryptedData
);
407 Files
.delete(tmpFile
.toPath());
408 } catch (IOException e
) {
409 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
416 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
417 for (var member
: group
.getMembersList()) {
418 final var serviceId
= ServiceId
.fromByteString(member
.getUuid());
419 final var recipientId
= account
.getRecipientStore().resolveRecipient(serviceId
);
421 account
.getProfileStore()
422 .storeProfileKey(recipientId
, new ProfileKey(member
.getProfileKey().toByteArray()));
423 } catch (InvalidInputException ignored
) {
428 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
429 var g
= getGroup(groupId
);
431 throw new GroupNotFoundException(groupId
);
433 if (!g
.isMember(account
.getSelfRecipientId()) && !g
.isPendingMember(account
.getSelfRecipientId())) {
434 throw new NotAGroupMemberException(groupId
, g
.getTitle());
439 private SendGroupMessageResults
updateGroupV1(
440 final GroupInfoV1 gv1
, final String name
, final Set
<RecipientId
> members
, final File avatarFile
441 ) throws IOException
, AttachmentInvalidException
{
442 updateGroupV1Details(gv1
, name
, members
, avatarFile
);
444 account
.getGroupStore().updateGroup(gv1
);
446 var messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
447 return sendGroupMessage(messageBuilder
,
448 gv1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
449 gv1
.getDistributionId());
452 private void updateGroupV1Details(
453 final GroupInfoV1 g
, final String name
, final Collection
<RecipientId
> members
, final File avatarFile
454 ) throws IOException
{
459 if (members
!= null) {
460 g
.addMembers(members
);
463 if (avatarFile
!= null) {
464 context
.getAvatarStore()
465 .storeGroupAvatar(g
.getGroupId(),
466 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
471 * Change the expiration timer for a group
473 private void setExpirationTimer(
474 GroupInfoV1 groupInfoV1
, int messageExpirationTimer
475 ) throws NotAGroupMemberException
, GroupNotFoundException
, IOException
, GroupSendingNotAllowedException
{
476 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
477 account
.getGroupStore().updateGroup(groupInfoV1
);
478 sendExpirationTimerUpdate(groupInfoV1
.getGroupId());
481 private void sendExpirationTimerUpdate(GroupIdV1 groupId
) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
482 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().asExpirationUpdate();
483 context
.getSendHelper().sendAsGroupMessage(messageBuilder
, groupId
);
486 private SendGroupMessageResults
updateGroupV2(
487 final GroupInfoV2 group
,
489 final String description
,
490 final Set
<RecipientId
> members
,
491 final Set
<RecipientId
> removeMembers
,
492 final Set
<RecipientId
> admins
,
493 final Set
<RecipientId
> removeAdmins
,
494 final Set
<RecipientId
> banMembers
,
495 final Set
<RecipientId
> unbanMembers
,
496 final boolean resetGroupLink
,
497 final GroupLinkState groupLinkState
,
498 final GroupPermission addMemberPermission
,
499 final GroupPermission editDetailsPermission
,
500 final File avatarFile
,
501 final Integer expirationTimer
,
502 final Boolean isAnnouncementGroup
503 ) throws IOException
{
504 SendGroupMessageResults result
= null;
505 final var groupV2Helper
= context
.getGroupV2Helper();
506 if (group
.isPendingMember(account
.getSelfRecipientId())) {
507 var groupGroupChangePair
= groupV2Helper
.acceptInvite(group
);
508 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
511 if (members
!= null) {
512 final var newMembers
= new HashSet
<>(members
);
513 newMembers
.removeAll(group
.getMembers());
514 if (newMembers
.size() > 0) {
515 var groupGroupChangePair
= groupV2Helper
.addMembers(group
, newMembers
);
516 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
520 if (removeMembers
!= null) {
521 var existingRemoveMembers
= new HashSet
<>(removeMembers
);
522 if (banMembers
!= null) {
523 existingRemoveMembers
.addAll(banMembers
);
525 existingRemoveMembers
.retainAll(group
.getMembers());
526 if (members
!= null) {
527 existingRemoveMembers
.removeAll(members
);
529 existingRemoveMembers
.remove(account
.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
530 if (existingRemoveMembers
.size() > 0) {
531 var groupGroupChangePair
= groupV2Helper
.removeMembers(group
, existingRemoveMembers
);
532 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
535 var pendingRemoveMembers
= new HashSet
<>(removeMembers
);
536 pendingRemoveMembers
.retainAll(group
.getPendingMembers());
537 if (pendingRemoveMembers
.size() > 0) {
538 var groupGroupChangePair
= groupV2Helper
.revokeInvitedMembers(group
, pendingRemoveMembers
);
539 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
543 if (admins
!= null) {
544 final var newAdmins
= new HashSet
<>(admins
);
545 newAdmins
.retainAll(group
.getMembers());
546 newAdmins
.removeAll(group
.getAdminMembers());
547 if (newAdmins
.size() > 0) {
548 for (var admin
: newAdmins
) {
549 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, true);
550 result
= sendUpdateGroupV2Message(group
,
551 groupGroupChangePair
.first(),
552 groupGroupChangePair
.second());
557 if (removeAdmins
!= null) {
558 final var existingRemoveAdmins
= new HashSet
<>(removeAdmins
);
559 existingRemoveAdmins
.retainAll(group
.getAdminMembers());
560 if (existingRemoveAdmins
.size() > 0) {
561 for (var admin
: existingRemoveAdmins
) {
562 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, false);
563 result
= sendUpdateGroupV2Message(group
,
564 groupGroupChangePair
.first(),
565 groupGroupChangePair
.second());
570 if (banMembers
!= null) {
571 final var newlyBannedMembers
= new HashSet
<>(banMembers
);
572 newlyBannedMembers
.removeAll(group
.getBannedMembers());
573 if (newlyBannedMembers
.size() > 0) {
574 var groupGroupChangePair
= groupV2Helper
.banMembers(group
, newlyBannedMembers
);
575 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
579 if (unbanMembers
!= null) {
580 var existingUnbanMembers
= new HashSet
<>(unbanMembers
);
581 existingUnbanMembers
.retainAll(group
.getBannedMembers());
582 if (existingUnbanMembers
.size() > 0) {
583 var groupGroupChangePair
= groupV2Helper
.unbanMembers(group
, existingUnbanMembers
);
584 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
588 if (resetGroupLink
) {
589 var groupGroupChangePair
= groupV2Helper
.resetGroupLinkPassword(group
);
590 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
593 if (groupLinkState
!= null) {
594 var groupGroupChangePair
= groupV2Helper
.setGroupLinkState(group
, groupLinkState
);
595 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
598 if (addMemberPermission
!= null) {
599 var groupGroupChangePair
= groupV2Helper
.setAddMemberPermission(group
, addMemberPermission
);
600 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
603 if (editDetailsPermission
!= null) {
604 var groupGroupChangePair
= groupV2Helper
.setEditDetailsPermission(group
, editDetailsPermission
);
605 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
608 if (expirationTimer
!= null) {
609 var groupGroupChangePair
= groupV2Helper
.setMessageExpirationTimer(group
, expirationTimer
);
610 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
613 if (isAnnouncementGroup
!= null) {
614 var groupGroupChangePair
= groupV2Helper
.setIsAnnouncementGroup(group
, isAnnouncementGroup
);
615 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
618 if (name
!= null || description
!= null || avatarFile
!= null) {
619 var groupGroupChangePair
= groupV2Helper
.updateGroup(group
, name
, description
, avatarFile
);
620 if (avatarFile
!= null) {
621 context
.getAvatarStore()
622 .storeGroupAvatar(group
.getGroupId(),
623 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
625 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
631 private SendGroupMessageResults
quitGroupV1(final GroupInfoV1 groupInfoV1
) throws IOException
{
632 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
633 .withId(groupInfoV1
.getGroupId().serialize())
636 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
637 groupInfoV1
.removeMember(account
.getSelfRecipientId());
638 account
.getGroupStore().updateGroup(groupInfoV1
);
639 return sendGroupMessage(messageBuilder
,
640 groupInfoV1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
641 groupInfoV1
.getDistributionId());
644 private SendGroupMessageResults
quitGroupV2(
645 final GroupInfoV2 groupInfoV2
, final Set
<RecipientId
> newAdmins
646 ) throws LastGroupAdminException
, IOException
{
647 final var currentAdmins
= groupInfoV2
.getAdminMembers();
648 newAdmins
.removeAll(currentAdmins
);
649 newAdmins
.retainAll(groupInfoV2
.getMembers());
650 if (currentAdmins
.contains(account
.getSelfRecipientId())
651 && currentAdmins
.size() == 1
652 && groupInfoV2
.getMembers().size() > 1
653 && newAdmins
.size() == 0) {
654 // Last admin can't leave the group, unless she's also the last member
655 throw new LastGroupAdminException(groupInfoV2
.getGroupId(), groupInfoV2
.getTitle());
657 final var groupGroupChangePair
= context
.getGroupV2Helper().leaveGroup(groupInfoV2
, newAdmins
);
658 groupInfoV2
.setGroup(groupGroupChangePair
.first(), account
.getRecipientStore());
659 account
.getGroupStore().updateGroup(groupInfoV2
);
661 var messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
662 return sendGroupMessage(messageBuilder
,
663 groupInfoV2
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
664 groupInfoV2
.getDistributionId());
667 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
668 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
669 .withId(g
.getGroupId().serialize())
671 .withMembers(g
.getMembers()
673 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
677 final var attachment
= createGroupAvatarAttachment(g
.getGroupId());
678 attachment
.ifPresent(group
::withAvatar
);
679 } catch (IOException e
) {
680 throw new AttachmentInvalidException(g
.getGroupId().toBase64(), e
);
683 return SignalServiceDataMessage
.newBuilder()
684 .asGroupMessage(group
.build())
685 .withExpiration(g
.getMessageExpirationTimer());
688 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
689 var group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
690 .withRevision(g
.getGroup().getRevision())
691 .withSignedGroupChange(signedGroupChange
);
692 return SignalServiceDataMessage
.newBuilder()
693 .asGroupMessage(group
.build())
694 .withExpiration(g
.getMessageExpirationTimer());
697 private SendGroupMessageResults
sendUpdateGroupV2Message(
698 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
699 ) throws IOException
{
700 final var selfRecipientId
= account
.getSelfRecipientId();
701 final var members
= group
.getMembersIncludingPendingWithout(selfRecipientId
);
702 group
.setGroup(newDecryptedGroup
, account
.getRecipientStore());
703 members
.addAll(group
.getMembersIncludingPendingWithout(selfRecipientId
));
704 account
.getGroupStore().updateGroup(group
);
706 final var messageBuilder
= getGroupUpdateMessageBuilder(group
, groupChange
.toByteArray());
707 return sendGroupMessage(messageBuilder
, members
, group
.getDistributionId());
710 private SendGroupMessageResults
sendGroupMessage(
711 final SignalServiceDataMessage
.Builder messageBuilder
,
712 final Set
<RecipientId
> members
,
713 final DistributionId distributionId
714 ) throws IOException
{
715 final var timestamp
= System
.currentTimeMillis();
716 messageBuilder
.withTimestamp(timestamp
);
717 final var results
= context
.getSendHelper().sendGroupMessage(messageBuilder
.build(), members
, distributionId
);
718 return new SendGroupMessageResults(timestamp
,
720 .map(sendMessageResult
-> SendMessageResult
.from(sendMessageResult
,
721 account
.getRecipientStore(),
722 account
.getRecipientStore()::resolveRecipientAddress
))