Extract AttachmentHelper and SyncHelper

This commit is contained in:
AsamK 2021-08-26 12:05:15 +02:00
parent e532a24cf8
commit debbaa81ba
4 changed files with 562 additions and 455 deletions

View file

@ -35,11 +35,13 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.helper.AttachmentHelper;
import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupHelper;
import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.GroupV2Helper;
import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SendHelper;
import org.asamk.signal.manager.helper.SyncHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Context;
import org.asamk.signal.manager.jobs.Job; import org.asamk.signal.manager.jobs.Job;
@ -55,8 +57,6 @@ import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.Sticker;
import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.StickerUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
@ -69,7 +69,6 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
@ -82,30 +81,15 @@ import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.DeviceNameUtil;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@ -113,23 +97,17 @@ import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableExcept
import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -165,6 +143,8 @@ public class Manager implements Closeable {
private final ProfileHelper profileHelper; private final ProfileHelper profileHelper;
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final SendHelper sendHelper; private final SendHelper sendHelper;
private final SyncHelper syncHelper;
private final AttachmentHelper attachmentHelper;
private final GroupHelper groupHelper; private final GroupHelper groupHelper;
private final AvatarStore avatarStore; private final AvatarStore avatarStore;
@ -204,6 +184,7 @@ public class Manager implements Closeable {
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore);
this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); this.pinHelper = new PinHelper(dependencies.getKeyBackupService());
final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
account.getProfileStore()::getProfileKey, account.getProfileStore()::getProfileKey,
@ -233,11 +214,19 @@ public class Manager implements Closeable {
this::refreshRegisteredUser); this::refreshRegisteredUser);
this.groupHelper = new GroupHelper(account, this.groupHelper = new GroupHelper(account,
dependencies, dependencies,
attachmentHelper,
sendHelper, sendHelper,
groupV2Helper, groupV2Helper,
avatarStore, avatarStore,
this::resolveSignalServiceAddress, this::resolveSignalServiceAddress,
this::resolveRecipient); this::resolveRecipient);
this.syncHelper = new SyncHelper(account,
attachmentHelper,
sendHelper,
groupHelper,
avatarStore,
this::resolveSignalServiceAddress,
this::resolveRecipient);
} }
public String getUsername() { public String getUsername() {
@ -376,12 +365,7 @@ public class Manager implements Closeable {
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException { ) throws IOException {
profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar);
syncHelper.sendSyncFetchProfileMessage();
sendSyncFetchProfileMessage();
}
private void sendSyncFetchProfileMessage() throws IOException {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
} }
public void unregister() throws IOException { public void unregister() throws IOException {
@ -495,15 +479,6 @@ public class Manager implements Closeable {
profileHelper.refreshRecipientProfile(recipientId); profileHelper.refreshRecipientProfile(recipientId);
} }
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
final var streamDetails = avatarStore.retrieveContactAvatar(address);
if (streamDetails == null) {
return Optional.absent();
}
return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
}
public List<GroupInfo> getGroups() { public List<GroupInfo> getGroups() {
return account.getGroupStore().getGroups(); return account.getGroupStore().getGroups();
} }
@ -516,8 +491,7 @@ public class Manager implements Closeable {
} }
public void deleteGroup(GroupId groupId) throws IOException { public void deleteGroup(GroupId groupId) throws IOException {
account.getGroupStore().deleteGroup(groupId); groupHelper.deleteGroup(groupId);
avatarStore.deleteGroupAvatar(groupId);
} }
public Pair<GroupId, SendGroupMessageResults> createGroup( public Pair<GroupId, SendGroupMessageResults> createGroup(
@ -660,21 +634,9 @@ public class Manager implements Closeable {
final SignalServiceDataMessage.Builder messageBuilder, final Message message final SignalServiceDataMessage.Builder messageBuilder, final Message message
) throws AttachmentInvalidException, IOException { ) throws AttachmentInvalidException, IOException {
messageBuilder.withBody(message.getMessageText()); messageBuilder.withBody(message.getMessageText());
if (message.getAttachments() != null) { final var attachments = message.getAttachments();
var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); if (attachments != null) {
messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments));
// Upload attachments here, so we only upload once even for multiple recipients
var messageSender = dependencies.getMessageSender();
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachment : attachmentStreams) {
if (attachment.isStream()) {
attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream()));
} else if (attachment.isPointer()) {
attachmentPointers.add(attachment.asPointer());
}
}
messageBuilder.withAttachments(attachmentPointers);
} }
} }
@ -822,51 +784,7 @@ public class Manager implements Closeable {
} }
public void requestAllSyncData() throws IOException { public void requestAllSyncData() throws IOException {
requestSyncGroups(); syncHelper.requestAllSyncData();
requestSyncContacts();
requestSyncBlocked();
requestSyncConfiguration();
requestSyncKeys();
}
private void requestSyncGroups() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncContacts() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncBlocked() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncConfiguration() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncKeys() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
} }
private byte[] getSenderCertificate() { private byte[] getSenderCertificate() {
@ -984,7 +902,7 @@ public class Manager implements Closeable {
if (groupInfo.getAvatar().isPresent()) { if (groupInfo.getAvatar().isPresent()) {
var avatar = groupInfo.getAvatar().get(); var avatar = groupInfo.getAvatar().get();
downloadGroupAvatar(groupV1.getGroupId(), avatar); groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar);
} }
if (groupInfo.getName().isPresent()) { if (groupInfo.getName().isPresent()) {
@ -1059,13 +977,31 @@ public class Manager implements Closeable {
if (!ignoreAttachments) { if (!ignoreAttachments) {
if (message.getAttachments().isPresent()) { if (message.getAttachments().isPresent()) {
for (var attachment : message.getAttachments().get()) { for (var attachment : message.getAttachments().get()) {
downloadAttachment(attachment); attachmentHelper.downloadAttachment(attachment);
} }
} }
if (message.getSharedContacts().isPresent()) { if (message.getSharedContacts().isPresent()) {
for (var contact : message.getSharedContacts().get()) { for (var contact : message.getSharedContacts().get()) {
if (contact.getAvatar().isPresent()) { if (contact.getAvatar().isPresent()) {
downloadAttachment(contact.getAvatar().get().getAttachment()); attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment());
}
}
}
if (message.getPreviews().isPresent()) {
final var previews = message.getPreviews().get();
for (var preview : previews) {
if (preview.getImage().isPresent()) {
attachmentHelper.downloadAttachment(preview.getImage().get());
}
}
}
if (message.getQuote().isPresent()) {
final var quote = message.getQuote().get();
for (var quotedAttachment : quote.getAttachments()) {
final var thumbnail = quotedAttachment.getThumbnail();
if (thumbnail != null) {
attachmentHelper.downloadAttachment(thumbnail);
} }
} }
} }
@ -1082,24 +1018,6 @@ public class Manager implements Closeable {
} }
this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey);
} }
if (message.getPreviews().isPresent()) {
final var previews = message.getPreviews().get();
for (var preview : previews) {
if (preview.getImage().isPresent()) {
downloadAttachment(preview.getImage().get());
}
}
}
if (message.getQuote().isPresent()) {
final var quote = message.getQuote().get();
for (var quotedAttachment : quote.getAttachments()) {
final var thumbnail = quotedAttachment.getThumbnail();
if (thumbnail != null) {
downloadAttachment(thumbnail);
}
}
}
if (message.getSticker().isPresent()) { if (message.getSticker().isPresent()) {
final var messageSticker = message.getSticker().get(); final var messageSticker = message.getSticker().get();
final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId());
@ -1441,65 +1359,11 @@ public class Manager implements Closeable {
// TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest();
} }
if (syncMessage.getGroups().isPresent()) { if (syncMessage.getGroups().isPresent()) {
File tmpFile = null;
try { try {
tmpFile = IOUtils.createTempFile();
final var groupsMessage = syncMessage.getGroups().get(); final var groupsMessage = syncMessage.getGroups().get();
try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups);
var s = new DeviceGroupsInputStream(attachmentAsStream);
DeviceGroup g;
while (true) {
try {
g = s.read();
} catch (IOException e) {
logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
continue;
}
if (g == null) {
break;
}
var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
if (syncGroup != null) {
if (g.getName().isPresent()) {
syncGroup.name = g.getName().get();
}
syncGroup.addMembers(g.getMembers()
.stream()
.map(this::resolveRecipient)
.collect(Collectors.toSet()));
if (!g.isActive()) {
syncGroup.removeMember(account.getSelfRecipientId());
} else {
// Add ourself to the member set as it's marked as active
syncGroup.addMembers(List.of(account.getSelfRecipientId()));
}
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
syncGroup.color = g.getColor().get();
}
if (g.getAvatar().isPresent()) {
downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
}
syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup);
}
}
}
} catch (Exception e) { } catch (Exception e) {
logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage());
tmpFile,
e.getMessage());
} finally {
if (tmpFile != null) {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}",
tmpFile,
e.getMessage());
}
}
} }
} }
if (syncMessage.getBlockedList().isPresent()) { if (syncMessage.getBlockedList().isPresent()) {
@ -1520,75 +1384,12 @@ public class Manager implements Closeable {
} }
} }
if (syncMessage.getContacts().isPresent()) { if (syncMessage.getContacts().isPresent()) {
File tmpFile = null;
try { try {
tmpFile = IOUtils.createTempFile();
final var contactsMessage = syncMessage.getContacts().get(); final var contactsMessage = syncMessage.getContacts().get();
try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(),
.asPointer(), tmpFile)) { syncHelper::handleSyncDeviceContacts);
var s = new DeviceContactsInputStream(attachmentAsStream);
DeviceContact c;
while (true) {
try {
c = s.read();
} catch (IOException e) {
logger.warn("Sync contacts contained invalid contact, ignoring: {}",
e.getMessage());
continue;
}
if (c == null) {
break;
}
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
final var recipientId = resolveRecipientTrusted(c.getAddress());
var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null
? Contact.newBuilder()
: Contact.newBuilder(contact);
if (c.getName().isPresent()) {
builder.withName(c.getName().get());
}
if (c.getColor().isPresent()) {
builder.withColor(c.getColor().get());
}
if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
builder.withMessageExpirationTime(c.getExpirationTimer().get());
}
builder.withBlocked(c.isBlocked());
builder.withArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) {
downloadContactAvatar(c.getAvatar().get(), c.getAddress());
}
}
}
} catch (Exception e) { } catch (Exception e) {
logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}", logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage());
tmpFile,
e.getMessage());
} finally {
if (tmpFile != null) {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}",
tmpFile,
e.getMessage());
}
}
} }
} }
if (syncMessage.getVerified().isPresent()) { if (syncMessage.getVerified().isPresent()) {
@ -1647,227 +1448,20 @@ public class Manager implements Closeable {
return actions; return actions;
} }
private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
try {
avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream));
} catch (IOException e) {
logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
}
}
private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) {
try {
avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream));
} catch (IOException e) {
logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
}
}
public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) {
return attachmentStore.getAttachmentFile(attachmentId); return attachmentStore.getAttachmentFile(attachmentId);
} }
private void downloadAttachment(final SignalServiceAttachment attachment) {
if (!attachment.isPointer()) {
logger.warn("Invalid state, can't store an attachment stream.");
}
var pointer = attachment.asPointer();
if (pointer.getPreview().isPresent()) {
final var preview = pointer.getPreview().get();
try {
attachmentStore.storeAttachmentPreview(pointer.getRemoteId(),
outputStream -> outputStream.write(preview, 0, preview.length));
} catch (IOException e) {
logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage());
}
}
try {
attachmentStore.storeAttachment(pointer.getRemoteId(),
outputStream -> retrieveAttachmentPointer(pointer, outputStream));
} catch (IOException e) {
logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage());
}
}
private void retrieveAttachment(
final SignalServiceAttachment attachment, final OutputStream outputStream
) throws IOException {
if (attachment.isPointer()) {
var pointer = attachment.asPointer();
retrieveAttachmentPointer(pointer, outputStream);
} else {
var stream = attachment.asStream();
IOUtils.copyStream(stream.getInputStream(), outputStream);
}
}
private void retrieveAttachmentPointer(
SignalServiceAttachmentPointer pointer, OutputStream outputStream
) throws IOException {
var tmpFile = IOUtils.createTempFile();
try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) {
IOUtils.copyStream(input, outputStream);
} catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e);
} finally {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
tmpFile,
e.getMessage());
}
}
}
private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile
) throws IOException, InvalidMessageException, MissingConfigurationException {
return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
}
void sendGroups() throws IOException { void sendGroups() throws IOException {
var groupsFile = IOUtils.createTempFile(); syncHelper.sendGroups();
try {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
var out = new DeviceGroupsOutputStream(fos);
for (var record : getGroups()) {
if (record instanceof GroupInfoV1) {
var groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name),
groupInfo.getMembers()
.stream()
.map(this::resolveSignalServiceAddress)
.collect(Collectors.toList()),
groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()),
groupInfo.isMember(account.getSelfRecipientId()),
Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color),
groupInfo.blocked,
Optional.absent(),
groupInfo.archived));
}
}
}
if (groupsFile.exists() && groupsFile.length() > 0) {
try (var groupsFileStream = new FileInputStream(groupsFile)) {
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(groupsFileStream)
.withContentType("application/octet-stream")
.withLength(groupsFile.length())
.build();
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
}
}
} finally {
try {
Files.delete(groupsFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
}
}
} }
public void sendContacts() throws IOException { public void sendContacts() throws IOException {
var contactsFile = IOUtils.createTempFile(); syncHelper.sendContacts();
try {
try (OutputStream fos = new FileOutputStream(contactsFile)) {
var out = new DeviceContactsOutputStream(fos);
for (var contactPair : account.getContactStore().getContacts()) {
final var recipientId = contactPair.first();
final var contact = contactPair.second();
final var address = resolveSignalServiceAddress(recipientId);
var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address,
currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAdded().getTime());
}
var profileKey = account.getProfileStore().getProfileKey(recipientId);
out.write(new DeviceContact(address,
Optional.fromNullable(contact.getName()),
createContactAvatarAttachment(address),
Optional.fromNullable(contact.getColor()),
Optional.fromNullable(verifiedMessage),
Optional.fromNullable(profileKey),
contact.isBlocked(),
Optional.of(contact.getMessageExpirationTime()),
Optional.absent(),
contact.isArchived()));
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
out.write(new DeviceContact(account.getSelfAddress(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(account.getProfileKey()),
false,
Optional.absent(),
Optional.absent(),
false));
}
}
if (contactsFile.exists() && contactsFile.length() > 0) {
try (var contactsFileStream = new FileInputStream(contactsFile)) {
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(contactsFileStream)
.withContentType("application/octet-stream")
.withLength(contactsFile.length())
.build();
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream,
true)));
}
}
} finally {
try {
Files.delete(contactsFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
}
}
} }
void sendBlockedList() throws IOException { void sendBlockedList() throws IOException {
var addresses = new ArrayList<SignalServiceAddress>(); syncHelper.sendBlockedList();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
addresses.add(resolveSignalServiceAddress(record.first()));
}
}
var groupIds = new ArrayList<byte[]>();
for (var record : getGroups()) {
if (record.isBlocked()) {
groupIds.add(record.getGroupId().serialize());
}
}
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
private void sendVerifiedMessage(
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
) throws IOException {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
trustLevel.toVerifiedState(),
System.currentTimeMillis());
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
} }
public List<Pair<RecipientId, Contact>> getContacts() { public List<Pair<RecipientId, Contact>> getContacts() {
@ -1974,7 +1568,7 @@ public class Manager implements Closeable {
account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
try { try {
var address = account.getRecipientStore().resolveServiceAddress(recipientId); var address = account.getRecipientStore().resolveServiceAddress(recipientId);
sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to send verification sync message: {}", e.getMessage()); logger.warn("Failed to send verification sync message: {}", e.getMessage());
} }

View file

@ -0,0 +1,122 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.AttachmentStore;
import org.asamk.signal.manager.SignalDependencies;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
public class AttachmentHelper {
private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
private final SignalDependencies dependencies;
private final AttachmentStore attachmentStore;
public AttachmentHelper(
final SignalDependencies dependencies, final AttachmentStore attachmentStore
) {
this.dependencies = dependencies;
this.attachmentStore = attachmentStore;
}
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments);
// Upload attachments here, so we only upload once even for multiple recipients
var messageSender = dependencies.getMessageSender();
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachment : attachmentStreams) {
if (attachment.isStream()) {
attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream()));
} else if (attachment.isPointer()) {
attachmentPointers.add(attachment.asPointer());
}
}
return attachmentPointers;
}
public void downloadAttachment(final SignalServiceAttachment attachment) {
if (!attachment.isPointer()) {
logger.warn("Invalid state, can't store an attachment stream.");
}
var pointer = attachment.asPointer();
if (pointer.getPreview().isPresent()) {
final var preview = pointer.getPreview().get();
try {
attachmentStore.storeAttachmentPreview(pointer.getRemoteId(),
outputStream -> outputStream.write(preview, 0, preview.length));
} catch (IOException e) {
logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage());
}
}
try {
attachmentStore.storeAttachment(pointer.getRemoteId(),
outputStream -> this.retrieveAttachment(pointer, outputStream));
} catch (IOException e) {
logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage());
}
}
void retrieveAttachment(SignalServiceAttachment attachment, OutputStream outputStream) throws IOException {
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
}
public void retrieveAttachment(
SignalServiceAttachment attachment, AttachmentHandler consumer
) throws IOException {
if (attachment.isStream()) {
try (var input = attachment.asStream().getInputStream()) {
consumer.handle(input);
}
return;
}
var tmpFile = IOUtils.createTempFile();
try (var input = retrieveAttachmentAsStream(attachment.asPointer(), tmpFile)) {
consumer.handle(input);
} finally {
try {
Files.delete(tmpFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}",
tmpFile,
e.getMessage());
}
}
}
private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile
) throws IOException {
try {
return dependencies.getMessageReceiver()
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
} catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e);
}
}
@FunctionalInterface
public interface AttachmentHandler {
void handle(InputStream inputStream) throws IOException;
}
}

