mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +00:00
Extract GroupHelper
This commit is contained in:
parent
6ee0a95aa2
commit
944c3327ee
3 changed files with 692 additions and 621 deletions
|
@ -34,6 +34,7 @@ import org.asamk.signal.manager.groups.GroupPermission;
|
|||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.groups.LastGroupAdminException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.manager.helper.GroupHelper;
|
||||
import org.asamk.signal.manager.helper.GroupV2Helper;
|
||||
import org.asamk.signal.manager.helper.PinHelper;
|
||||
import org.asamk.signal.manager.helper.ProfileHelper;
|
||||
|
@ -45,7 +46,6 @@ import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
|
|||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.identities.IdentityInfo;
|
||||
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
|
||||
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
||||
|
@ -60,23 +60,9 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||
import org.asamk.signal.manager.util.ProfileUtils;
|
||||
import org.asamk.signal.manager.util.StickerUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
|
||||
import org.signal.libsignal.metadata.ProtocolDuplicateMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidVersionException;
|
||||
import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
|
||||
import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
||||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.VerificationFailedException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -93,11 +79,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord;
|
|||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
|
@ -107,7 +91,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||
|
@ -125,18 +108,15 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage
|
|||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
@ -187,9 +167,9 @@ public class Manager implements Closeable {
|
|||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
private final ProfileHelper profileHelper;
|
||||
private final GroupV2Helper groupV2Helper;
|
||||
private final PinHelper pinHelper;
|
||||
private final SendHelper sendHelper;
|
||||
private final GroupHelper groupHelper;
|
||||
|
||||
private final AvatarStore avatarStore;
|
||||
private final AttachmentStore attachmentStore;
|
||||
|
@ -235,12 +215,11 @@ public class Manager implements Closeable {
|
|||
dependencies::getProfileService,
|
||||
dependencies::getMessageReceiver,
|
||||
this::resolveSignalServiceAddress);
|
||||
this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
|
||||
final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
|
||||
this::getRecipientProfile,
|
||||
account::getSelfRecipientId,
|
||||
dependencies.getGroupsV2Operations(),
|
||||
dependencies.getGroupsV2Api(),
|
||||
this::getGroupAuthForToday,
|
||||
this::resolveSignalServiceAddress);
|
||||
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
|
||||
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
|
||||
|
@ -253,6 +232,13 @@ public class Manager implements Closeable {
|
|||
this::handleIdentityFailure,
|
||||
this::getGroup,
|
||||
this::refreshRegisteredUser);
|
||||
this.groupHelper = new GroupHelper(account,
|
||||
dependencies,
|
||||
sendHelper,
|
||||
groupV2Helper,
|
||||
avatarStore,
|
||||
this::resolveSignalServiceAddress,
|
||||
this::resolveRecipient);
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
|
@ -665,15 +651,6 @@ public class Manager implements Closeable {
|
|||
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
|
||||
}
|
||||
|
||||
private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupId groupId) throws IOException {
|
||||
final var streamDetails = avatarStore.retrieveGroupAvatar(groupId);
|
||||
if (streamDetails == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
|
||||
}
|
||||
|
||||
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
|
||||
final var streamDetails = avatarStore.retrieveContactAvatar(address);
|
||||
if (streamDetails == null) {
|
||||
|
@ -683,17 +660,6 @@ public class Manager implements Closeable {
|
|||
return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
|
||||
}
|
||||
|
||||
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
||||
var g = getGroup(groupId);
|
||||
if (g == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
|
||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
public List<GroupInfo> getGroups() {
|
||||
return account.getGroupStore().getGroups();
|
||||
}
|
||||
|
@ -701,53 +667,8 @@ public class Manager implements Closeable {
|
|||
public SendGroupMessageResults sendQuitGroupMessage(
|
||||
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
|
||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
return quitGroupV1((GroupInfoV1) group);
|
||||
}
|
||||
|
||||
final var newAdmins = getRecipientIds(groupAdmins);
|
||||
try {
|
||||
return quitGroupV2((GroupInfoV2) group, newAdmins);
|
||||
} catch (ConflictException e) {
|
||||
// Detected conflicting update, refreshing group and trying again
|
||||
group = getGroup(groupId, true);
|
||||
return quitGroupV2((GroupInfoV2) group, newAdmins);
|
||||
}
|
||||
}
|
||||
|
||||
private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
|
||||
.withId(groupInfoV1.getGroupId().serialize())
|
||||
.build();
|
||||
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
|
||||
groupInfoV1.removeMember(account.getSelfRecipientId());
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
return sendGroupMessage(messageBuilder,
|
||||
groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
private SendGroupMessageResults quitGroupV2(
|
||||
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
|
||||
) throws LastGroupAdminException, IOException {
|
||||
final var currentAdmins = groupInfoV2.getAdminMembers();
|
||||
newAdmins.removeAll(currentAdmins);
|
||||
newAdmins.retainAll(groupInfoV2.getMembers());
|
||||
if (currentAdmins.contains(getSelfRecipientId())
|
||||
&& currentAdmins.size() == 1
|
||||
&& groupInfoV2.getMembers().size() > 1
|
||||
&& newAdmins.size() == 0) {
|
||||
// Last admin can't leave the group, unless she's also the last member
|
||||
throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle());
|
||||
}
|
||||
final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
|
||||
groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
|
||||
return sendGroupMessage(messageBuilder,
|
||||
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
return groupHelper.quitGroup(groupId, newAdmins);
|
||||
}
|
||||
|
||||
public void deleteGroup(GroupId groupId) throws IOException {
|
||||
|
@ -758,45 +679,7 @@ public class Manager implements Closeable {
|
|||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name, Set<RecipientIdentifier.Single> members, File avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile);
|
||||
}
|
||||
|
||||
private Pair<GroupId, SendGroupMessageResults> createGroupInternal(
|
||||
String name, Set<RecipientId> members, File avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
if (members != null && members.contains(selfRecipientId)) {
|
||||
members = new HashSet<>(members);
|
||||
members.remove(selfRecipientId);
|
||||
}
|
||||
|
||||
var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name,
|
||||
members == null ? Set.of() : members,
|
||||
avatarFile);
|
||||
|
||||
if (gv2Pair == null) {
|
||||
// Failed to create v2 group, creating v1 group instead
|
||||
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
|
||||
gv1.addMembers(List.of(selfRecipientId));
|
||||
final var result = updateGroupV1(gv1, name, members, avatarFile);
|
||||
return new Pair<>(gv1.getGroupId(), result);
|
||||
}
|
||||
|
||||
final var gv2 = gv2Pair.first();
|
||||
final var decryptedGroup = gv2Pair.second();
|
||||
|
||||
gv2.setGroup(decryptedGroup, this::resolveRecipient);
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(gv2.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
|
||||
account.getGroupStore().updateGroup(gv2);
|
||||
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
|
||||
|
||||
final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
|
||||
return new Pair<>(gv2.getGroupId(), result);
|
||||
return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile);
|
||||
}
|
||||
|
||||
public SendGroupMessageResults updateGroup(
|
||||
|
@ -815,7 +698,7 @@ public class Manager implements Closeable {
|
|||
Integer expirationTimer,
|
||||
Boolean isAnnouncementGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
|
||||
return updateGroupInternal(groupId,
|
||||
return groupHelper.updateGroup(groupId,
|
||||
name,
|
||||
description,
|
||||
members == null ? null : getRecipientIds(members),
|
||||
|
@ -831,267 +714,10 @@ public class Manager implements Closeable {
|
|||
isAnnouncementGroup);
|
||||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupInternal(
|
||||
final GroupId groupId,
|
||||
final String name,
|
||||
final String description,
|
||||
final Set<RecipientId> members,
|
||||
final Set<RecipientId> removeMembers,
|
||||
final Set<RecipientId> admins,
|
||||
final Set<RecipientId> removeAdmins,
|
||||
final boolean resetGroupLink,
|
||||
final GroupLinkState groupLinkState,
|
||||
final GroupPermission addMemberPermission,
|
||||
final GroupPermission editDetailsPermission,
|
||||
final File avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
|
||||
if (group instanceof GroupInfoV2) {
|
||||
try {
|
||||
return updateGroupV2((GroupInfoV2) group,
|
||||
name,
|
||||
description,
|
||||
members,
|
||||
removeMembers,
|
||||
admins,
|
||||
removeAdmins,
|
||||
resetGroupLink,
|
||||
groupLinkState,
|
||||
addMemberPermission,
|
||||
editDetailsPermission,
|
||||
avatarFile,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
} catch (ConflictException e) {
|
||||
// Detected conflicting update, refreshing group and trying again
|
||||
group = getGroup(groupId, true);
|
||||
return updateGroupV2((GroupInfoV2) group,
|
||||
name,
|
||||
description,
|
||||
members,
|
||||
removeMembers,
|
||||
admins,
|
||||
removeAdmins,
|
||||
resetGroupLink,
|
||||
groupLinkState,
|
||||
addMemberPermission,
|
||||
editDetailsPermission,
|
||||
avatarFile,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
}
|
||||
}
|
||||
|
||||
final var gv1 = (GroupInfoV1) group;
|
||||
final var result = updateGroupV1(gv1, name, members, avatarFile);
|
||||
if (expirationTimer != null) {
|
||||
setExpirationTimer(gv1, expirationTimer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupV1(
|
||||
final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
updateGroupV1Details(gv1, name, members, avatarFile);
|
||||
|
||||
account.getGroupStore().updateGroup(gv1);
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(gv1);
|
||||
return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
private void updateGroupV1Details(
|
||||
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
|
||||
) throws IOException {
|
||||
if (name != null) {
|
||||
g.name = name;
|
||||
}
|
||||
|
||||
if (members != null) {
|
||||
final var newMemberAddresses = members.stream()
|
||||
.filter(member -> !g.isMember(member))
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList());
|
||||
final var newE164Members = new HashSet<String>();
|
||||
for (var member : newMemberAddresses) {
|
||||
if (!member.getNumber().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
newE164Members.add(member.getNumber().get());
|
||||
}
|
||||
|
||||
final var registeredUsers = getRegisteredUsers(newE164Members);
|
||||
if (registeredUsers.size() != newE164Members.size()) {
|
||||
// Some of the new members are not registered on Signal
|
||||
newE164Members.removeAll(registeredUsers.keySet());
|
||||
throw new IOException("Failed to add members "
|
||||
+ String.join(", ", newE164Members)
|
||||
+ " to group: Not registered on Signal");
|
||||
}
|
||||
|
||||
g.addMembers(members);
|
||||
}
|
||||
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(g.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupV2(
|
||||
final GroupInfoV2 group,
|
||||
final String name,
|
||||
final String description,
|
||||
final Set<RecipientId> members,
|
||||
final Set<RecipientId> removeMembers,
|
||||
final Set<RecipientId> admins,
|
||||
final Set<RecipientId> removeAdmins,
|
||||
final boolean resetGroupLink,
|
||||
final GroupLinkState groupLinkState,
|
||||
final GroupPermission addMemberPermission,
|
||||
final GroupPermission editDetailsPermission,
|
||||
final File avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
) throws IOException {
|
||||
SendGroupMessageResults result = null;
|
||||
if (group.isPendingMember(account.getSelfRecipientId())) {
|
||||
var groupGroupChangePair = groupV2Helper.acceptInvite(group);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (members != null) {
|
||||
final var newMembers = new HashSet<>(members);
|
||||
newMembers.removeAll(group.getMembers());
|
||||
if (newMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
if (removeMembers != null) {
|
||||
var existingRemoveMembers = new HashSet<>(removeMembers);
|
||||
existingRemoveMembers.retainAll(group.getMembers());
|
||||
existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage
|
||||
if (existingRemoveMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
var pendingRemoveMembers = new HashSet<>(removeMembers);
|
||||
pendingRemoveMembers.retainAll(group.getPendingMembers());
|
||||
if (pendingRemoveMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
if (admins != null) {
|
||||
final var newAdmins = new HashSet<>(admins);
|
||||
newAdmins.retainAll(group.getMembers());
|
||||
newAdmins.removeAll(group.getAdminMembers());
|
||||
if (newAdmins.size() > 0) {
|
||||
for (var admin : newAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removeAdmins != null) {
|
||||
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
||||
existingRemoveAdmins.retainAll(group.getAdminMembers());
|
||||
if (existingRemoveAdmins.size() > 0) {
|
||||
for (var admin : existingRemoveAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resetGroupLink) {
|
||||
var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (groupLinkState != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (addMemberPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (editDetailsPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (expirationTimer != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (isAnnouncementGroup != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (name != null || description != null || avatarFile != null) {
|
||||
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(group.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Pair<GroupId, SendGroupMessageResults> joinGroup(
|
||||
GroupInviteLinkUrl inviteLinkUrl
|
||||
) throws IOException, GroupLinkNotActiveException {
|
||||
final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
|
||||
inviteLinkUrl.getPassword());
|
||||
final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||
inviteLinkUrl.getPassword(),
|
||||
groupJoinInfo);
|
||||
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||
groupJoinInfo.getRevision() + 1,
|
||||
groupChange.toByteArray());
|
||||
|
||||
if (group.getGroup() == null) {
|
||||
// Only requested member, can't send update to group members
|
||||
return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of()));
|
||||
}
|
||||
|
||||
final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
|
||||
|
||||
return new Pair<>(group.getGroupId(), result);
|
||||
}
|
||||
|
||||
private SendGroupMessageResults sendUpdateGroupV2Message(
|
||||
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
|
||||
) throws IOException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
|
||||
group.setGroup(newDecryptedGroup, this::resolveRecipient);
|
||||
members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
|
||||
account.getGroupStore().updateGroup(group);
|
||||
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
|
||||
return sendGroupMessage(messageBuilder, members);
|
||||
return groupHelper.joinGroup(inviteLinkUrl);
|
||||
}
|
||||
|
||||
public SendMessageResults sendMessage(
|
||||
|
@ -1134,100 +760,18 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private SendGroupMessageResults sendGroupMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder, final Set<RecipientId> members
|
||||
) throws IOException {
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
messageBuilder.withTimestamp(timestamp);
|
||||
final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members);
|
||||
return new SendGroupMessageResults(timestamp, results);
|
||||
}
|
||||
|
||||
private static int currentTimeDays() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getGroupAuthForToday(
|
||||
final GroupSecretParams groupSecretParams
|
||||
) throws IOException {
|
||||
final var today = currentTimeDays();
|
||||
// Returns credentials for the next 7 days
|
||||
final var credentials = dependencies.getGroupsV2Api().getCredentials(today);
|
||||
// TODO cache credentials until they expire
|
||||
var authCredentialResponse = credentials.get(today);
|
||||
try {
|
||||
return dependencies.getGroupsV2Api()
|
||||
.getGroupsV2AuthorizationString(account.getUuid(),
|
||||
today,
|
||||
groupSecretParams,
|
||||
authCredentialResponse);
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
SendGroupMessageResults sendGroupInfoMessage(
|
||||
GroupIdV1 groupId, SignalServiceAddress recipient
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
|
||||
GroupInfoV1 g;
|
||||
var group = getGroupForUpdating(groupId);
|
||||
if (!(group instanceof GroupInfoV1)) {
|
||||
throw new IOException("Received an invalid group request for a v2 group!");
|
||||
}
|
||||
g = (GroupInfoV1) group;
|
||||
|
||||
final var recipientId = resolveRecipient(recipient);
|
||||
if (!g.isMember(recipientId)) {
|
||||
throw new NotAGroupMemberException(groupId, g.name);
|
||||
}
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(g);
|
||||
|
||||
// Send group message only to the recipient who requested it
|
||||
return sendGroupMessage(messageBuilder, Set.of(recipientId));
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
||||
.withId(g.getGroupId().serialize())
|
||||
.withName(g.name)
|
||||
.withMembers(g.getMembers()
|
||||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
try {
|
||||
final var attachment = createGroupAvatarAttachment(g.getGroupId());
|
||||
if (attachment.isPresent()) {
|
||||
group.withAvatar(attachment.get());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
|
||||
}
|
||||
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(group.build())
|
||||
.withExpiration(g.getMessageExpirationTime());
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
|
||||
var group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
|
||||
.withRevision(g.getGroup().getRevision())
|
||||
.withSignedGroupChange(signedGroupChange);
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(group.build())
|
||||
.withExpiration(g.getMessageExpirationTime());
|
||||
return groupHelper.sendGroupInfoMessage(groupId, recipientId);
|
||||
}
|
||||
|
||||
SendGroupMessageResults sendGroupInfoRequest(
|
||||
GroupIdV1 groupId, SignalServiceAddress recipient
|
||||
) throws IOException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
|
||||
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
||||
|
||||
// Send group info request message to the recipient who sent us a message with this groupId
|
||||
return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient)));
|
||||
final var recipientId = resolveRecipient(recipient);
|
||||
return groupHelper.sendGroupInfoRequest(groupId, recipientId);
|
||||
}
|
||||
|
||||
public void sendReadReceipt(
|
||||
|
@ -1361,6 +905,7 @@ public class Manager implements Closeable {
|
|||
private void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
// TODO cycle our profile key
|
||||
account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build());
|
||||
}
|
||||
|
||||
|
@ -1371,19 +916,10 @@ public class Manager implements Closeable {
|
|||
}
|
||||
|
||||
group.setBlocked(blocked);
|
||||
// TODO cycle our profile key
|
||||
account.getGroupStore().updateGroup(group);
|
||||
}
|
||||
|
||||
private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
|
||||
return;
|
||||
}
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
account.getContactStore()
|
||||
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the expiration timer for a contact
|
||||
*/
|
||||
|
@ -1400,20 +936,14 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the expiration timer for a group
|
||||
*/
|
||||
private void setExpirationTimer(
|
||||
GroupInfoV1 groupInfoV1, int messageExpirationTimer
|
||||
) throws NotAGroupMemberException, GroupNotFoundException, IOException {
|
||||
groupInfoV1.messageExpirationTime = messageExpirationTimer;
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
sendExpirationTimerUpdate(groupInfoV1.getGroupId());
|
||||
}
|
||||
|
||||
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException {
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
|
||||
sendHelper.sendAsGroupMessage(messageBuilder, groupId);
|
||||
private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) {
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) {
|
||||
return;
|
||||
}
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
account.getContactStore()
|
||||
.storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1583,10 +1113,6 @@ public class Manager implements Closeable {
|
|||
sendTypingMessage(action.toSignalService(), recipients);
|
||||
}
|
||||
|
||||
private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException {
|
||||
return dependencies.getCipher().decrypt(envelope);
|
||||
}
|
||||
|
||||
private void handleEndSession(RecipientId recipientId) {
|
||||
account.getSessionStore().deleteAllSessions(recipientId);
|
||||
}
|
||||
|
@ -1614,7 +1140,7 @@ public class Manager implements Closeable {
|
|||
|
||||
if (groupInfo.getAvatar().isPresent()) {
|
||||
var avatar = groupInfo.getAvatar().get();
|
||||
downloadGroupAvatar(avatar, groupV1.getGroupId());
|
||||
downloadGroupAvatar(groupV1.getGroupId(), avatar);
|
||||
}
|
||||
|
||||
if (groupInfo.getName().isPresent()) {
|
||||
|
@ -1658,7 +1184,7 @@ public class Manager implements Closeable {
|
|||
final var groupContext = message.getGroupContext().get().getGroupV2().get();
|
||||
final var groupMasterKey = groupContext.getMasterKey();
|
||||
|
||||
getOrMigrateGroup(groupMasterKey,
|
||||
groupHelper.getOrMigrateGroup(groupMasterKey,
|
||||
groupContext.getRevision(),
|
||||
groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null);
|
||||
}
|
||||
|
@ -1743,65 +1269,6 @@ public class Manager implements Closeable {
|
|||
return actions;
|
||||
}
|
||||
|
||||
private GroupInfoV2 getOrMigrateGroup(
|
||||
final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
|
||||
var groupInfo = getGroup(groupId);
|
||||
final GroupInfoV2 groupInfoV2;
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
// Received a v2 group message for a v1 group, we need to locally migrate the group
|
||||
account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
|
||||
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
|
||||
logger.info("Locally migrated group {} to group v2, id: {}",
|
||||
groupInfo.getGroupId().toBase64(),
|
||||
groupInfoV2.getGroupId().toBase64());
|
||||
} else if (groupInfo instanceof GroupInfoV2) {
|
||||
groupInfoV2 = (GroupInfoV2) groupInfo;
|
||||
} else {
|
||||
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
|
||||
}
|
||||
|
||||
if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
|
||||
DecryptedGroup group = null;
|
||||
if (signedGroupChange != null
|
||||
&& groupInfoV2.getGroup() != null
|
||||
&& groupInfoV2.getGroup().getRevision() + 1 == revision) {
|
||||
group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
|
||||
signedGroupChange,
|
||||
groupMasterKey);
|
||||
}
|
||||
if (group == null) {
|
||||
group = groupV2Helper.getDecryptedGroup(groupSecretParams);
|
||||
}
|
||||
if (group != null) {
|
||||
storeProfileKeysFromMembers(group);
|
||||
final var avatar = group.getAvatar();
|
||||
if (avatar != null && !avatar.isEmpty()) {
|
||||
downloadGroupAvatar(groupId, groupSecretParams, avatar);
|
||||
}
|
||||
}
|
||||
groupInfoV2.setGroup(group, this::resolveRecipient);
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
}
|
||||
|
||||
return groupInfoV2;
|
||||
}
|
||||
|
||||
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
||||
for (var member : group.getMembersList()) {
|
||||
final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray());
|
||||
final var recipientId = account.getRecipientStore().resolveRecipient(uuid);
|
||||
try {
|
||||
account.getProfileStore()
|
||||
.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
|
||||
} catch (InvalidInputException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
|
||||
Set<HandleAction> queuedActions = new HashSet<>();
|
||||
for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
|
||||
|
@ -1824,7 +1291,7 @@ public class Manager implements Closeable {
|
|||
List<HandleAction> actions = null;
|
||||
if (!envelope.isReceipt()) {
|
||||
try {
|
||||
content = decryptMessage(envelope);
|
||||
content = dependencies.getCipher().decrypt(envelope);
|
||||
} catch (ProtocolUntrustedIdentityException e) {
|
||||
if (!envelope.hasSource()) {
|
||||
final var identifier = e.getSender();
|
||||
|
@ -1915,7 +1382,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
if (!envelope.isReceipt()) {
|
||||
try {
|
||||
content = decryptMessage(envelope);
|
||||
content = dependencies.getCipher().decrypt(envelope);
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
}
|
||||
|
@ -2157,7 +1624,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
|
||||
if (g.getAvatar().isPresent()) {
|
||||
downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId());
|
||||
downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
|
||||
}
|
||||
syncGroup.archived = g.isArchived();
|
||||
account.getGroupStore().updateGroup(syncGroup);
|
||||
|
@ -2333,7 +1800,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) {
|
||||
private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) {
|
||||
try {
|
||||
avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream));
|
||||
} catch (IOException e) {
|
||||
|
@ -2341,15 +1808,6 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) {
|
||||
try {
|
||||
avatarStore.storeGroupAvatar(groupId,
|
||||
outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadProfileAvatar(
|
||||
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
|
||||
) {
|
||||
|
@ -2392,29 +1850,6 @@ public class Manager implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private void retrieveGroupV2Avatar(
|
||||
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
|
||||
) throws IOException {
|
||||
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
|
||||
var tmpFile = IOUtils.createTempFile();
|
||||
try (InputStream input = dependencies.getMessageReceiver()
|
||||
.retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
|
||||
var encryptedData = IOUtils.readFully(input);
|
||||
|
||||
var decryptedData = groupOperations.decryptAvatar(encryptedData);
|
||||
outputStream.write(decryptedData);
|
||||
} finally {
|
||||
try {
|
||||
Files.delete(tmpFile.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
|
||||
tmpFile,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveProfileAvatar(
|
||||
String avatarPath, ProfileKey profileKey, OutputStream outputStream
|
||||
) throws IOException {
|
||||
|
@ -2490,7 +1925,7 @@ public class Manager implements Closeable {
|
|||
.stream()
|
||||
.map(this::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList()),
|
||||
createGroupAvatarAttachment(groupInfo.getGroupId()),
|
||||
groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()),
|
||||
groupInfo.isMember(account.getSelfRecipientId()),
|
||||
Optional.of(groupInfo.messageExpirationTime),
|
||||
Optional.fromNullable(groupInfo.color),
|
||||
|
@ -2639,17 +2074,7 @@ public class Manager implements Closeable {
|
|||
}
|
||||
|
||||
public GroupInfo getGroup(GroupId groupId) {
|
||||
return getGroup(groupId, false);
|
||||
}
|
||||
|
||||
public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
|
||||
final var group = account.getGroupStore().getGroup(groupId);
|
||||
if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
|
||||
((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
|
||||
account.getGroupStore().updateGroup(group);
|
||||
}
|
||||
return group;
|
||||
return groupHelper.getGroup(groupId);
|
||||
}
|
||||
|
||||
public List<IdentityInfo> getIdentities() {
|
||||
|
|
|
@ -0,0 +1,628 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.AvatarStore;
|
||||
import org.asamk.signal.manager.SignalDependencies;
|
||||
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.groups.GroupId;
|
||||
import org.asamk.signal.manager.groups.GroupIdV1;
|
||||
import org.asamk.signal.manager.groups.GroupIdV2;
|
||||
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
|
||||
import org.asamk.signal.manager.groups.GroupLinkState;
|
||||
import org.asamk.signal.manager.groups.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.groups.GroupPermission;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.groups.LastGroupAdminException;
|
||||
import org.asamk.signal.manager.groups.NotAGroupMemberException;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||
import org.asamk.signal.manager.util.IOUtils;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GroupHelper {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class);
|
||||
|
||||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
private final SendHelper sendHelper;
|
||||
private final GroupV2Helper groupV2Helper;
|
||||
private final AvatarStore avatarStore;
|
||||
private final SignalServiceAddressResolver addressResolver;
|
||||
private final RecipientResolver recipientResolver;
|
||||
|
||||
public GroupHelper(
|
||||
final SignalAccount account,
|
||||
final SignalDependencies dependencies,
|
||||
final SendHelper sendHelper,
|
||||
final GroupV2Helper groupV2Helper,
|
||||
final AvatarStore avatarStore,
|
||||
final SignalServiceAddressResolver addressResolver,
|
||||
final RecipientResolver recipientResolver
|
||||
) {
|
||||
this.account = account;
|
||||
this.dependencies = dependencies;
|
||||
this.sendHelper = sendHelper;
|
||||
this.groupV2Helper = groupV2Helper;
|
||||
this.avatarStore = avatarStore;
|
||||
this.addressResolver = addressResolver;
|
||||
this.recipientResolver = recipientResolver;
|
||||
}
|
||||
|
||||
public GroupInfo getGroup(GroupId groupId) {
|
||||
return getGroup(groupId, false);
|
||||
}
|
||||
|
||||
public Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException {
|
||||
final var streamDetails = avatarStore.retrieveGroupAvatar(groupId);
|
||||
if (streamDetails == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
|
||||
}
|
||||
|
||||
public GroupInfoV2 getOrMigrateGroup(
|
||||
final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
|
||||
var groupInfo = getGroup(groupId);
|
||||
final GroupInfoV2 groupInfoV2;
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
// Received a v2 group message for a v1 group, we need to locally migrate the group
|
||||
account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId());
|
||||
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
|
||||
logger.info("Locally migrated group {} to group v2, id: {}",
|
||||
groupInfo.getGroupId().toBase64(),
|
||||
groupInfoV2.getGroupId().toBase64());
|
||||
} else if (groupInfo instanceof GroupInfoV2) {
|
||||
groupInfoV2 = (GroupInfoV2) groupInfo;
|
||||
} else {
|
||||
groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
|
||||
}
|
||||
|
||||
if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) {
|
||||
DecryptedGroup group = null;
|
||||
if (signedGroupChange != null
|
||||
&& groupInfoV2.getGroup() != null
|
||||
&& groupInfoV2.getGroup().getRevision() + 1 == revision) {
|
||||
group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
|
||||
signedGroupChange,
|
||||
groupMasterKey);
|
||||
}
|
||||
if (group == null) {
|
||||
group = groupV2Helper.getDecryptedGroup(groupSecretParams);
|
||||
}
|
||||
if (group != null) {
|
||||
storeProfileKeysFromMembers(group);
|
||||
final var avatar = group.getAvatar();
|
||||
if (avatar != null && !avatar.isEmpty()) {
|
||||
downloadGroupAvatar(groupId, groupSecretParams, avatar);
|
||||
}
|
||||
}
|
||||
groupInfoV2.setGroup(group, recipientResolver);
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
}
|
||||
|
||||
return groupInfoV2;
|
||||
}
|
||||
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name, Set<RecipientId> members, File avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
if (members != null && members.contains(selfRecipientId)) {
|
||||
members = new HashSet<>(members);
|
||||
members.remove(selfRecipientId);
|
||||
}
|
||||
|
||||
var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name,
|
||||
members == null ? Set.of() : members,
|
||||
avatarFile);
|
||||
|
||||
if (gv2Pair == null) {
|
||||
// Failed to create v2 group, creating v1 group instead
|
||||
var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
|
||||
gv1.addMembers(List.of(selfRecipientId));
|
||||
final var result = updateGroupV1(gv1, name, members, avatarFile);
|
||||
return new Pair<>(gv1.getGroupId(), result);
|
||||
}
|
||||
|
||||
final var gv2 = gv2Pair.first();
|
||||
final var decryptedGroup = gv2Pair.second();
|
||||
|
||||
gv2.setGroup(decryptedGroup, recipientResolver);
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(gv2.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
|
||||
account.getGroupStore().updateGroup(gv2);
|
||||
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
|
||||
|
||||
final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
|
||||
return new Pair<>(gv2.getGroupId(), result);
|
||||
}
|
||||
|
||||
public SendGroupMessageResults updateGroup(
|
||||
final GroupId groupId,
|
||||
final String name,
|
||||
final String description,
|
||||
final Set<RecipientId> members,
|
||||
final Set<RecipientId> removeMembers,
|
||||
final Set<RecipientId> admins,
|
||||
final Set<RecipientId> removeAdmins,
|
||||
final boolean resetGroupLink,
|
||||
final GroupLinkState groupLinkState,
|
||||
final GroupPermission addMemberPermission,
|
||||
final GroupPermission editDetailsPermission,
|
||||
final File avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
|
||||
if (group instanceof GroupInfoV2) {
|
||||
try {
|
||||
return updateGroupV2((GroupInfoV2) group,
|
||||
name,
|
||||
description,
|
||||
members,
|
||||
removeMembers,
|
||||
admins,
|
||||
removeAdmins,
|
||||
resetGroupLink,
|
||||
groupLinkState,
|
||||
addMemberPermission,
|
||||
editDetailsPermission,
|
||||
avatarFile,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
} catch (ConflictException e) {
|
||||
// Detected conflicting update, refreshing group and trying again
|
||||
group = getGroup(groupId, true);
|
||||
return updateGroupV2((GroupInfoV2) group,
|
||||
name,
|
||||
description,
|
||||
members,
|
||||
removeMembers,
|
||||
admins,
|
||||
removeAdmins,
|
||||
resetGroupLink,
|
||||
groupLinkState,
|
||||
addMemberPermission,
|
||||
editDetailsPermission,
|
||||
avatarFile,
|
||||
expirationTimer,
|
||||
isAnnouncementGroup);
|
||||
}
|
||||
}
|
||||
|
||||
final var gv1 = (GroupInfoV1) group;
|
||||
final var result = updateGroupV1(gv1, name, members, avatarFile);
|
||||
if (expirationTimer != null) {
|
||||
setExpirationTimer(gv1, expirationTimer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Pair<GroupId, SendGroupMessageResults> joinGroup(
|
||||
GroupInviteLinkUrl inviteLinkUrl
|
||||
) throws IOException, GroupLinkNotActiveException {
|
||||
final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
|
||||
inviteLinkUrl.getPassword());
|
||||
final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||
inviteLinkUrl.getPassword(),
|
||||
groupJoinInfo);
|
||||
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||
groupJoinInfo.getRevision() + 1,
|
||||
groupChange.toByteArray());
|
||||
|
||||
if (group.getGroup() == null) {
|
||||
// Only requested member, can't send update to group members
|
||||
return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of()));
|
||||
}
|
||||
|
||||
final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
|
||||
|
||||
return new Pair<>(group.getGroupId(), result);
|
||||
}
|
||||
|
||||
public SendGroupMessageResults quitGroup(
|
||||
final GroupId groupId, final Set<RecipientId> newAdmins
|
||||
) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
return quitGroupV1((GroupInfoV1) group);
|
||||
}
|
||||
|
||||
try {
|
||||
return quitGroupV2((GroupInfoV2) group, newAdmins);
|
||||
} catch (ConflictException e) {
|
||||
// Detected conflicting update, refreshing group and trying again
|
||||
group = getGroup(groupId, true);
|
||||
return quitGroupV2((GroupInfoV2) group, newAdmins);
|
||||
}
|
||||
}
|
||||
|
||||
public SendGroupMessageResults sendGroupInfoRequest(
|
||||
GroupIdV1 groupId, RecipientId recipientId
|
||||
) throws IOException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
|
||||
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
||||
|
||||
// Send group info request message to the recipient who sent us a message with this groupId
|
||||
return sendGroupMessage(messageBuilder, Set.of(recipientId));
|
||||
}
|
||||
|
||||
public SendGroupMessageResults sendGroupInfoMessage(
|
||||
GroupIdV1 groupId, RecipientId recipientId
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
|
||||
GroupInfoV1 g;
|
||||
var group = getGroupForUpdating(groupId);
|
||||
if (!(group instanceof GroupInfoV1)) {
|
||||
throw new IOException("Received an invalid group request for a v2 group!");
|
||||
}
|
||||
g = (GroupInfoV1) group;
|
||||
|
||||
if (!g.isMember(recipientId)) {
|
||||
throw new NotAGroupMemberException(groupId, g.name);
|
||||
}
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(g);
|
||||
|
||||
// Send group message only to the recipient who requested it
|
||||
return sendGroupMessage(messageBuilder, Set.of(recipientId));
|
||||
}
|
||||
|
||||
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
|
||||
final var group = account.getGroupStore().getGroup(groupId);
|
||||
if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
|
||||
((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver);
|
||||
account.getGroupStore().updateGroup(group);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) {
|
||||
try {
|
||||
avatarStore.storeGroupAvatar(groupId,
|
||||
outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveGroupV2Avatar(
|
||||
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
|
||||
) throws IOException {
|
||||
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
|
||||
var tmpFile = IOUtils.createTempFile();
|
||||
try (InputStream input = dependencies.getMessageReceiver()
|
||||
.retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) {
|
||||
var encryptedData = IOUtils.readFully(input);
|
||||
|
||||
var decryptedData = groupOperations.decryptAvatar(encryptedData);
|
||||
outputStream.write(decryptedData);
|
||||
} finally {
|
||||
try {
|
||||
Files.delete(tmpFile.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}",
|
||||
tmpFile,
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void storeProfileKeysFromMembers(final DecryptedGroup group) {
|
||||
for (var member : group.getMembersList()) {
|
||||
final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray());
|
||||
final var recipientId = account.getRecipientStore().resolveRecipient(uuid);
|
||||
try {
|
||||
account.getProfileStore()
|
||||
.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
|
||||
} catch (InvalidInputException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
||||
var g = getGroup(groupId);
|
||||
if (g == null) {
|
||||
throw new GroupNotFoundException(groupId);
|
||||
}
|
||||
if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) {
|
||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupV1(
|
||||
final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
updateGroupV1Details(gv1, name, members, avatarFile);
|
||||
|
||||
account.getGroupStore().updateGroup(gv1);
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(gv1);
|
||||
return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
private void updateGroupV1Details(
|
||||
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
|
||||
) throws IOException {
|
||||
if (name != null) {
|
||||
g.name = name;
|
||||
}
|
||||
|
||||
if (members != null) {
|
||||
g.addMembers(members);
|
||||
}
|
||||
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(g.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the expiration timer for a group
|
||||
*/
|
||||
private void setExpirationTimer(
|
||||
GroupInfoV1 groupInfoV1, int messageExpirationTimer
|
||||
) throws NotAGroupMemberException, GroupNotFoundException, IOException {
|
||||
groupInfoV1.messageExpirationTime = messageExpirationTimer;
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
sendExpirationTimerUpdate(groupInfoV1.getGroupId());
|
||||
}
|
||||
|
||||
private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException {
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate();
|
||||
sendHelper.sendAsGroupMessage(messageBuilder, groupId);
|
||||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupV2(
|
||||
final GroupInfoV2 group,
|
||||
final String name,
|
||||
final String description,
|
||||
final Set<RecipientId> members,
|
||||
final Set<RecipientId> removeMembers,
|
||||
final Set<RecipientId> admins,
|
||||
final Set<RecipientId> removeAdmins,
|
||||
final boolean resetGroupLink,
|
||||
final GroupLinkState groupLinkState,
|
||||
final GroupPermission addMemberPermission,
|
||||
final GroupPermission editDetailsPermission,
|
||||
final File avatarFile,
|
||||
final Integer expirationTimer,
|
||||
final Boolean isAnnouncementGroup
|
||||
) throws IOException {
|
||||
SendGroupMessageResults result = null;
|
||||
if (group.isPendingMember(account.getSelfRecipientId())) {
|
||||
var groupGroupChangePair = groupV2Helper.acceptInvite(group);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (members != null) {
|
||||
final var newMembers = new HashSet<>(members);
|
||||
newMembers.removeAll(group.getMembers());
|
||||
if (newMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
if (removeMembers != null) {
|
||||
var existingRemoveMembers = new HashSet<>(removeMembers);
|
||||
existingRemoveMembers.retainAll(group.getMembers());
|
||||
existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
|
||||
if (existingRemoveMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
var pendingRemoveMembers = new HashSet<>(removeMembers);
|
||||
pendingRemoveMembers.retainAll(group.getPendingMembers());
|
||||
if (pendingRemoveMembers.size() > 0) {
|
||||
var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
if (admins != null) {
|
||||
final var newAdmins = new HashSet<>(admins);
|
||||
newAdmins.retainAll(group.getMembers());
|
||||
newAdmins.removeAll(group.getAdminMembers());
|
||||
if (newAdmins.size() > 0) {
|
||||
for (var admin : newAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removeAdmins != null) {
|
||||
final var existingRemoveAdmins = new HashSet<>(removeAdmins);
|
||||
existingRemoveAdmins.retainAll(group.getAdminMembers());
|
||||
if (existingRemoveAdmins.size() > 0) {
|
||||
for (var admin : existingRemoveAdmins) {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resetGroupLink) {
|
||||
var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (groupLinkState != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (addMemberPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (editDetailsPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (expirationTimer != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (isAnnouncementGroup != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup);
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (name != null || description != null || avatarFile != null) {
|
||||
var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
|
||||
if (avatarFile != null) {
|
||||
avatarStore.storeGroupAvatar(group.getGroupId(),
|
||||
outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
|
||||
}
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
|
||||
.withId(groupInfoV1.getGroupId().serialize())
|
||||
.build();
|
||||
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group);
|
||||
groupInfoV1.removeMember(account.getSelfRecipientId());
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
return sendGroupMessage(messageBuilder,
|
||||
groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
private SendGroupMessageResults quitGroupV2(
|
||||
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
|
||||
) throws LastGroupAdminException, IOException {
|
||||
final var currentAdmins = groupInfoV2.getAdminMembers();
|
||||
newAdmins.removeAll(currentAdmins);
|
||||
newAdmins.retainAll(groupInfoV2.getMembers());
|
||||
if (currentAdmins.contains(account.getSelfRecipientId())
|
||||
&& currentAdmins.size() == 1
|
||||
&& groupInfoV2.getMembers().size() > 1
|
||||
&& newAdmins.size() == 0) {
|
||||
// Last admin can't leave the group, unless she's also the last member
|
||||
throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle());
|
||||
}
|
||||
final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins);
|
||||
groupInfoV2.setGroup(groupGroupChangePair.first(), recipientResolver);
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
|
||||
return sendGroupMessage(messageBuilder,
|
||||
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
|
||||
.withId(g.getGroupId().serialize())
|
||||
.withName(g.name)
|
||||
.withMembers(g.getMembers()
|
||||
.stream()
|
||||
.map(addressResolver::resolveSignalServiceAddress)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
try {
|
||||
final var attachment = createGroupAvatarAttachment(g.getGroupId());
|
||||
if (attachment.isPresent()) {
|
||||
group.withAvatar(attachment.get());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AttachmentInvalidException(g.getGroupId().toBase64(), e);
|
||||
}
|
||||
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(group.build())
|
||||
.withExpiration(g.getMessageExpirationTime());
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
|
||||
var group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
|
||||
.withRevision(g.getGroup().getRevision())
|
||||
.withSignedGroupChange(signedGroupChange);
|
||||
return SignalServiceDataMessage.newBuilder()
|
||||
.asGroupMessage(group.build())
|
||||
.withExpiration(g.getMessageExpirationTime());
|
||||
}
|
||||
|
||||
private SendGroupMessageResults sendUpdateGroupV2Message(
|
||||
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
|
||||
) throws IOException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
|
||||
group.setGroup(newDecryptedGroup, recipientResolver);
|
||||
members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
|
||||
account.getGroupStore().updateGroup(group);
|
||||
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
|
||||
return sendGroupMessage(messageBuilder, members);
|
||||
}
|
||||
|
||||
private SendGroupMessageResults sendGroupMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder, final Set<RecipientId> members
|
||||
) throws IOException {
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
messageBuilder.withTimestamp(timestamp);
|
||||
final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members);
|
||||
return new SendGroupMessageResults(timestamp, results);
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GroupV2Helper {
|
||||
|
@ -59,8 +60,6 @@ public class GroupV2Helper {
|
|||
|
||||
private final GroupsV2Api groupsV2Api;
|
||||
|
||||
private final GroupAuthorizationProvider groupAuthorizationProvider;
|
||||
|
||||
private final SignalServiceAddressResolver addressResolver;
|
||||
|
||||
public GroupV2Helper(
|
||||
|
@ -69,7 +68,6 @@ public class GroupV2Helper {
|
|||
final SelfRecipientIdProvider selfRecipientIdProvider,
|
||||
final GroupsV2Operations groupsV2Operations,
|
||||
final GroupsV2Api groupsV2Api,
|
||||
final GroupAuthorizationProvider groupAuthorizationProvider,
|
||||
final SignalServiceAddressResolver addressResolver
|
||||
) {
|
||||
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
|
||||
|
@ -77,14 +75,12 @@ public class GroupV2Helper {
|
|||
this.selfRecipientIdProvider = selfRecipientIdProvider;
|
||||
this.groupsV2Operations = groupsV2Operations;
|
||||
this.groupsV2Api = groupsV2Api;
|
||||
this.groupAuthorizationProvider = groupAuthorizationProvider;
|
||||
this.addressResolver = addressResolver;
|
||||
}
|
||||
|
||||
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
|
||||
try {
|
||||
final var groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday(
|
||||
groupSecretParams);
|
||||
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
|
||||
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
|
||||
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
|
||||
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
|
||||
|
@ -99,7 +95,7 @@ public class GroupV2Helper {
|
|||
|
||||
return groupsV2Api.getGroupJoinInfo(groupSecretParams,
|
||||
Optional.fromNullable(password).transform(GroupLinkPassword::serialize),
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
|
||||
getGroupAuthForToday(groupSecretParams));
|
||||
}
|
||||
|
||||
public Pair<GroupInfoV2, DecryptedGroup> createGroup(
|
||||
|
@ -116,7 +112,7 @@ public class GroupV2Helper {
|
|||
final GroupsV2AuthorizationString groupAuthForToday;
|
||||
final DecryptedGroup decryptedGroup;
|
||||
try {
|
||||
groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams);
|
||||
groupAuthForToday = getGroupAuthForToday(groupSecretParams);
|
||||
groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
|
||||
decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
|
||||
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
|
||||
|
@ -214,7 +210,7 @@ public class GroupV2Helper {
|
|||
final var avatarBytes = readAvatarBytes(avatarFile);
|
||||
var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes,
|
||||
groupSecretParams,
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams));
|
||||
getGroupAuthForToday(groupSecretParams));
|
||||
change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
|
||||
}
|
||||
|
||||
|
@ -487,7 +483,7 @@ public class GroupV2Helper {
|
|||
}
|
||||
|
||||
var signedGroupChange = groupsV2Api.patchGroup(changeActions,
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
|
||||
getGroupAuthForToday(groupSecretParams),
|
||||
Optional.absent());
|
||||
|
||||
return new Pair<>(decryptedGroupState, signedGroupChange);
|
||||
|
@ -503,7 +499,7 @@ public class GroupV2Helper {
|
|||
final var changeActions = change.setRevision(nextRevision).build();
|
||||
|
||||
return groupsV2Api.patchGroup(changeActions,
|
||||
groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams),
|
||||
getGroupAuthForToday(groupSecretParams),
|
||||
Optional.fromNullable(password).transform(GroupLinkPassword::serialize));
|
||||
}
|
||||
|
||||
|
@ -534,4 +530,26 @@ public class GroupV2Helper {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int currentTimeDays() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private GroupsV2AuthorizationString getGroupAuthForToday(
|
||||
final GroupSecretParams groupSecretParams
|
||||
) throws IOException {
|
||||
final var today = currentTimeDays();
|
||||
// Returns credentials for the next 7 days
|
||||
final var credentials = groupsV2Api.getCredentials(today);
|
||||
// TODO cache credentials until they expire
|
||||
var authCredentialResponse = credentials.get(today);
|
||||
final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId())
|
||||
.getUuid()
|
||||
.get();
|
||||
try {
|
||||
return groupsV2Api.getGroupsV2AuthorizationString(uuid, today, groupSecretParams, authCredentialResponse);
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue