package org.asamk.signal.manager.helper;
-import com.google.protobuf.ByteString;
-
+import org.asamk.signal.manager.api.Contact;
+import org.asamk.signal.manager.api.GroupId;
+import org.asamk.signal.manager.api.MessageEnvelope.Sync.MessageRequestResponse;
import org.asamk.signal.manager.api.TrustLevel;
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.RecipientAddress;
-import org.asamk.signal.manager.util.AttachmentUtils;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
+import org.asamk.signal.manager.storage.stickers.StickerPack;
import org.asamk.signal.manager.util.IOUtils;
+import org.asamk.signal.manager.util.MimeUtils;
+import org.jetbrains.annotations.NotNull;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
-import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
+import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
+import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactAvatar;
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.KeysMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
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.VerifiedMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
+import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
+import org.whispersystems.signalservice.internal.push.SyncMessage;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class SyncHelper {
- private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class);
+ private static final Logger logger = LoggerFactory.getLogger(SyncHelper.class);
private final Context context;
private final SignalAccount account;
}
public void requestAllSyncData() {
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.GROUPS);
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS);
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED);
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION);
+ requestSyncData(SyncMessage.Request.Type.GROUPS);
+ requestSyncData(SyncMessage.Request.Type.CONTACTS);
+ requestSyncData(SyncMessage.Request.Type.BLOCKED);
+ requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys();
- requestSyncPniIdentity();
}
public void requestSyncKeys() {
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.KEYS);
+ requestSyncData(SyncMessage.Request.Type.KEYS);
}
- public void requestSyncPniIdentity() {
- requestSyncData(SignalServiceProtos.SyncMessage.Request.Type.PNI_IDENTITY);
+ public SendMessageResult sendSyncFetchProfileMessage() {
+ return context.getSendHelper()
+ .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
}
- public void sendSyncFetchProfileMessage() {
+ public void sendSyncFetchStorageMessage() {
context.getSendHelper()
- .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
+ .sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST));
+ }
+
+ public void sendSyncReceiptMessage(ServiceId sender, SignalServiceReceiptMessage receiptMessage) {
+ if (receiptMessage.isReadReceipt()) {
+ final var readMessages = receiptMessage.getTimestamps()
+ .stream()
+ .map(t -> new ReadMessage(sender, t))
+ .toList();
+ context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages));
+ } else if (receiptMessage.isViewedReceipt()) {
+ final var viewedMessages = receiptMessage.getTimestamps()
+ .stream()
+ .map(t -> new ViewedMessage(sender, t))
+ .toList();
+ context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages));
+ }
}
public void sendGroups() throws IOException {
if (groupsFile.exists() && groupsFile.length() > 0) {
try (var groupsFileStream = new FileInputStream(groupsFile)) {
+ final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(groupsFileStream)
- .withContentType("application/octet-stream")
+ .withContentType(MimeUtils.OCTET_STREAM)
.withLength(groupsFile.length())
+ .withResumableUploadSpec(uploadSpec)
.build();
context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
for (var contactPair : account.getContactStore().getContacts()) {
final var recipientId = contactPair.first();
final var contact = contactPair.second();
- final var address = context.getRecipientHelper().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.ofNullable(contact.getName()),
- createContactAvatarAttachment(new RecipientAddress(address)),
- Optional.ofNullable(contact.getColor()),
- Optional.ofNullable(verifiedMessage),
- Optional.ofNullable(profileKey),
- contact.isBlocked(),
- Optional.of(contact.getMessageExpirationTime()),
- Optional.empty(),
- contact.isArchived()));
+ final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
+
+ final var deviceContact = getDeviceContact(address, contact);
+ out.write(deviceContact);
+ deviceContact.getAvatar().ifPresent(a -> {
+ try {
+ a.getInputStream().close();
+ } catch (IOException ignored) {
+ }
+ });
}
if (account.getProfileKey() != null) {
// Send our own profile key as well
- out.write(new DeviceContact(account.getSelfAddress(),
- Optional.empty(),
- Optional.empty(),
- Optional.empty(),
- Optional.empty(),
- Optional.of(account.getProfileKey()),
- false,
- Optional.empty(),
- Optional.empty(),
- false));
+ final var address = account.getSelfRecipientAddress();
+ final var recipientId = account.getSelfRecipientId();
+ final var contact = account.getContactStore().getContact(recipientId);
+ final var deviceContact = getDeviceContact(address, contact);
+ out.write(deviceContact);
+ deviceContact.getAvatar().ifPresent(a -> {
+ try {
+ a.getInputStream().close();
+ } catch (IOException ignored) {
+ }
+ });
}
}
if (contactsFile.exists() && contactsFile.length() > 0) {
try (var contactsFileStream = new FileInputStream(contactsFile)) {
+ final var uploadSpec = context.getDependencies().getMessageSender().getResumableUploadSpec();
var attachmentStream = SignalServiceAttachment.newStreamBuilder()
.withStream(contactsFileStream)
- .withContentType("application/octet-stream")
+ .withContentType(MimeUtils.OCTET_STREAM)
.withLength(contactsFile.length())
+ .withResumableUploadSpec(uploadSpec)
.build();
context.getSendHelper()
}
}
- public void sendBlockedList() {
- var addresses = new ArrayList<SignalServiceAddress>();
+ @NotNull
+ private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
+ return new DeviceContact(address.aci(),
+ address.number(),
+ Optional.ofNullable(contact == null ? null : contact.getName()),
+ createContactAvatarAttachment(address),
+ Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
+ Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
+ Optional.empty());
+ }
+
+ public SendMessageResult sendBlockedList() {
+ var addresses = new ArrayList<BlockedListMessage.Individual>();
for (var record : account.getContactStore().getContacts()) {
if (record.second().isBlocked()) {
- addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
+ final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
+ if (address.aci().isPresent() || address.number().isPresent()) {
+ addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
+ address.number().orElse(null)));
+ }
}
}
var groupIds = new ArrayList<byte[]>();
groupIds.add(record.getGroupId().serialize());
}
}
- context.getSendHelper()
+ return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
- public void sendVerifiedMessage(
- SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
- ) throws IOException {
+ public SendMessageResult sendVerifiedMessage(
+ SignalServiceAddress destination,
+ IdentityKey identityKey,
+ TrustLevel trustLevel
+ ) {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
trustLevel.toVerifiedState(),
System.currentTimeMillis());
- context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
+ return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
+ }
+
+ public SendMessageResult sendKeysMessage() {
+ var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
+ account.getOrCreatePinMasterKey(),
+ account.getOrCreateAccountEntropyPool(),
+ account.getOrCreateMediaRootBackupKey());
+ return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
- public void sendKeysMessage() {
- var keysMessage = new KeysMessage(Optional.ofNullable(account.getStorageKey()));
- context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
+ public SendMessageResult sendStickerOperationsMessage(
+ List<StickerPack> installStickers,
+ List<StickerPack> removeStickers
+ ) {
+ var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
+ var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
+ var stickerMessages = Stream.concat(installStickerMessages, removeStickerMessages).toList();
+ return context.getSendHelper()
+ .sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(stickerMessages));
}
- public void sendConfigurationMessage() {
+ private static StickerPackOperationMessage getStickerPackOperationMessage(
+ final StickerPack s,
+ final boolean installed
+ ) {
+ return new StickerPackOperationMessage(s.packId().serialize(),
+ s.packKey(),
+ installed ? StickerPackOperationMessage.Type.INSTALL : StickerPackOperationMessage.Type.REMOVE);
+ }
+
+ public SendMessageResult sendConfigurationMessage() {
final var config = account.getConfigurationStore();
var configurationMessage = new ConfigurationMessage(Optional.ofNullable(config.getReadReceipts()),
Optional.ofNullable(config.getUnidentifiedDeliveryIndicators()),
Optional.ofNullable(config.getTypingIndicators()),
Optional.ofNullable(config.getLinkPreviews()));
- context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
+ return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
}
- public void sendPniIdentity() {
- final var pniIdentityKeyPair = account.getPniIdentityKeyPair();
- var pniIdentity = SignalServiceProtos.SyncMessage.PniIdentity.newBuilder()
- .setPrivateKey(ByteString.copyFrom(pniIdentityKeyPair.getPrivateKey().serialize()))
- .setPublicKey(ByteString.copyFrom(pniIdentityKeyPair.getPublicKey().serialize()))
- .build();
- context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forPniIdentity(pniIdentity));
+ 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(account.getRecipientResolver()::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()) {
+ context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get());
+ }
+ syncGroup.archived = g.isArchived();
+ account.getGroupStore().updateGroup(syncGroup);
+ }
+ }
}
public void handleSyncDeviceContacts(final InputStream input) throws IOException {
c = s.read();
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
- logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
+ logger.debug("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
continue;
} else {
throw e;
}
}
- if (c == null) {
+ if (c == null || (c.getAci().isEmpty() && c.getE164().isEmpty())) {
break;
}
- if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
- account.setProfileKey(c.getProfileKey().get());
- }
- final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress());
+ final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
+ final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
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.getName().isPresent() && (
+ contact == null || (
+ contact.givenName() == null && contact.familyName() == null
+ )
+ )) {
+ builder.withGivenName(c.getName().get());
+ builder.withFamilyName(null);
}
if (c.getExpirationTimer().isPresent()) {
- builder.withMessageExpirationTime(c.getExpirationTimer().get());
+ if (c.getExpirationTimerVersion().isPresent() && (
+ contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
+ )) {
+ builder.withMessageExpirationTime(c.getExpirationTimer().get());
+ builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
+ } else {
+ logger.debug(
+ "[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
+ recipientId,
+ c.getExpirationTimerVersion(),
+ contact == null ? 1 : contact.messageExpirationTimeVersion());
+ }
}
- builder.withBlocked(c.isBlocked());
- builder.withArchived(c.isArchived());
account.getContactStore().storeContact(recipientId, builder.build());
if (c.getAvatar().isPresent()) {
- downloadContactAvatar(c.getAvatar().get(), new RecipientAddress(c.getAddress()));
+ storeContactAvatar(c.getAvatar().get(), address);
}
}
}
- private void requestSyncData(final SignalServiceProtos.SyncMessage.Request.Type type) {
- var r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(type).build();
+ public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) {
+ final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
+ return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
+ }
+
+ public SendMessageResult sendMessageRequestResponse(
+ final MessageRequestResponse.Type type,
+ final RecipientId recipientId
+ ) {
+ final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
+ if (address.serviceId().isEmpty()) {
+ return null;
+ }
+ context.getContactHelper()
+ .setContactProfileSharing(recipientId,
+ type == MessageRequestResponse.Type.ACCEPT
+ || type == MessageRequestResponse.Type.UNBLOCK_AND_ACCEPT);
+ final var response = MessageRequestResponseMessage.forIndividual(address.serviceId().get(),
+ localToRemoteType(type));
+ return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
+ }
+
+ private SendMessageResult requestSyncData(final SyncMessage.Request.Type type) {
+ var r = new SyncMessage.Request.Builder().type(type).build();
var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
- context.getSendHelper().sendSyncMessage(message);
+ return context.getSendHelper().sendSyncMessage(message);
}
- private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(RecipientAddress address) throws IOException {
+ private Optional<DeviceContactAvatar> createContactAvatarAttachment(RecipientAddress address) throws IOException {
final var streamDetails = context.getAvatarStore().retrieveContactAvatar(address);
if (streamDetails == null) {
return Optional.empty();
}
- return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.empty()));
+ return Optional.of(new DeviceContactAvatar(streamDetails.getStream(),
+ streamDetails.getLength(),
+ streamDetails.getContentType()));
}
- private void downloadContactAvatar(SignalServiceAttachment avatar, RecipientAddress address) {
+ private void storeContactAvatar(DeviceContactAvatar avatar, RecipientAddress address) {
try {
context.getAvatarStore()
.storeContactAvatar(address,
- outputStream -> context.getAttachmentHelper().retrieveAttachment(avatar, outputStream));
+ outputStream -> IOUtils.copyStream(avatar.getInputStream(), outputStream));
} catch (IOException e) {
logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage());
}
}
+
+ private MessageRequestResponseMessage.Type localToRemoteType(final MessageRequestResponse.Type type) {
+ return switch (type) {
+ case UNKNOWN -> MessageRequestResponseMessage.Type.UNKNOWN;
+ case ACCEPT -> MessageRequestResponseMessage.Type.ACCEPT;
+ case DELETE -> MessageRequestResponseMessage.Type.DELETE;
+ case BLOCK -> MessageRequestResponseMessage.Type.BLOCK;
+ case BLOCK_AND_DELETE -> MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
+ case UNBLOCK_AND_ACCEPT -> MessageRequestResponseMessage.Type.UNBLOCK_AND_ACCEPT;
+ case SPAM -> MessageRequestResponseMessage.Type.SPAM;
+ case BLOCK_AND_SPAM -> MessageRequestResponseMessage.Type.BLOCK_AND_SPAM;
+ };
+ }
}