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.RecipientAddress;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickers.StickerPack;
-import org.asamk.signal.manager.util.AttachmentUtils;
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.SyncMessage;
requestSyncData(SyncMessage.Request.Type.BLOCKED);
requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
requestSyncKeys();
- requestSyncPniIdentity();
}
public void requestSyncKeys() {
requestSyncData(SyncMessage.Request.Type.KEYS);
}
- public void requestSyncPniIdentity() {
- requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
- }
-
public SendMessageResult sendSyncFetchProfileMessage() {
return context.getSendHelper()
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
}
+ public void sendSyncFetchStorageMessage() {
+ context.getSendHelper()
+ .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 {
var groupsFile = IOUtils.createTempFile();
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(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().getIdentityInfo(address.getServiceId());
- VerifiedMessage verifiedMessage = null;
- if (currentIdentity != null) {
- verifiedMessage = new VerifiedMessage(address,
- currentIdentity.getIdentityKey(),
- currentIdentity.getTrustLevel().toVerifiedState(),
- currentIdentity.getDateAddedTimestamp());
- }
-
- var profileKey = account.getProfileStore().getProfileKey(recipientId);
- out.write(new DeviceContact(address,
- Optional.ofNullable(contact.getName()),
- createContactAvatarAttachment(new RecipientAddress(address)),
- Optional.ofNullable(contact.color()),
- Optional.ofNullable(verifiedMessage),
- Optional.ofNullable(profileKey),
- contact.isBlocked(),
- Optional.of(contact.messageExpirationTime()),
- 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(MimeUtils.OCTET_STREAM)
.withLength(contactsFile.length())
+ .withResumableUploadSpec(uploadSpec)
.build();
context.getSendHelper()
}
}
+ @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<SignalServiceAddress>();
+ 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[]>();
}
public SendMessageResult sendVerifiedMessage(
- SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
+ SignalServiceAddress destination,
+ IdentityKey identityKey,
+ TrustLevel trustLevel
) {
var verifiedMessage = new VerifiedMessage(destination,
identityKey,
}
public SendMessageResult sendKeysMessage() {
- var keysMessage = new KeysMessage(Optional.ofNullable(account.getStorageKey()),
- Optional.ofNullable(account.getOrCreatePinMasterKey()));
+ var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
+ account.getOrCreatePinMasterKey(),
+ account.getOrCreateAccountEntropyPool(),
+ account.getOrCreateMediaRootBackupKey());
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public SendMessageResult sendStickerOperationsMessage(
- List<StickerPack> installStickers, List<StickerPack> removeStickers
+ List<StickerPack> installStickers,
+ List<StickerPack> removeStickers
) {
var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
}
private static StickerPackOperationMessage getStickerPackOperationMessage(
- final StickerPack s, final boolean installed
+ final StickerPack s,
+ final boolean installed
) {
return new StickerPackOperationMessage(s.packId().serialize(),
s.packKey(),
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.getRecipientTrustedResolver().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()) {
+ if (c.getName().isPresent() && (
+ contact == null || (
+ contact.givenName() == null && contact.familyName() == null
+ )
+ )) {
builder.withGivenName(c.getName().get());
builder.withFamilyName(null);
}
- 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(verifiedMessage.getDestination().getServiceId(),
- verifiedMessage.getIdentityKey(),
- TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
- }
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);
}
}
}
+ 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));
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.createAttachmentStream(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;
+ };
+ }
}