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