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
.DecryptedGroupChange
;
35 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroupJoinInfo
;
36 import org
.slf4j
.Logger
;
37 import org
.slf4j
.LoggerFactory
;
38 import org
.whispersystems
.signalservice
.api
.groupsv2
.DecryptedGroupHistoryEntry
;
39 import org
.whispersystems
.signalservice
.api
.groupsv2
.GroupLinkNotActiveException
;
40 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachment
;
41 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceAttachmentStream
;
42 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceDataMessage
;
43 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroup
;
44 import org
.whispersystems
.signalservice
.api
.messages
.SignalServiceGroupV2
;
45 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
46 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
47 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.ConflictException
;
50 import java
.io
.IOException
;
51 import java
.io
.InputStream
;
52 import java
.io
.OutputStream
;
53 import java
.nio
.file
.Files
;
54 import java
.util
.Collection
;
55 import java
.util
.HashMap
;
56 import java
.util
.HashSet
;
57 import java
.util
.List
;
58 import java
.util
.Objects
;
59 import java
.util
.Optional
;
62 public class GroupHelper
{
64 private final static Logger logger
= LoggerFactory
.getLogger(GroupHelper
.class);
66 private final SignalAccount account
;
67 private final SignalDependencies dependencies
;
68 private final Context context
;
70 public GroupHelper(final Context context
) {
71 this.account
= context
.getAccount();
72 this.dependencies
= context
.getDependencies();
73 this.context
= context
;
76 public GroupInfo
getGroup(GroupId groupId
) {
77 return getGroup(groupId
, false);
80 public boolean isGroupBlocked(final GroupId groupId
) {
81 var group
= getGroup(groupId
);
82 return group
!= null && group
.isBlocked();
85 public void downloadGroupAvatar(GroupIdV1 groupId
, SignalServiceAttachment avatar
) {
87 context
.getAvatarStore()
88 .storeGroupAvatar(groupId
,
89 outputStream
-> context
.getAttachmentHelper().retrieveAttachment(avatar
, outputStream
));
90 } catch (IOException e
) {
91 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
95 public Optional
<SignalServiceAttachmentStream
> createGroupAvatarAttachment(GroupIdV1 groupId
) throws IOException
{
96 final var streamDetails
= context
.getAvatarStore().retrieveGroupAvatar(groupId
);
97 if (streamDetails
== null) {
98 return Optional
.empty();
101 return Optional
.of(AttachmentUtils
.createAttachmentStream(streamDetails
, Optional
.empty()));
104 public GroupInfoV2
getOrMigrateGroup(
105 final GroupMasterKey groupMasterKey
, final int revision
, final byte[] signedGroupChange
107 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupMasterKey
);
109 var groupId
= GroupUtils
.getGroupIdV2(groupSecretParams
);
110 var groupInfo
= getGroup(groupId
);
111 final GroupInfoV2 groupInfoV2
;
112 if (groupInfo
instanceof GroupInfoV1
) {
113 // Received a v2 group message for a v1 group, we need to locally migrate the group
114 account
.getGroupStore().deleteGroup(((GroupInfoV1
) groupInfo
).getGroupId());
115 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
, account
.getRecipientResolver());
116 groupInfoV2
.setBlocked(groupInfo
.isBlocked());
117 account
.getGroupStore().updateGroup(groupInfoV2
);
118 logger
.info("Locally migrated group {} to group v2, id: {}",
119 groupInfo
.getGroupId().toBase64(),
120 groupInfoV2
.getGroupId().toBase64());
121 } else if (groupInfo
instanceof GroupInfoV2
) {
122 groupInfoV2
= (GroupInfoV2
) groupInfo
;
124 groupInfoV2
= new GroupInfoV2(groupId
, groupMasterKey
, account
.getRecipientResolver());
127 if (groupInfoV2
.getGroup() == null || groupInfoV2
.getGroup().getRevision() < revision
) {
128 DecryptedGroup group
= null;
129 if (signedGroupChange
!= null
130 && groupInfoV2
.getGroup() != null
131 && groupInfoV2
.getGroup().getRevision() + 1 == revision
) {
132 final var decryptedGroupChange
= context
.getGroupV2Helper()
133 .getDecryptedGroupChange(signedGroupChange
, groupMasterKey
);
135 if (decryptedGroupChange
!= null) {
136 storeProfileKeyFromChange(decryptedGroupChange
);
137 group
= context
.getGroupV2Helper()
138 .getUpdatedDecryptedGroup(groupInfoV2
.getGroup(), decryptedGroupChange
);
143 group
= context
.getGroupV2Helper().getDecryptedGroup(groupSecretParams
);
146 storeProfileKeysFromHistory(groupSecretParams
, groupInfoV2
, group
);
148 } catch (NotAGroupMemberException ignored
) {
152 storeProfileKeysFromMembers(group
);
153 final var avatar
= group
.getAvatar();
154 if (avatar
!= null && !avatar
.isEmpty()) {
155 downloadGroupAvatar(groupId
, groupSecretParams
, avatar
);
158 groupInfoV2
.setGroup(group
);
159 account
.getGroupStore().updateGroup(groupInfoV2
);
165 public Pair
<GroupId
, SendGroupMessageResults
> createGroup(
166 String name
, Set
<RecipientId
> members
, File avatarFile
167 ) throws IOException
, AttachmentInvalidException
{
168 final var selfRecipientId
= account
.getSelfRecipientId();
169 if (members
!= null && members
.contains(selfRecipientId
)) {
170 members
= new HashSet
<>(members
);
171 members
.remove(selfRecipientId
);
174 var gv2Pair
= context
.getGroupV2Helper()
175 .createGroup(name
== null ?
"" : name
, members
== null ? Set
.of() : members
, avatarFile
);
177 if (gv2Pair
== null) {
178 // Failed to create v2 group, creating v1 group instead
179 var gv1
= new GroupInfoV1(GroupIdV1
.createRandom());
180 gv1
.addMembers(List
.of(selfRecipientId
));
181 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
182 return new Pair
<>(gv1
.getGroupId(), result
);
185 final var gv2
= gv2Pair
.first();
186 final var decryptedGroup
= gv2Pair
.second();
188 gv2
.setGroup(decryptedGroup
);
189 if (avatarFile
!= null) {
190 context
.getAvatarStore()
191 .storeGroupAvatar(gv2
.getGroupId(),
192 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
195 account
.getGroupStore().updateGroup(gv2
);
197 final var messageBuilder
= getGroupUpdateMessageBuilder(gv2
, null);
199 final var result
= sendGroupMessage(messageBuilder
,
200 gv2
.getMembersIncludingPendingWithout(selfRecipientId
),
201 gv2
.getDistributionId());
202 return new Pair
<>(gv2
.getGroupId(), result
);
205 public SendGroupMessageResults
updateGroup(
206 final GroupId groupId
,
208 final String description
,
209 final Set
<RecipientId
> members
,
210 final Set
<RecipientId
> removeMembers
,
211 final Set
<RecipientId
> admins
,
212 final Set
<RecipientId
> removeAdmins
,
213 final Set
<RecipientId
> banMembers
,
214 final Set
<RecipientId
> unbanMembers
,
215 final boolean resetGroupLink
,
216 final GroupLinkState groupLinkState
,
217 final GroupPermission addMemberPermission
,
218 final GroupPermission editDetailsPermission
,
219 final File avatarFile
,
220 final Integer expirationTimer
,
221 final Boolean isAnnouncementGroup
222 ) throws IOException
, GroupNotFoundException
, AttachmentInvalidException
, NotAGroupMemberException
, GroupSendingNotAllowedException
{
223 var group
= getGroupForUpdating(groupId
);
225 if (group
instanceof GroupInfoV2
) {
227 return updateGroupV2((GroupInfoV2
) group
,
239 editDetailsPermission
,
242 isAnnouncementGroup
);
243 } catch (ConflictException e
) {
244 // Detected conflicting update, refreshing group and trying again
245 group
= getGroup(groupId
, true);
246 return updateGroupV2((GroupInfoV2
) group
,
258 editDetailsPermission
,
261 isAnnouncementGroup
);
265 final var gv1
= (GroupInfoV1
) group
;
266 final var result
= updateGroupV1(gv1
, name
, members
, avatarFile
);
267 if (expirationTimer
!= null) {
268 setExpirationTimer(gv1
, expirationTimer
);
273 public void updateGroupProfileKey(GroupIdV2 groupId
) throws GroupNotFoundException
, NotAGroupMemberException
, IOException
{
274 var group
= getGroupForUpdating(groupId
);
276 if (group
instanceof GroupInfoV2 groupInfoV2
) {
277 Pair
<DecryptedGroup
, GroupChange
> groupChangePair
;
279 groupChangePair
= context
.getGroupV2Helper().updateSelfProfileKey(groupInfoV2
);
280 } catch (ConflictException e
) {
281 // Detected conflicting update, refreshing group and trying again
282 groupInfoV2
= (GroupInfoV2
) getGroup(groupId
, true);
283 groupChangePair
= context
.getGroupV2Helper().updateSelfProfileKey(groupInfoV2
);
285 if (groupChangePair
!= null) {
286 sendUpdateGroupV2Message(groupInfoV2
, groupChangePair
.first(), groupChangePair
.second());
291 public Pair
<GroupId
, SendGroupMessageResults
> joinGroup(
292 GroupInviteLinkUrl inviteLinkUrl
293 ) throws IOException
, InactiveGroupLinkException
{
294 final DecryptedGroupJoinInfo groupJoinInfo
;
296 groupJoinInfo
= context
.getGroupV2Helper()
297 .getDecryptedGroupJoinInfo(inviteLinkUrl
.getGroupMasterKey(), inviteLinkUrl
.getPassword());
298 } catch (GroupLinkNotActiveException e
) {
299 throw new InactiveGroupLinkException("Group link inactive (reason: " + e
.getReason() + ")", e
);
301 final var groupChange
= context
.getGroupV2Helper()
302 .joinGroup(inviteLinkUrl
.getGroupMasterKey(), inviteLinkUrl
.getPassword(), groupJoinInfo
);
303 final var group
= getOrMigrateGroup(inviteLinkUrl
.getGroupMasterKey(),
304 groupJoinInfo
.getRevision() + 1,
305 groupChange
.toByteArray());
307 if (group
.getGroup() == null) {
308 // Only requested member, can't send update to group members
309 return new Pair
<>(group
.getGroupId(), new SendGroupMessageResults(0, List
.of()));
312 final var result
= sendUpdateGroupV2Message(group
, group
.getGroup(), groupChange
);
314 return new Pair
<>(group
.getGroupId(), result
);
317 public SendGroupMessageResults
quitGroup(
318 final GroupId groupId
, final Set
<RecipientId
> newAdmins
319 ) throws IOException
, LastGroupAdminException
, NotAGroupMemberException
, GroupNotFoundException
{
320 var group
= getGroupForUpdating(groupId
);
321 if (group
instanceof GroupInfoV1
) {
322 return quitGroupV1((GroupInfoV1
) group
);
326 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
327 } catch (ConflictException e
) {
328 // Detected conflicting update, refreshing group and trying again
329 group
= getGroup(groupId
, true);
330 return quitGroupV2((GroupInfoV2
) group
, newAdmins
);
334 public void deleteGroup(GroupId groupId
) throws IOException
{
335 account
.getGroupStore().deleteGroup(groupId
);
336 context
.getAvatarStore().deleteGroupAvatar(groupId
);
339 public void setGroupBlocked(final GroupId groupId
, final boolean blocked
) throws GroupNotFoundException
{
340 var group
= getGroup(groupId
);
342 throw new GroupNotFoundException(groupId
);
345 group
.setBlocked(blocked
);
346 account
.getGroupStore().updateGroup(group
);
349 public SendGroupMessageResults
sendGroupInfoRequest(
350 GroupIdV1 groupId
, RecipientId recipientId
351 ) throws IOException
{
352 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.REQUEST_INFO
).withId(groupId
.serialize());
354 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
.build());
356 // Send group info request message to the recipient who sent us a message with this groupId
357 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
360 public SendGroupMessageResults
sendGroupInfoMessage(
361 GroupIdV1 groupId
, RecipientId recipientId
362 ) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, AttachmentInvalidException
{
364 var group
= getGroupForUpdating(groupId
);
365 if (!(group
instanceof GroupInfoV1
)) {
366 throw new IOException("Received an invalid group request for a v2 group!");
368 g
= (GroupInfoV1
) group
;
370 if (!g
.isMember(recipientId
)) {
371 throw new NotAGroupMemberException(groupId
, g
.name
);
374 var messageBuilder
= getGroupUpdateMessageBuilder(g
);
376 // Send group message only to the recipient who requested it
377 return sendGroupMessage(messageBuilder
, Set
.of(recipientId
), null);
380 private GroupInfo
getGroup(GroupId groupId
, boolean forceUpdate
) {
381 final var group
= account
.getGroupStore().getGroup(groupId
);
382 if (group
instanceof GroupInfoV2 groupInfoV2
) {
383 if (forceUpdate
|| (!groupInfoV2
.isPermissionDenied() && groupInfoV2
.getGroup() == null)) {
384 final var groupSecretParams
= GroupSecretParams
.deriveFromMasterKey(groupInfoV2
.getMasterKey());
385 DecryptedGroup decryptedGroup
;
387 decryptedGroup
= context
.getGroupV2Helper().getDecryptedGroup(groupSecretParams
);
388 } catch (NotAGroupMemberException e
) {
389 groupInfoV2
.setPermissionDenied(true);
390 decryptedGroup
= null;
392 if (decryptedGroup
!= null) {
394 storeProfileKeysFromHistory(groupSecretParams
, groupInfoV2
, decryptedGroup
);
395 } catch (NotAGroupMemberException ignored
) {
397 storeProfileKeysFromMembers(decryptedGroup
);
398 final var avatar
= decryptedGroup
.getAvatar();
399 if (avatar
!= null && !avatar
.isEmpty()) {
400 downloadGroupAvatar(groupInfoV2
.getGroupId(), groupSecretParams
, avatar
);
403 groupInfoV2
.setGroup(decryptedGroup
);
404 account
.getGroupStore().updateGroup(group
);
410 private void downloadGroupAvatar(GroupIdV2 groupId
, GroupSecretParams groupSecretParams
, String cdnKey
) {
412 context
.getAvatarStore()
413 .storeGroupAvatar(groupId
,
414 outputStream
-> retrieveGroupV2Avatar(groupSecretParams
, cdnKey
, outputStream
));
415 } catch (IOException e
) {
416 logger
.warn("Failed to download avatar for group {}, ignoring: {}", groupId
.toBase64(), e
.getMessage());
420 private void retrieveGroupV2Avatar(
421 GroupSecretParams groupSecretParams
, String cdnKey
, OutputStream outputStream
422 ) throws IOException
{
423 var groupOperations
= dependencies
.getGroupsV2Operations().forGroup(groupSecretParams
);
425 var tmpFile
= IOUtils
.createTempFile();
426 try (InputStream input
= dependencies
.getMessageReceiver()
427 .retrieveGroupsV2ProfileAvatar(cdnKey
, tmpFile
, ServiceConfig
.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE
)) {
428 var encryptedData
= IOUtils
.readFully(input
);
430 var decryptedData
= groupOperations
.decryptAvatar(encryptedData
);
431 outputStream
.write(decryptedData
);
434 Files
.delete(tmpFile
.toPath());
435 } catch (IOException e
) {
436 logger
.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
443 private void storeProfileKeysFromMembers(final DecryptedGroup group
) {
444 for (var member
: group
.getMembersList()) {
445 final var serviceId
= ServiceId
.fromByteString(member
.getUuid());
446 final var recipientId
= account
.getRecipientResolver().resolveRecipient(serviceId
);
447 final var profileStore
= account
.getProfileStore();
448 if (profileStore
.getProfileKey(recipientId
) != null) {
449 // We already have a profile key, not updating it from a non-authoritative source
453 profileStore
.storeProfileKey(recipientId
, new ProfileKey(member
.getProfileKey().toByteArray()));
454 } catch (InvalidInputException ignored
) {
459 private void storeProfileKeyFromChange(final DecryptedGroupChange decryptedGroupChange
) {
460 final var profileKeyFromChange
= context
.getGroupV2Helper()
461 .getAuthoritativeProfileKeyFromChange(decryptedGroupChange
);
463 if (profileKeyFromChange
!= null) {
464 final var serviceId
= profileKeyFromChange
.first();
465 final var profileKey
= profileKeyFromChange
.second();
466 final var recipientId
= account
.getRecipientResolver().resolveRecipient(serviceId
);
467 account
.getProfileStore().storeProfileKey(recipientId
, profileKey
);
471 private void storeProfileKeysFromHistory(
472 final GroupSecretParams groupSecretParams
,
473 final GroupInfoV2 localGroup
,
474 final DecryptedGroup newDecryptedGroup
475 ) throws NotAGroupMemberException
{
476 final var revisionWeWereAdded
= context
.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup
);
477 final var localRevision
= localGroup
.getGroup() == null ?
0 : localGroup
.getGroup().getRevision();
478 var fromRevision
= Math
.max(revisionWeWereAdded
, localRevision
);
479 final var newProfileKeys
= new HashMap
<RecipientId
, ProfileKey
>();
481 final var page
= context
.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams
, fromRevision
);
484 .map(DecryptedGroupHistoryEntry
::getChange
)
485 .filter(Optional
::isPresent
)
487 .map(context
.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange
)
488 .filter(Objects
::nonNull
)
490 final var serviceId
= p
.first();
491 final var profileKey
= p
.second();
492 final var recipientId
= account
.getRecipientResolver().resolveRecipient(serviceId
);
493 newProfileKeys
.put(recipientId
, profileKey
);
495 if (!page
.getPagingData().hasMorePages()) {
498 fromRevision
= page
.getPagingData().getNextPageRevision();
501 newProfileKeys
.forEach(account
.getProfileStore()::storeProfileKey
);
504 private GroupInfo
getGroupForUpdating(GroupId groupId
) throws GroupNotFoundException
, NotAGroupMemberException
{
505 var g
= getGroup(groupId
);
507 throw new GroupNotFoundException(groupId
);
509 if (!g
.isMember(account
.getSelfRecipientId()) && !g
.isPendingMember(account
.getSelfRecipientId())) {
510 throw new NotAGroupMemberException(groupId
, g
.getTitle());
515 private SendGroupMessageResults
updateGroupV1(
516 final GroupInfoV1 gv1
, final String name
, final Set
<RecipientId
> members
, final File avatarFile
517 ) throws IOException
, AttachmentInvalidException
{
518 updateGroupV1Details(gv1
, name
, members
, avatarFile
);
520 account
.getGroupStore().updateGroup(gv1
);
522 var messageBuilder
= getGroupUpdateMessageBuilder(gv1
);
523 return sendGroupMessage(messageBuilder
,
524 gv1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
525 gv1
.getDistributionId());
528 private void updateGroupV1Details(
529 final GroupInfoV1 g
, final String name
, final Collection
<RecipientId
> members
, final File avatarFile
530 ) throws IOException
{
535 if (members
!= null) {
536 g
.addMembers(members
);
539 if (avatarFile
!= null) {
540 context
.getAvatarStore()
541 .storeGroupAvatar(g
.getGroupId(),
542 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
547 * Change the expiration timer for a group
549 private void setExpirationTimer(
550 GroupInfoV1 groupInfoV1
, int messageExpirationTimer
551 ) throws NotAGroupMemberException
, GroupNotFoundException
, IOException
, GroupSendingNotAllowedException
{
552 groupInfoV1
.messageExpirationTime
= messageExpirationTimer
;
553 account
.getGroupStore().updateGroup(groupInfoV1
);
554 sendExpirationTimerUpdate(groupInfoV1
.getGroupId());
557 private void sendExpirationTimerUpdate(GroupIdV1 groupId
) throws IOException
, NotAGroupMemberException
, GroupNotFoundException
, GroupSendingNotAllowedException
{
558 final var messageBuilder
= SignalServiceDataMessage
.newBuilder().asExpirationUpdate();
559 context
.getSendHelper().sendAsGroupMessage(messageBuilder
, groupId
);
562 private SendGroupMessageResults
updateGroupV2(
563 final GroupInfoV2 group
,
565 final String description
,
566 final Set
<RecipientId
> members
,
567 final Set
<RecipientId
> removeMembers
,
568 final Set
<RecipientId
> admins
,
569 final Set
<RecipientId
> removeAdmins
,
570 final Set
<RecipientId
> banMembers
,
571 final Set
<RecipientId
> unbanMembers
,
572 final boolean resetGroupLink
,
573 final GroupLinkState groupLinkState
,
574 final GroupPermission addMemberPermission
,
575 final GroupPermission editDetailsPermission
,
576 final File avatarFile
,
577 final Integer expirationTimer
,
578 final Boolean isAnnouncementGroup
579 ) throws IOException
{
580 SendGroupMessageResults result
= null;
581 final var groupV2Helper
= context
.getGroupV2Helper();
582 if (group
.isPendingMember(account
.getSelfRecipientId())) {
583 var groupGroupChangePair
= groupV2Helper
.acceptInvite(group
);
584 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
587 if (members
!= null) {
588 final var newMembers
= new HashSet
<>(members
);
589 newMembers
.removeAll(group
.getMembers());
590 if (newMembers
.size() > 0) {
591 var groupGroupChangePair
= groupV2Helper
.addMembers(group
, newMembers
);
592 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
596 if (removeMembers
!= null) {
597 var existingRemoveMembers
= new HashSet
<>(removeMembers
);
598 if (banMembers
!= null) {
599 existingRemoveMembers
.addAll(banMembers
);
601 existingRemoveMembers
.retainAll(group
.getMembers());
602 if (members
!= null) {
603 existingRemoveMembers
.removeAll(members
);
605 existingRemoveMembers
.remove(account
.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
606 if (existingRemoveMembers
.size() > 0) {
607 var groupGroupChangePair
= groupV2Helper
.removeMembers(group
, existingRemoveMembers
);
608 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
611 var pendingRemoveMembers
= new HashSet
<>(removeMembers
);
612 pendingRemoveMembers
.retainAll(group
.getPendingMembers());
613 if (pendingRemoveMembers
.size() > 0) {
614 var groupGroupChangePair
= groupV2Helper
.revokeInvitedMembers(group
, pendingRemoveMembers
);
615 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
619 if (admins
!= null) {
620 final var newAdmins
= new HashSet
<>(admins
);
621 newAdmins
.retainAll(group
.getMembers());
622 newAdmins
.removeAll(group
.getAdminMembers());
623 if (newAdmins
.size() > 0) {
624 for (var admin
: newAdmins
) {
625 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, true);
626 result
= sendUpdateGroupV2Message(group
,
627 groupGroupChangePair
.first(),
628 groupGroupChangePair
.second());
633 if (removeAdmins
!= null) {
634 final var existingRemoveAdmins
= new HashSet
<>(removeAdmins
);
635 existingRemoveAdmins
.retainAll(group
.getAdminMembers());
636 if (existingRemoveAdmins
.size() > 0) {
637 for (var admin
: existingRemoveAdmins
) {
638 var groupGroupChangePair
= groupV2Helper
.setMemberAdmin(group
, admin
, false);
639 result
= sendUpdateGroupV2Message(group
,
640 groupGroupChangePair
.first(),
641 groupGroupChangePair
.second());
646 if (banMembers
!= null) {
647 final var newlyBannedMembers
= new HashSet
<>(banMembers
);
648 newlyBannedMembers
.removeAll(group
.getBannedMembers());
649 if (newlyBannedMembers
.size() > 0) {
650 var groupGroupChangePair
= groupV2Helper
.banMembers(group
, newlyBannedMembers
);
651 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
655 if (unbanMembers
!= null) {
656 var existingUnbanMembers
= new HashSet
<>(unbanMembers
);
657 existingUnbanMembers
.retainAll(group
.getBannedMembers());
658 if (existingUnbanMembers
.size() > 0) {
659 var groupGroupChangePair
= groupV2Helper
.unbanMembers(group
, existingUnbanMembers
);
660 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
664 if (resetGroupLink
) {
665 var groupGroupChangePair
= groupV2Helper
.resetGroupLinkPassword(group
);
666 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
669 if (groupLinkState
!= null) {
670 var groupGroupChangePair
= groupV2Helper
.setGroupLinkState(group
, groupLinkState
);
671 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
674 if (addMemberPermission
!= null) {
675 var groupGroupChangePair
= groupV2Helper
.setAddMemberPermission(group
, addMemberPermission
);
676 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
679 if (editDetailsPermission
!= null) {
680 var groupGroupChangePair
= groupV2Helper
.setEditDetailsPermission(group
, editDetailsPermission
);
681 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
684 if (expirationTimer
!= null) {
685 var groupGroupChangePair
= groupV2Helper
.setMessageExpirationTimer(group
, expirationTimer
);
686 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
689 if (isAnnouncementGroup
!= null) {
690 var groupGroupChangePair
= groupV2Helper
.setIsAnnouncementGroup(group
, isAnnouncementGroup
);
691 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
694 if (name
!= null || description
!= null || avatarFile
!= null) {
695 var groupGroupChangePair
= groupV2Helper
.updateGroup(group
, name
, description
, avatarFile
);
696 if (avatarFile
!= null) {
697 context
.getAvatarStore()
698 .storeGroupAvatar(group
.getGroupId(),
699 outputStream
-> IOUtils
.copyFileToStream(avatarFile
, outputStream
));
701 result
= sendUpdateGroupV2Message(group
, groupGroupChangePair
.first(), groupGroupChangePair
.second());
707 private SendGroupMessageResults
quitGroupV1(final GroupInfoV1 groupInfoV1
) throws IOException
{
708 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.QUIT
)
709 .withId(groupInfoV1
.getGroupId().serialize())
712 var messageBuilder
= SignalServiceDataMessage
.newBuilder().asGroupMessage(group
);
713 groupInfoV1
.removeMember(account
.getSelfRecipientId());
714 account
.getGroupStore().updateGroup(groupInfoV1
);
715 return sendGroupMessage(messageBuilder
,
716 groupInfoV1
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
717 groupInfoV1
.getDistributionId());
720 private SendGroupMessageResults
quitGroupV2(
721 final GroupInfoV2 groupInfoV2
, final Set
<RecipientId
> newAdmins
722 ) throws LastGroupAdminException
, IOException
{
723 final var currentAdmins
= groupInfoV2
.getAdminMembers();
724 newAdmins
.removeAll(currentAdmins
);
725 newAdmins
.retainAll(groupInfoV2
.getMembers());
726 if (currentAdmins
.contains(account
.getSelfRecipientId())
727 && currentAdmins
.size() == 1
728 && groupInfoV2
.getMembers().size() > 1
729 && newAdmins
.size() == 0) {
730 // Last admin can't leave the group, unless she's also the last member
731 throw new LastGroupAdminException(groupInfoV2
.getGroupId(), groupInfoV2
.getTitle());
733 final var groupGroupChangePair
= context
.getGroupV2Helper().leaveGroup(groupInfoV2
, newAdmins
);
734 groupInfoV2
.setGroup(groupGroupChangePair
.first());
735 account
.getGroupStore().updateGroup(groupInfoV2
);
737 var messageBuilder
= getGroupUpdateMessageBuilder(groupInfoV2
, groupGroupChangePair
.second().toByteArray());
738 return sendGroupMessage(messageBuilder
,
739 groupInfoV2
.getMembersIncludingPendingWithout(account
.getSelfRecipientId()),
740 groupInfoV2
.getDistributionId());
743 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV1 g
) throws AttachmentInvalidException
{
744 var group
= SignalServiceGroup
.newBuilder(SignalServiceGroup
.Type
.UPDATE
)
745 .withId(g
.getGroupId().serialize())
747 .withMembers(g
.getMembers()
749 .map(context
.getRecipientHelper()::resolveSignalServiceAddress
)
753 final var attachment
= createGroupAvatarAttachment(g
.getGroupId());
754 attachment
.ifPresent(group
::withAvatar
);
755 } catch (IOException e
) {
756 throw new AttachmentInvalidException(g
.getGroupId().toBase64(), e
);
759 return SignalServiceDataMessage
.newBuilder()
760 .asGroupMessage(group
.build())
761 .withExpiration(g
.getMessageExpirationTimer());
764 private SignalServiceDataMessage
.Builder
getGroupUpdateMessageBuilder(GroupInfoV2 g
, byte[] signedGroupChange
) {
765 var group
= SignalServiceGroupV2
.newBuilder(g
.getMasterKey())
766 .withRevision(g
.getGroup().getRevision())
767 .withSignedGroupChange(signedGroupChange
);
768 return SignalServiceDataMessage
.newBuilder()
769 .asGroupMessage(group
.build())
770 .withExpiration(g
.getMessageExpirationTimer());
773 private SendGroupMessageResults
sendUpdateGroupV2Message(
774 GroupInfoV2 group
, DecryptedGroup newDecryptedGroup
, GroupChange groupChange
775 ) throws IOException
{
776 final var selfRecipientId
= account
.getSelfRecipientId();
777 final var members
= group
.getMembersIncludingPendingWithout(selfRecipientId
);
778 group
.setGroup(newDecryptedGroup
);
779 members
.addAll(group
.getMembersIncludingPendingWithout(selfRecipientId
));
780 account
.getGroupStore().updateGroup(group
);
782 final var messageBuilder
= getGroupUpdateMessageBuilder(group
, groupChange
.toByteArray());
783 return sendGroupMessage(messageBuilder
, members
, group
.getDistributionId());
786 private SendGroupMessageResults
sendGroupMessage(
787 final SignalServiceDataMessage
.Builder messageBuilder
,
788 final Set
<RecipientId
> members
,
789 final DistributionId distributionId
790 ) throws IOException
{
791 final var timestamp
= System
.currentTimeMillis();
792 messageBuilder
.withTimestamp(timestamp
);
793 final var results
= context
.getSendHelper().sendGroupMessage(messageBuilder
.build(), members
, distributionId
);
794 return new SendGroupMessageResults(timestamp
,
796 .map(sendMessageResult
-> SendMessageResult
.from(sendMessageResult
,
797 account
.getRecipientResolver(),
798 account
.getRecipientAddressResolver()))