]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Move all message decryption to IncomingMessageHandler
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / GroupHelper.java
1 package org.asamk.signal.manager.helper;
2
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.GroupSendingNotAllowedException;
16 import org.asamk.signal.manager.groups.GroupUtils;
17 import org.asamk.signal.manager.groups.LastGroupAdminException;
18 import org.asamk.signal.manager.groups.NotAGroupMemberException;
19 import org.asamk.signal.manager.storage.SignalAccount;
20 import org.asamk.signal.manager.storage.groups.GroupInfo;
21 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
22 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
23 import org.asamk.signal.manager.storage.recipients.RecipientId;
24 import org.asamk.signal.manager.storage.recipients.RecipientResolver;
25 import org.asamk.signal.manager.util.AttachmentUtils;
26 import org.asamk.signal.manager.util.IOUtils;
27 import org.signal.storageservice.protos.groups.GroupChange;
28 import org.signal.storageservice.protos.groups.local.DecryptedGroup;
29 import org.signal.zkgroup.InvalidInputException;
30 import org.signal.zkgroup.groups.GroupMasterKey;
31 import org.signal.zkgroup.groups.GroupSecretParams;
32 import org.signal.zkgroup.profiles.ProfileKey;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.whispersystems.libsignal.util.Pair;
36 import org.whispersystems.libsignal.util.guava.Optional;
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.exceptions.ConflictException;
44 import org.whispersystems.signalservice.api.util.UuidUtil;
45
46 import java.io.File;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.OutputStream;
50 import java.nio.file.Files;
51 import java.util.Collection;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Set;
55 import java.util.stream.Collectors;
56
57 public class GroupHelper {
58
59 private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
60
61 private final SignalAccount account;
62 private final SignalDependencies dependencies;
63 private final AttachmentHelper attachmentHelper;
64 private final SendHelper sendHelper;
65 private final GroupV2Helper groupV2Helper;
66 private final AvatarStore avatarStore;
67 private final SignalServiceAddressResolver addressResolver;
68 private final RecipientResolver recipientResolver;
69
70 public GroupHelper(
71 final SignalAccount account,
72 final SignalDependencies dependencies,
73 final AttachmentHelper attachmentHelper,
74 final SendHelper sendHelper,
75 final GroupV2Helper groupV2Helper,
76 final AvatarStore avatarStore,
77 final SignalServiceAddressResolver addressResolver,
78 final RecipientResolver recipientResolver
79 ) {
80 this.account = account;
81 this.dependencies = dependencies;
82 this.attachmentHelper = attachmentHelper;
83 this.sendHelper = sendHelper;
84 this.groupV2Helper = groupV2Helper;
85 this.avatarStore = avatarStore;
86 this.addressResolver = addressResolver;
87 this.recipientResolver = recipientResolver;
88 }
89
90 public GroupInfo getGroup(GroupId groupId) {
91 return getGroup(groupId, false);
92 }
93
94 public boolean isGroupBlocked(final GroupId groupId) {
95 var group = getGroup(groupId);
96 return group != null && group.isBlocked();
97 }
98
99 public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) {
100 try {
101 avatarStore.storeGroupAvatar(groupId,
102 outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream));
103 } catch (IOException e) {
104 logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
105 }
106 }
107
108 public Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException {
109 final var streamDetails = avatarStore.retrieveGroupAvatar(groupId);
110 if (streamDetails == null) {
111 return Optional.absent();
112 }
113
114 return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
115 }
116
117 public GroupInfoV2 getOrMigrateGroup(
118 final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
119 ) {
120 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
121
122 var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
123 var groupInfo = getGroup(groupId);
124 final GroupInfoV2 groupInfoV2;
125 if (groupInfo instanceof GroupInfoV1) {
126 // Received a v2 group message for a v1 group, we need to locally migrate the group
127 account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
128 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
129 logger.info("Locally migrated group {} to group v2, id: {}",
130 groupInfo.getGroupId().toBase64(),
131 groupInfoV2.getGroupId().toBase64());
132 } else if (groupInfo instanceof GroupInfoV2) {
133 groupInfoV2 = (GroupInfoV2) groupInfo;
134 } else {
135 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
136 }
137
138 if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
139 DecryptedGroup group = null;
140 if (signedGroupChange != null
141 && groupInfoV2.getGroup() != null
142 && groupInfoV2.getGroup().getRevision() + 1 == revision) {
143 group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
144 signedGroupChange,
145 groupMasterKey);
146 }
147 if (group == null) {
148 group = groupV2Helper.getDecryptedGroup(groupSecretParams);
149 }
150 if (group != null) {
151 storeProfileKeysFromMembers(group);
152 final var avatar = group.getAvatar();
153 if (avatar != null && !avatar.isEmpty()) {
154 downloadGroupAvatar(groupId, groupSecretParams, avatar);
155 }
156 }
157 groupInfoV2.setGroup(group, recipientResolver);
158 account.getGroupStore().updateGroup(groupInfoV2);
159 }
160
161 return groupInfoV2;
162 }
163
164 public Pair<GroupId, SendGroupMessageResults> createGroup(
165 String name, Set<RecipientId> members, File avatarFile
166 ) throws IOException, AttachmentInvalidException {
167 final var selfRecipientId = account.getSelfRecipientId();
168 if (members != null && members.contains(selfRecipientId)) {
169 members = new HashSet<>(members);
170 members.remove(selfRecipientId);
171 }
172
173 var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name,
174 members == null ? Set.of() : members,
175 avatarFile);
176
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);
183 }
184
185 final var gv2 = gv2Pair.first();
186 final var decryptedGroup = gv2Pair.second();
187
188 gv2.setGroup(decryptedGroup, recipientResolver);
189 if (avatarFile != null) {
190 avatarStore.storeGroupAvatar(gv2.getGroupId(),
191 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
192 }
193
194 account.getGroupStore().updateGroup(gv2);
195
196 final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
197
198 final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
199 return new Pair<>(gv2.getGroupId(), result);
200 }
201
202 public SendGroupMessageResults updateGroup(
203 final GroupId groupId,
204 final String name,
205 final String description,
206 final Set<RecipientId> members,
207 final Set<RecipientId> removeMembers,
208 final Set<RecipientId> admins,
209 final Set<RecipientId> removeAdmins,
210 final boolean resetGroupLink,
211 final GroupLinkState groupLinkState,
212 final GroupPermission addMemberPermission,
213 final GroupPermission editDetailsPermission,
214 final File avatarFile,
215 final Integer expirationTimer,
216 final Boolean isAnnouncementGroup
217 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
218 var group = getGroupForUpdating(groupId);
219
220 if (group instanceof GroupInfoV2) {
221 try {
222 return updateGroupV2((GroupInfoV2) group,
223 name,
224 description,
225 members,
226 removeMembers,
227 admins,
228 removeAdmins,
229 resetGroupLink,
230 groupLinkState,
231 addMemberPermission,
232 editDetailsPermission,
233 avatarFile,
234 expirationTimer,
235 isAnnouncementGroup);
236 } catch (ConflictException e) {
237 // Detected conflicting update, refreshing group and trying again
238 group = getGroup(groupId, true);
239 return updateGroupV2((GroupInfoV2) group,
240 name,
241 description,
242 members,
243 removeMembers,
244 admins,
245 removeAdmins,
246 resetGroupLink,
247 groupLinkState,
248 addMemberPermission,
249 editDetailsPermission,
250 avatarFile,
251 expirationTimer,
252 isAnnouncementGroup);
253 }
254 }
255
256 final var gv1 = (GroupInfoV1) group;
257 final var result = updateGroupV1(gv1, name, members, avatarFile);
258 if (expirationTimer != null) {
259 setExpirationTimer(gv1, expirationTimer);
260 }
261 return result;
262 }
263
264 public Pair<GroupId, SendGroupMessageResults> joinGroup(
265 GroupInviteLinkUrl inviteLinkUrl
266 ) throws IOException, GroupLinkNotActiveException {
267 final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
268 inviteLinkUrl.getPassword());
269 final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
270 inviteLinkUrl.getPassword(),
271 groupJoinInfo);
272 final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
273 groupJoinInfo.getRevision() + 1,
274 groupChange.toByteArray());
275
276 if (group.getGroup() == null) {
277 // Only requested member, can't send update to group members
278 return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of()));
279 }
280
281 final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
282
283 return new Pair<>(group.getGroupId(), result);
284 }
285
286 public SendGroupMessageResults quitGroup(
287 final GroupId groupId, final Set<RecipientId> newAdmins
288 ) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
289 var group = getGroupForUpdating(groupId);
290 if (group instanceof GroupInfoV1) {
291 return quitGroupV1((GroupInfoV1) group);
292 }
293
294 try {
295 return quitGroupV2((GroupInfoV2) group, newAdmins);
296 } catch (ConflictException e) {
297 // Detected conflicting update, refreshing group and trying again
298 group = getGroup(groupId, true);
299 return quitGroupV2((GroupInfoV2) group, newAdmins);
300 }
301 }
302
303 public void deleteGroup(GroupId groupId) throws IOException {
304 account.getGroupStore().deleteGroup(groupId);
305 avatarStore.deleteGroupAvatar(groupId);
306 }
307
308 public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
309 var group = getGroup(groupId);
310 if (group == null) {
311 throw new GroupNotFoundException(groupId);
312 }
313
314 group.setBlocked(blocked);
315 account.getGroupStore().updateGroup(group);
316 }
317
318 public SendGroupMessageResults sendGroupInfoRequest(
319 GroupIdV1 groupId, RecipientId recipientId
320 ) throws IOException {
321 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
322
323 var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
324
325 // Send group info request message to the recipient who sent us a message with this groupId
326 return sendGroupMessage(messageBuilder, Set.of(recipientId));
327 }
328
329 public SendGroupMessageResults sendGroupInfoMessage(
330 GroupIdV1 groupId, RecipientId recipientId
331 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
332 GroupInfoV1 g;
333 var group = getGroupForUpdating(groupId);
334 if (!(group instanceof GroupInfoV1)) {
335 throw new IOException("Received an invalid group request for a v2 group!");
336 }
337 g = (GroupInfoV1) group;
338
339 if (!g.isMember(recipientId)) {
340 throw new NotAGroupMemberException(groupId, g.name);
341 }
342
343 var messageBuilder = getGroupUpdateMessageBuilder(g);
344
345 // Send group message only to the recipient who requested it
346 return sendGroupMessage(messageBuilder, Set.of(recipientId));
347 }
348
349 private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
350 final var group = account.getGroupStore().getGroup(groupId);
351 if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
352 final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
353 ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver);
354 account.getGroupStore().updateGroup(group);
355 }
356 return group;
357 }
358
359 private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) {
360 try {
361 avatarStore.storeGroupAvatar(groupId,
362 outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream));
363 } catch (IOException e) {
364 logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
365 }
366 }
367
368 private void retrieveGroupV2Avatar(
369 GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
370 ) throws IOException {
371 var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
372
373 var tmpFile = IOUtils.createTempFile();
374 try (InputStream input = dependencies.getMessageReceiver()
375 .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
376 var encryptedData = IOUtils.readFully(input);
377
378 var decryptedData = groupOperations.decryptAvatar(encryptedData);
379 outputStream.write(decryptedData);
380 } finally {
381 try {
382 Files.delete(tmpFile.toPath());
383 } catch (IOException e) {
384 logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
385 tmpFile,
386 e.getMessage());
387 }
388 }
389 }
390
391 private void storeProfileKeysFromMembers(final DecryptedGroup group) {
392 for (var member : group.getMembersList()) {
393 final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray());
394 final var recipientId = account.getRecipientStore().resolveRecipient(uuid);
395 try {
396 account.getProfileStore()
397 .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
398 } catch (InvalidInputException ignored) {
399 }
400 }
401 }
402
403 private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
404 var g = getGroup(groupId);
405 if (g == null) {
406 throw new GroupNotFoundException(groupId);
407 }
408 if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
409 throw new NotAGroupMemberException(groupId, g.getTitle());
410 }
411 return g;
412 }
413
414 private SendGroupMessageResults updateGroupV1(
415 final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
416 ) throws IOException, AttachmentInvalidException {
417 updateGroupV1Details(gv1, name, members, avatarFile);
418
419 account.getGroupStore().updateGroup(gv1);
420
421 var messageBuilder = getGroupUpdateMessageBuilder(gv1);
422 return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
423 }
424
425 private void updateGroupV1Details(
426 final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
427 ) throws IOException {
428 if (name != null) {
429 g.name = name;
430 }
431
432 if (members != null) {
433 g.addMembers(members);
434 }
435
436 if (avatarFile != null) {
437 avatarStore.storeGroupAvatar(g.getGroupId(),
438 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
439 }
440 }
441
442 /**
443 * Change the expiration timer for a group
444 */
445 private void setExpirationTimer(
446 GroupInfoV1 groupInfoV1, int messageExpirationTimer
447 ) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException {
448 groupInfoV1.messageExpirationTime = messageExpirationTimer;
449 account.getGroupStore().updateGroup(groupInfoV1);
450 sendExpirationTimerUpdate(groupInfoV1.getGroupId());
451 }
452
453 private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
454 final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
455 sendHelper.sendAsGroupMessage(messageBuilder, groupId);
456 }
457
458 private SendGroupMessageResults updateGroupV2(
459 final GroupInfoV2 group,
460 final String name,
461 final String description,
462 final Set<RecipientId> members,
463 final Set<RecipientId> removeMembers,
464 final Set<RecipientId> admins,
465 final Set<RecipientId> removeAdmins,
466 final boolean resetGroupLink,
467 final GroupLinkState groupLinkState,
468 final GroupPermission addMemberPermission,
469 final GroupPermission editDetailsPermission,
470 final File avatarFile,
471 final Integer expirationTimer,
472 final Boolean isAnnouncementGroup
473 ) throws IOException {
474 SendGroupMessageResults result = null;
475 if (group.isPendingMember(account.getSelfRecipientId())) {
476 var groupGroupChangePair = groupV2Helper.acceptInvite(group);
477 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
478 }
479
480 if (members != null) {
481 final var newMembers = new HashSet<>(members);
482 newMembers.removeAll(group.getMembers());
483 if (newMembers.size() > 0) {
484 var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
485 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
486 }
487 }
488
489 if (removeMembers != null) {
490 var existingRemoveMembers = new HashSet<>(removeMembers);
491 existingRemoveMembers.retainAll(group.getMembers());
492 existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
493 if (existingRemoveMembers.size() > 0) {
494 var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
495 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
496 }
497
498 var pendingRemoveMembers = new HashSet<>(removeMembers);
499 pendingRemoveMembers.retainAll(group.getPendingMembers());
500 if (pendingRemoveMembers.size() > 0) {
501 var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
502 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
503 }
504 }
505
506 if (admins != null) {
507 final var newAdmins = new HashSet<>(admins);
508 newAdmins.retainAll(group.getMembers());
509 newAdmins.removeAll(group.getAdminMembers());
510 if (newAdmins.size() > 0) {
511 for (var admin : newAdmins) {
512 var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
513 result = sendUpdateGroupV2Message(group,
514 groupGroupChangePair.first(),
515 groupGroupChangePair.second());
516 }
517 }
518 }
519
520 if (removeAdmins != null) {
521 final var existingRemoveAdmins = new HashSet<>(removeAdmins);
522 existingRemoveAdmins.retainAll(group.getAdminMembers());
523 if (existingRemoveAdmins.size() > 0) {
524 for (var admin : existingRemoveAdmins) {
525 var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
526 result = sendUpdateGroupV2Message(group,
527 groupGroupChangePair.first(),
528 groupGroupChangePair.second());
529 }
530 }
531 }
532
533 if (resetGroupLink) {
534 var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
535 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
536 }
537
538 if (groupLinkState != null) {
539 var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
540 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
541 }
542
543 if (addMemberPermission != null) {
544 var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
545 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
546 }
547
548 if (editDetailsPermission != null) {
549 var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
550 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
551 }
552
553 if (expirationTimer != null) {
554 var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
555 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
556 }
557
558 if (isAnnouncementGroup != null) {
559 var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup);
560 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
561 }
562
563 if (name != null || description != null || avatarFile != null) {
564 var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
565 if (avatarFile != null) {
566 avatarStore.storeGroupAvatar(group.getGroupId(),
567 outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
568 }
569 result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
570 }
571
572 return result;
573 }
574
575 private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException {
576 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
577 .withId(groupInfoV1.getGroupId().serialize())
578 .build();
579
580 var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
581 groupInfoV1.removeMember(account.getSelfRecipientId());
582 account.getGroupStore().updateGroup(groupInfoV1);
583 return sendGroupMessage(messageBuilder,
584 groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
585 }
586
587 private SendGroupMessageResults quitGroupV2(
588 final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
589 ) throws LastGroupAdminException, IOException {
590 final var currentAdmins = groupInfoV2.getAdminMembers();
591 newAdmins.removeAll(currentAdmins);
592 newAdmins.retainAll(groupInfoV2.getMembers());
593 if (currentAdmins.contains(account.getSelfRecipientId())
594 && currentAdmins.size() == 1
595 && groupInfoV2.getMembers().size() > 1
596 && newAdmins.size() == 0) {
597 // Last admin can't leave the group, unless she's also the last member
598 throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle());
599 }
600 final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
601 groupInfoV2.setGroup(groupGroupChangePair.first(), recipientResolver);
602 account.getGroupStore().updateGroup(groupInfoV2);
603
604 var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
605 return sendGroupMessage(messageBuilder,
606 groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
607 }
608
609 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
610 var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
611 .withId(g.getGroupId().serialize())
612 .withName(g.name)
613 .withMembers(g.getMembers()
614 .stream()
615 .map(addressResolver::resolveSignalServiceAddress)
616 .collect(Collectors.toList()));
617
618 try {
619 final var attachment = createGroupAvatarAttachment(g.getGroupId());
620 if (attachment.isPresent()) {
621 group.withAvatar(attachment.get());
622 }
623 } catch (IOException e) {
624 throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
625 }
626
627 return SignalServiceDataMessage.newBuilder()
628 .asGroupMessage(group.build())
629 .withExpiration(g.getMessageExpirationTime());
630 }
631
632 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
633 var group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
634 .withRevision(g.getGroup().getRevision())
635 .withSignedGroupChange(signedGroupChange);
636 return SignalServiceDataMessage.newBuilder()
637 .asGroupMessage(group.build())
638 .withExpiration(g.getMessageExpirationTime());
639 }
640
641 private SendGroupMessageResults sendUpdateGroupV2Message(
642 GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
643 ) throws IOException {
644 final var selfRecipientId = account.getSelfRecipientId();
645 final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
646 group.setGroup(newDecryptedGroup, recipientResolver);
647 members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
648 account.getGroupStore().updateGroup(group);
649
650 final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
651 return sendGroupMessage(messageBuilder, members);
652 }
653
654 private SendGroupMessageResults sendGroupMessage(
655 final SignalServiceDataMessage.Builder messageBuilder, final Set<RecipientId> members
656 ) throws IOException {
657 final var timestamp = System.currentTimeMillis();
658 messageBuilder.withTimestamp(timestamp);
659 final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members);
660 return new SendGroupMessageResults(timestamp, results);
661 }
662 }