View file

@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
@ -59,6 +60,7 @@ public class GroupHelper {
private final SignalAccount account; private final SignalAccount account;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final AttachmentHelper attachmentHelper;
private final SendHelper sendHelper; private final SendHelper sendHelper;
private final GroupV2Helper groupV2Helper; private final GroupV2Helper groupV2Helper;
private final AvatarStore avatarStore; private final AvatarStore avatarStore;
@ -68,6 +70,7 @@ public class GroupHelper {
public GroupHelper( public GroupHelper(
final SignalAccount account, final SignalAccount account,
final SignalDependencies dependencies, final SignalDependencies dependencies,
final AttachmentHelper attachmentHelper,
final SendHelper sendHelper, final SendHelper sendHelper,
final GroupV2Helper groupV2Helper, final GroupV2Helper groupV2Helper,
final AvatarStore avatarStore, final AvatarStore avatarStore,
@ -76,6 +79,7 @@ public class GroupHelper {
) { ) {
this.account = account; this.account = account;
this.dependencies = dependencies; this.dependencies = dependencies;
this.attachmentHelper = attachmentHelper;
this.sendHelper = sendHelper; this.sendHelper = sendHelper;
this.groupV2Helper = groupV2Helper; this.groupV2Helper = groupV2Helper;
this.avatarStore = avatarStore; this.avatarStore = avatarStore;
@ -87,6 +91,15 @@ public class GroupHelper {
return getGroup(groupId, false); return getGroup(groupId, false);
} }
public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) {
try {
avatarStore.storeGroupAvatar(groupId,
outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream));
} catch (IOException e) {
logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage());
}
}
public Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException { public Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException {
final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); final var streamDetails = avatarStore.retrieveGroupAvatar(groupId);
if (streamDetails == null) { if (streamDetails == null) {
@ -282,6 +295,11 @@ public class GroupHelper {
} }
} }
public void deleteGroup(GroupId groupId) throws IOException {
account.getGroupStore().deleteGroup(groupId);
avatarStore.deleteGroupAvatar(groupId);
}
public SendGroupMessageResults sendGroupInfoRequest( public SendGroupMessageResults sendGroupInfoRequest(
GroupIdV1 groupId, RecipientId recipientId GroupIdV1 groupId, RecipientId recipientId
) throws IOException { ) throws IOException {

View file

@ -0,0 +1,373 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.AvatarStore;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SyncHelper {
private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class);
private final SignalAccount account;
private final AttachmentHelper attachmentHelper;
private final SendHelper sendHelper;
private final GroupHelper groupHelper;
private final AvatarStore avatarStore;
private final SignalServiceAddressResolver addressResolver;
private final RecipientResolver recipientResolver;
public SyncHelper(
final SignalAccount account,
final AttachmentHelper attachmentHelper,
final SendHelper sendHelper,
final GroupHelper groupHelper,
final AvatarStore avatarStore,
final SignalServiceAddressResolver addressResolver,
final RecipientResolver recipientResolver
) {
this.account = account;
this.attachmentHelper = attachmentHelper;
this.sendHelper = sendHelper;
this.groupHelper = groupHelper;
this.avatarStore = avatarStore;
this.addressResolver = addressResolver;
this.recipientResolver = recipientResolver;
}
public void requestAllSyncData() throws IOException {
requestSyncGroups();
requestSyncContacts();
requestSyncBlocked();
requestSyncConfiguration();
requestSyncKeys();
}
public void sendSyncFetchProfileMessage() throws IOException {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
}
public void sendGroups() throws IOException {
var groupsFile = IOUtils.createTempFile();
try {
try (OutputStream fos = new FileOutputStream(groupsFile)) {
var out = new DeviceGroupsOutputStream(fos);
for (var record : account.getGroupStore().getGroups()) {
if (record instanceof GroupInfoV1) {
var groupInfo = (GroupInfoV1) record;
out.write(new DeviceGroup(groupInfo.getGroupId().serialize(),
Optional.fromNullable(groupInfo.name),
groupInfo.getMembers()
.stream()
.map(addressResolver::resolveSignalServiceAddress)
.collect(Collectors.toList()),
groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()),
groupInfo.isMember(account.getSelfRecipientId()),
Optional.of(groupInfo.messageExpirationTime),
Optional.fromNullable(groupInfo.color),
groupInfo.blocked,
Optional.absent(),
groupInfo.archived));
}
}
}
if (groupsFile.exists() && groupsFile.length() > 0) {
try (var groupsFileStream = new FileInputStream(groupsFile)) {
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(groupsFileStream)
.withContentType("application/octet-stream")
.withLength(groupsFile.length())
.build();
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
}
}
} finally {
try {
Files.delete(groupsFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage());
}
}
}
public void sendContacts() throws IOException {
var contactsFile = IOUtils.createTempFile();
try {
try (OutputStream fos = new FileOutputStream(contactsFile)) {
var out = new DeviceContactsOutputStream(fos);
for (var contactPair : account.getContactStore().getContacts()) {
final var recipientId = contactPair.first();
final var contact = contactPair.second();
final var address = addressResolver.resolveSignalServiceAddress(recipientId);
var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId);
VerifiedMessage verifiedMessage = null;
if (currentIdentity != null) {
verifiedMessage = new VerifiedMessage(address,
currentIdentity.getIdentityKey(),
currentIdentity.getTrustLevel().toVerifiedState(),
currentIdentity.getDateAdded().getTime());
}
var profileKey = account.getProfileStore().getProfileKey(recipientId);
out.write(new DeviceContact(address,
Optional.fromNullable(contact.getName()),
createContactAvatarAttachment(address),
Optional.fromNullable(contact.getColor()),
Optional.fromNullable(verifiedMessage),
Optional.fromNullable(profileKey),
contact.isBlocked(),
Optional.of(contact.getMessageExpirationTime()),
Optional.absent(),
contact.isArchived()));
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
out.write(new DeviceContact(account.getSelfAddress(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(account.getProfileKey()),
false,
Optional.absent(),
Optional.absent(),
false));
}
}
if (contactsFile.exists() && contactsFile.length() > 0) {
try (var contactsFileStream = new FileInputStream(contactsFile)) {
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(contactsFileStream)
.withContentType("application/octet-stream")
.withLength(contactsFile.length())
.build();
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream,
true)));
}
}
} finally {
try {
Files.delete(contactsFile.toPath());
} catch (IOException e) {
logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage());
}
}
}
public void sendBlockedList() throws IOException {
var addresses = new ArrayList<SignalServiceAddress>();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
addresses.add(addressResolver.resolveSignalServiceAddress(record.first()));
}
}
var groupIds = new ArrayList<byte[]>();
for (var record : account.getGroupStore().getGroups()) {
if (record.isBlocked()) {
groupIds.add(record.getGroupId().serialize());
}
}
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
public void sendVerifiedMessage(
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
) throws IOException {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
trustLevel.toVerifiedState(),
System.currentTimeMillis());
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
}
public void handleSyncDeviceGroups(final InputStream input) {
final var s = new DeviceGroupsInputStream(input);
DeviceGroup g;
while (true) {
try {
g = s.read();
} catch (IOException e) {
logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage());
continue;
}
if (g == null) {
break;
}
var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId()));
if (syncGroup != null) {
if (g.getName().isPresent()) {
syncGroup.name = g.getName().get();
}
syncGroup.addMembers(g.getMembers()
.stream()
.map(recipientResolver::resolveRecipient)
.collect(Collectors.toSet()));
if (!g.isActive()) {
syncGroup.removeMember(account.getSelfRecipientId());
} else {
// Add ourself to the member set as it's marked as active
syncGroup.addMembers(List.of(account.getSelfRecipientId()));
}
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
syncGroup.color = g.getColor().get();
}
if (g.getAvatar().isPresent()) {
groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
}
syncGroup.archived = g.isArchived();
account.getGroupStore().updateGroup(syncGroup);
}
}
}
public void handleSyncDeviceContacts(final InputStream input) {
final var s = new DeviceContactsInputStream(input);
DeviceContact c;
while (true) {
try {
c = s.read();
} catch (IOException e) {
logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
continue;
}
if (c == null) {
break;
}
if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
account.setProfileKey(c.getProfileKey().get());
}
final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress());
var contact = account.getContactStore().getContact(recipientId);
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
if (c.getName().isPresent()) {
builder.withName(c.getName().get());
}
if (c.getColor().isPresent()) {
builder.withColor(c.getColor().get());
}
if (c.getProfileKey().isPresent()) {
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
}
if (c.getVerified().isPresent()) {
final var verifiedMessage = c.getVerified().get();
account.getIdentityKeyStore()
.setIdentityTrustLevel(account.getRecipientStore()
.resolveRecipientTrusted(verifiedMessage.getDestination()),
verifiedMessage.getIdentityKey(),
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
}
if (c.getExpirationTimer().isPresent()) {
builder.withMessageExpirationTime(c.getExpirationTimer().get());
}
builder.withBlocked(c.isBlocked());
builder.withArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) {
downloadContactAvatar(c.getAvatar().get(), c.getAddress());
}
}
}
private void requestSyncGroups() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncContacts() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncBlocked() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncConfiguration() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private void requestSyncKeys() throws IOException {
var r = SignalServiceProtos.SyncMessage.Request.newBuilder()
.setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS)
.build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
sendHelper.sendSyncMessage(message);
}
private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(SignalServiceAddress address) throws IOException {
final var streamDetails = avatarStore.retrieveContactAvatar(address);
if (streamDetails == null) {
return Optional.absent();
}
return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent()));
}
private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) {
try {
avatarStore.storeContactAvatar(address,
outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream));
} catch (IOException e) {
logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
}
}
}