mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-08 06:54:09 +01:00
Add contact and key sync message receive support.
This commit is contained in:
committed by
Greyson Parrelli
parent
c5028720e3
commit
c548816daa
@@ -207,7 +207,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
|
||||
if (avatar.hasRecipientId()) {
|
||||
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
|
||||
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
|
||||
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.getLength());
|
||||
} else {
|
||||
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
|
||||
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
|
||||
|
||||
@@ -65,10 +65,6 @@ public class ProfileContactPhoto implements ContactPhoto {
|
||||
}
|
||||
|
||||
private long getFileLastModified() {
|
||||
if (!recipient.isSelf()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1524,6 +1524,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||
}
|
||||
}
|
||||
|
||||
fun setSystemContactName(id: RecipientId, systemContactName: String) {
|
||||
val values = ContentValues().apply {
|
||||
put(SYSTEM_JOINED_NAME, systemContactName)
|
||||
}
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun setProfileName(id: RecipientId, profileName: ProfileName) {
|
||||
val contentValues = ContentValues(1).apply {
|
||||
put(PROFILE_GIVEN_NAME, profileName.givenName)
|
||||
|
||||
@@ -104,6 +104,7 @@ public final class JobManagerFactories {
|
||||
put(MmsSendJob.KEY, new MmsSendJob.Factory());
|
||||
put(MultiDeviceBlockedUpdateJob.KEY, new MultiDeviceBlockedUpdateJob.Factory());
|
||||
put(MultiDeviceConfigurationUpdateJob.KEY, new MultiDeviceConfigurationUpdateJob.Factory());
|
||||
put(MultiDeviceContactSyncJob.KEY, new MultiDeviceContactSyncJob.Factory());
|
||||
put(MultiDeviceContactUpdateJob.KEY, new MultiDeviceContactUpdateJob.Factory());
|
||||
put(MultiDeviceGroupUpdateJob.KEY, new MultiDeviceGroupUpdateJob.Factory());
|
||||
put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory());
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.libsignal.InvalidMessageException
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage.VerifiedState
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Sync contact data from primary device.
|
||||
*/
|
||||
class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPointer: ByteArray) : BaseJob(parameters) {
|
||||
|
||||
constructor(contactsAttachment: SignalServiceAttachmentPointer) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue("MultiDeviceContactSyncJob")
|
||||
.build(),
|
||||
AttachmentPointerUtil.createAttachmentPointer(contactsAttachment).toByteArray()
|
||||
)
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putBlobAsString(KEY_ATTACHMENT_POINTER, attachmentPointer)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
override fun onRun() {
|
||||
if (!Recipient.self().isRegistered) {
|
||||
throw NotPushRegisteredException()
|
||||
}
|
||||
|
||||
if (SignalStore.account().isPrimaryDevice) {
|
||||
Log.i(TAG, "Sync jobs are for linked devices only")
|
||||
return
|
||||
}
|
||||
|
||||
val contactAttachment: SignalServiceAttachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(attachmentPointer)
|
||||
|
||||
try {
|
||||
val contactsFile: File = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(context)
|
||||
ApplicationDependencies.getSignalServiceMessageReceiver()
|
||||
.retrieveAttachment(contactAttachment, contactsFile, MAX_ATTACHMENT_SIZE)
|
||||
.use(this::processContactFile)
|
||||
} catch (e: MissingConfigurationException) {
|
||||
throw IOException(e)
|
||||
} catch (e: InvalidMessageException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processContactFile(inputStream: InputStream) {
|
||||
val deviceContacts = DeviceContactsInputStream(inputStream)
|
||||
val recipients = SignalDatabase.recipients
|
||||
val threads = SignalDatabase.threads
|
||||
|
||||
var contact: DeviceContact? = deviceContacts.read()
|
||||
while (contact != null) {
|
||||
val recipient = Recipient.externalPush(context, contact.address.aci, contact.address.number.orNull(), true)
|
||||
|
||||
if (recipient.isSelf) {
|
||||
contact = deviceContacts.read()
|
||||
continue
|
||||
}
|
||||
|
||||
if (contact.name.isPresent) {
|
||||
recipients.setSystemContactName(recipient.id, contact.name.get())
|
||||
}
|
||||
|
||||
if (contact.expirationTimer.isPresent) {
|
||||
recipients.setExpireMessages(recipient.id, contact.expirationTimer.get())
|
||||
}
|
||||
|
||||
if (contact.profileKey.isPresent) {
|
||||
val profileKey = contact.profileKey.get()
|
||||
recipients.setProfileKey(recipient.id, profileKey)
|
||||
}
|
||||
|
||||
if (contact.verified.isPresent) {
|
||||
val verifiedStatus: VerifiedStatus = when (contact.verified.get().verified) {
|
||||
VerifiedState.VERIFIED -> VerifiedStatus.VERIFIED
|
||||
VerifiedState.UNVERIFIED -> VerifiedStatus.UNVERIFIED
|
||||
else -> VerifiedStatus.DEFAULT
|
||||
}
|
||||
|
||||
ApplicationDependencies.getIdentityStore().saveIdentityWithoutSideEffects(
|
||||
recipient.id,
|
||||
contact.verified.get().identityKey,
|
||||
verifiedStatus,
|
||||
false,
|
||||
contact.verified.get().timestamp,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
recipients.setBlocked(recipient.id, contact.isBlocked)
|
||||
|
||||
val threadRecord = threads.getThreadRecord(threads.getThreadIdFor(recipient.id))
|
||||
if (threadRecord != null && contact.isArchived != threadRecord.isArchived) {
|
||||
if (contact.isArchived) {
|
||||
threads.archiveConversation(threadRecord.threadId)
|
||||
} else {
|
||||
threads.unarchiveConversation(threadRecord.threadId)
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.avatar.isPresent) {
|
||||
try {
|
||||
AvatarHelper.setSyncAvatar(context, recipient.id, contact.avatar.get().inputStream)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Unable to set sync avatar for ${recipient.id}")
|
||||
}
|
||||
}
|
||||
|
||||
contact = deviceContacts.read()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean = false
|
||||
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<MultiDeviceContactSyncJob> {
|
||||
override fun create(parameters: Parameters, data: Data): MultiDeviceContactSyncJob {
|
||||
return MultiDeviceContactSyncJob(parameters, data.getStringAsBlob(KEY_ATTACHMENT_POINTER))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "MultiDeviceContactSyncJob"
|
||||
const val KEY_ATTACHMENT_POINTER = "attachment_pointer"
|
||||
private const val MAX_ATTACHMENT_SIZE: Long = 100 * 1024 * 1024
|
||||
private val TAG = Log.tag(MultiDeviceContactSyncJob::class.java)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
|
||||
@@ -14,6 +15,7 @@ public class StorageServiceValues extends SignalStoreValues {
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore";
|
||||
private static final String MANIFEST = "storage.manifest";
|
||||
private static final String SYNC_STORAGE_KEY = "storage.syncStorageKey";
|
||||
|
||||
StorageServiceValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
@@ -29,6 +31,9 @@ public class StorageServiceValues extends SignalStoreValues {
|
||||
}
|
||||
|
||||
public synchronized StorageKey getOrCreateStorageKey() {
|
||||
if (getStore().containsKey(SYNC_STORAGE_KEY)) {
|
||||
return new StorageKey(getBlob(SYNC_STORAGE_KEY, null));
|
||||
}
|
||||
return SignalStore.kbsValues().getOrCreateMasterKey().deriveStorageServiceKey();
|
||||
}
|
||||
|
||||
@@ -61,4 +66,14 @@ public class StorageServiceValues extends SignalStoreValues {
|
||||
return SignalStorageManifest.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void setStorageKeyFromPrimary(@NonNull StorageKey storageKey) {
|
||||
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Can only set storage key directly on linked devices");
|
||||
putBlob(SYNC_STORAGE_KEY, storageKey.serialize());
|
||||
}
|
||||
|
||||
public void clearStorageKeyFromPrimary() {
|
||||
Preconditions.checkState(SignalStore.account().isLinkedDevice(), "Can only clear storage key directly on linked devices");
|
||||
remove(SYNC_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.jobs.GroupCallPeekJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
||||
@@ -123,6 +124,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
@@ -139,6 +141,8 @@ import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
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.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
@@ -309,6 +313,8 @@ public final class MessageContentProcessor {
|
||||
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get(), content.getTimestamp());
|
||||
else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get(), content.getTimestamp());
|
||||
else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get());
|
||||
else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp());
|
||||
else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp());
|
||||
else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types...");
|
||||
} else if (content.getCallMessage().isPresent()) {
|
||||
log(String.valueOf(content.getTimestamp()), "Got call message...");
|
||||
@@ -1081,6 +1087,35 @@ public final class MessageContentProcessor {
|
||||
log("Inserted synchronized payment " + uuid);
|
||||
}
|
||||
|
||||
private void handleSynchronizeKeys(@NonNull KeysMessage keysMessage, long envelopeTimestamp) {
|
||||
if (SignalStore.account().isLinkedDevice()) {
|
||||
log(envelopeTimestamp, "Synchronize keys.");
|
||||
} else {
|
||||
log(envelopeTimestamp, "Primary device ignores synchronize keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalStore.storageService().setStorageKeyFromPrimary(keysMessage.getStorageService().get());
|
||||
}
|
||||
|
||||
private void handleSynchronizeContacts(@NonNull ContactsMessage contactsMessage, long envelopeTimestamp) throws IOException {
|
||||
if (SignalStore.account().isLinkedDevice()) {
|
||||
log(envelopeTimestamp, "Synchronize contacts.");
|
||||
} else {
|
||||
log(envelopeTimestamp, "Primary device ignores synchronize contacts.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(contactsMessage.getContactsStream() instanceof SignalServiceAttachmentPointer)) {
|
||||
warn(envelopeTimestamp, "No contact stream available.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceAttachmentPointer contactsAttachment = (SignalServiceAttachmentPointer) contactsMessage.getContactsStream();
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactSyncJob(contactsAttachment));
|
||||
}
|
||||
|
||||
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SentTranscriptMessage message,
|
||||
@NonNull Recipient senderRecipient)
|
||||
@@ -1171,7 +1206,12 @@ public final class MessageContentProcessor {
|
||||
|
||||
private void handleSynchronizeRequestMessage(@NonNull RequestMessage message, long envelopeTimestamp)
|
||||
{
|
||||
log(envelopeTimestamp, "Synchronize request message.");
|
||||
if (SignalStore.account().isPrimaryDevice()) {
|
||||
log(envelopeTimestamp, "Synchronize request message.");
|
||||
} else {
|
||||
log(envelopeTimestamp, "Linked device ignoring synchronize request message.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.isContactsRequest()) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
@@ -129,7 +130,24 @@ public class AvatarHelper {
|
||||
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = getOutputStream(context, recipientId);
|
||||
outputStream = getOutputStream(context, recipientId, false);
|
||||
StreamUtil.copy(inputStream, outputStream);
|
||||
} finally {
|
||||
StreamUtil.close(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setSyncAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
if (inputStream == null) {
|
||||
delete(context, recipientId);
|
||||
return;
|
||||
}
|
||||
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = getOutputStream(context, recipientId, true);
|
||||
StreamUtil.copy(inputStream, outputStream);
|
||||
} finally {
|
||||
StreamUtil.close(outputStream);
|
||||
@@ -140,9 +158,9 @@ public class AvatarHelper {
|
||||
* Retrieves an output stream you can write to that will be saved as the avatar for the specified
|
||||
* recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}.
|
||||
*/
|
||||
public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
|
||||
public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) throws IOException {
|
||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
File targetFile = getAvatarFile(context, recipientId);
|
||||
File targetFile = getAvatarFile(context, recipientId, isSyncAvatar);
|
||||
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second;
|
||||
}
|
||||
|
||||
@@ -179,8 +197,25 @@ public class AvatarHelper {
|
||||
}
|
||||
|
||||
private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) {
|
||||
File profileAvatar = getAvatarFile(context, recipientId, false);
|
||||
boolean profileAvatarExists = profileAvatar.exists() && profileAvatar.length() > 0;
|
||||
File syncAvatar = getAvatarFile(context, recipientId, true);
|
||||
boolean syncAvatarExists = syncAvatar.exists() && syncAvatar.length() > 0;
|
||||
|
||||
if (SignalStore.settings().isPreferSystemContactPhotos() && syncAvatarExists) {
|
||||
return syncAvatar;
|
||||
} else if (profileAvatarExists) {
|
||||
return profileAvatar;
|
||||
} else if (syncAvatarExists) {
|
||||
return syncAvatar;
|
||||
}
|
||||
|
||||
return profileAvatar;
|
||||
}
|
||||
|
||||
private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) {
|
||||
File directory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE);
|
||||
return new File(directory, recipientId.serialize());
|
||||
return new File(directory, recipientId.serialize() + (isSyncAvatar ? "-sync" : ""));
|
||||
}
|
||||
|
||||
public static class Avatar {
|
||||
|
||||
@@ -413,6 +413,19 @@ public class BlobProvider {
|
||||
return context.getDir(directory, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link File} within the appropriate directory to be cleaned up as part of
|
||||
* normal operations. Unlike other blobs, this is just a file reference and no
|
||||
* automatic encryption occurs when reading or writing and must be done by the caller.
|
||||
*
|
||||
* @return file located in the appropriate directory to be delete on app session restarts
|
||||
*/
|
||||
public File forNonAutoEncryptingSingleSessionOnDisk(@NonNull Context context) {
|
||||
String directory = getDirectory(StorageType.SINGLE_SESSION_DISK);
|
||||
String id = UUID.randomUUID().toString();
|
||||
return new File(getOrCreateDirectory(context, directory), buildFileName(id));
|
||||
}
|
||||
|
||||
public class BlobBuilder {
|
||||
|
||||
private InputStream data;
|
||||
|
||||
@@ -80,10 +80,6 @@ public final class ConversationShortcutPhoto implements Key {
|
||||
}
|
||||
|
||||
private long getFileLastModified() {
|
||||
if (!recipient.isSelf()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
|
||||
}
|
||||
|
||||
|
||||
+18
-70
@@ -55,7 +55,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
@@ -75,6 +74,7 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.services.AttachmentService;
|
||||
import org.whispersystems.signalservice.api.services.MessagingService;
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Uint64RangeException;
|
||||
import org.whispersystems.signalservice.api.util.Uint64Util;
|
||||
@@ -506,7 +506,7 @@ public class SignalServiceMessageSender {
|
||||
} else if (message.getConfiguration().isPresent()) {
|
||||
content = createMultiDeviceConfigurationContent(message.getConfiguration().get());
|
||||
} else if (message.getSent().isPresent()) {
|
||||
content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess);
|
||||
content = createMultiDeviceSentTranscriptContent(message.getSent().get(), unidentifiedAccess.isPresent());
|
||||
} else if (message.getStickerPackOperations().isPresent()) {
|
||||
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
|
||||
} else if (message.getFetchType().isPresent()) {
|
||||
@@ -518,7 +518,7 @@ public class SignalServiceMessageSender {
|
||||
} else if (message.getKeys().isPresent()) {
|
||||
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
|
||||
} else if (message.getVerified().isPresent()) {
|
||||
return sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess);
|
||||
return sendVerifiedSyncMessage(message.getVerified().get());
|
||||
} else if (message.getRequest().isPresent()) {
|
||||
content = createRequestContent(message.getRequest().get().getRequest());
|
||||
} else {
|
||||
@@ -640,7 +640,7 @@ public class SignalServiceMessageSender {
|
||||
attachment.getUploadTimestamp());
|
||||
}
|
||||
|
||||
private SendMessageResult sendVerifiedMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
|
||||
private SendMessageResult sendVerifiedSyncMessage(VerifiedMessage message)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
byte[] nullMessageBody = DataMessage.newBuilder()
|
||||
@@ -658,7 +658,7 @@ public class SignalServiceMessageSender {
|
||||
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
|
||||
|
||||
SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, false, null);
|
||||
SendMessageResult result = sendMessage(message.getDestination(), Optional.absent(), message.getTimestamp(), envelopeContent, false, null);
|
||||
|
||||
if (result.getSuccess().isNeedsSync()) {
|
||||
Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray());
|
||||
@@ -1026,10 +1026,10 @@ public class SignalServiceMessageSender {
|
||||
return container.setSyncMessage(builder).build();
|
||||
}
|
||||
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
|
||||
SignalServiceAddress address = transcript.getDestination().get();
|
||||
Content content = createMessageContent(transcript.getMessage());
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess.isPresent(), true, -1, Optional.of(content));
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.of(content));
|
||||
|
||||
return createMultiDeviceSentTranscriptContent(content,
|
||||
Optional.of(address),
|
||||
@@ -1910,63 +1910,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
AttachmentPointer.Builder builder = AttachmentPointer.newBuilder()
|
||||
.setCdnNumber(attachment.getCdnNumber())
|
||||
.setContentType(attachment.getContentType())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get())
|
||||
.setUploadTimestamp(attachment.getUploadTimestamp());
|
||||
|
||||
if (attachment.getRemoteId().getV2().isPresent()) {
|
||||
builder.setCdnId(attachment.getRemoteId().getV2().get());
|
||||
}
|
||||
|
||||
if (attachment.getRemoteId().getV3().isPresent()) {
|
||||
builder.setCdnKey(attachment.getRemoteId().getV3().get());
|
||||
}
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
|
||||
if (attachment.getPreview().isPresent()) {
|
||||
builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get()));
|
||||
}
|
||||
|
||||
if (attachment.getWidth() > 0) {
|
||||
builder.setWidth(attachment.getWidth());
|
||||
}
|
||||
|
||||
if (attachment.getHeight() > 0) {
|
||||
builder.setHeight(attachment.getHeight());
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
|
||||
if (attachment.getVoiceNote()) {
|
||||
flags |= FlagUtil.toBinaryFlag(AttachmentPointer.Flags.VOICE_MESSAGE_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.isBorderless()) {
|
||||
flags |= FlagUtil.toBinaryFlag(AttachmentPointer.Flags.BORDERLESS_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.isGif()) {
|
||||
flags |= FlagUtil.toBinaryFlag(AttachmentPointer.Flags.GIF_VALUE);
|
||||
}
|
||||
|
||||
builder.setFlags(flags);
|
||||
|
||||
if (attachment.getCaption().isPresent()) {
|
||||
builder.setCaption(attachment.getCaption().get());
|
||||
}
|
||||
|
||||
if (attachment.getBlurHash().isPresent()) {
|
||||
builder.setBlurHash(attachment.getBlurHash().get());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
return AttachmentPointerUtil.createAttachmentPointer(attachment);
|
||||
}
|
||||
|
||||
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
|
||||
@@ -1986,12 +1930,17 @@ public class SignalServiceMessageSender {
|
||||
{
|
||||
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||
|
||||
boolean isLocalPrimaryDevice = recipient.matches(localAddress) && localDeviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
if (!isLocalPrimaryDevice || unidentifiedAccess.isPresent()) {
|
||||
messages.add(getEncryptedMessage(socket, recipient, unidentifiedAccess, SignalServiceAddress.DEFAULT_DEVICE_ID, plaintext));
|
||||
List<Integer> subDevices = store.getSubDeviceSessions(recipient.getIdentifier());
|
||||
|
||||
List<Integer> deviceIds = new ArrayList<>(subDevices.size() + 1);
|
||||
deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
deviceIds.addAll(subDevices);
|
||||
|
||||
if (recipient.matches(localAddress)) {
|
||||
deviceIds.remove(Integer.valueOf(localDeviceId));
|
||||
}
|
||||
|
||||
for (int deviceId : store.getSubDeviceSessions(recipient.getIdentifier())) {
|
||||
for (int deviceId : deviceIds) {
|
||||
if (store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) {
|
||||
messages.add(getEncryptedMessage(socket, recipient, unidentifiedAccess, deviceId, plaintext));
|
||||
}
|
||||
@@ -2010,8 +1959,7 @@ public class SignalServiceMessageSender {
|
||||
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);
|
||||
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, localDeviceId, store, sessionLock, null);
|
||||
|
||||
boolean isLocalDevice = recipient.matches(localAddress) && deviceId == localDeviceId;
|
||||
if (!store.containsSession(signalProtocolAddress) && !isLocalDevice) {
|
||||
if (!store.containsSession(signalProtocolAddress)) {
|
||||
try {
|
||||
List<PreKeyBundle> preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId);
|
||||
|
||||
|
||||
+11
@@ -130,6 +130,17 @@ public class AttachmentCipherInputStream extends FilterInputStream {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] buffer = new byte[1];
|
||||
int read;
|
||||
|
||||
//noinspection StatementWithEmptyBody
|
||||
while ((read = read(buffer)) == 0);
|
||||
|
||||
return (read == -1) ? -1 : ((int) buffer[0]) & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer) throws IOException {
|
||||
return read(buffer, 0, buffer.length);
|
||||
|
||||
+15
-15
@@ -31,6 +31,8 @@ import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
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.KeysMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||
@@ -45,6 +47,8 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
@@ -813,6 +817,16 @@ public final class SignalServiceContent {
|
||||
}
|
||||
}
|
||||
|
||||
if (content.hasKeys() && content.getKeys().hasStorageService()) {
|
||||
byte[] storageKey = content.getKeys().getStorageService().toByteArray();
|
||||
|
||||
return SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.of(new StorageKey(storageKey))));
|
||||
}
|
||||
|
||||
if (content.hasContacts()) {
|
||||
return SignalServiceSyncMessage.forContacts(new ContactsMessage(createAttachmentPointer(content.getContacts().getBlob()), content.getContacts().getComplete()));
|
||||
}
|
||||
|
||||
return SignalServiceSyncMessage.empty();
|
||||
}
|
||||
|
||||
@@ -1162,21 +1176,7 @@ public final class SignalServiceContent {
|
||||
}
|
||||
|
||||
private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws InvalidMessageStructureException {
|
||||
return new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
|
||||
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
|
||||
pointer.getWidth(), pointer.getHeight(),
|
||||
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
|
||||
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE)) != 0,
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE)) != 0,
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.GIF_VALUE)) != 0,
|
||||
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
|
||||
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent(),
|
||||
pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0);
|
||||
return AttachmentPointerUtil.createSignalAttachmentPointer(pointer);
|
||||
|
||||
}
|
||||
|
||||
|
||||
+34
-41
@@ -12,63 +12,48 @@ public class ChunkedInputStream {
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
protected int readRawVarint32() throws IOException {
|
||||
byte tmp = (byte)in.read();
|
||||
if (tmp >= 0) {
|
||||
return tmp;
|
||||
}
|
||||
int result = tmp & 0x7f;
|
||||
if ((tmp = (byte)in.read()) >= 0) {
|
||||
result |= tmp << 7;
|
||||
} else {
|
||||
result |= (tmp & 0x7f) << 7;
|
||||
if ((tmp = (byte)in.read()) >= 0) {
|
||||
result |= tmp << 14;
|
||||
} else {
|
||||
result |= (tmp & 0x7f) << 14;
|
||||
if ((tmp = (byte)in.read()) >= 0) {
|
||||
result |= tmp << 21;
|
||||
} else {
|
||||
result |= (tmp & 0x7f) << 21;
|
||||
result |= (tmp = (byte)in.read()) << 28;
|
||||
if (tmp < 0) {
|
||||
// Discard upper 32 bits.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if ((byte)in.read() >= 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Malformed varint!");
|
||||
}
|
||||
}
|
||||
long readRawVarint32() throws IOException {
|
||||
long result = 0;
|
||||
for (int shift = 0; shift < 32; shift += 7) {
|
||||
int tmpInt = in.read();
|
||||
if (tmpInt < 0) {
|
||||
return -1;
|
||||
}
|
||||
final byte b = (byte) tmpInt;
|
||||
result |= (long) (b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
throw new IOException("Malformed varint!");
|
||||
}
|
||||
|
||||
protected static final class LimitedInputStream extends FilterInputStream {
|
||||
protected static final class LimitedInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
|
||||
private long left;
|
||||
private long mark = -1;
|
||||
|
||||
LimitedInputStream(InputStream in, long limit) {
|
||||
super(in);
|
||||
left = limit;
|
||||
this.in = in;
|
||||
this.left = limit;
|
||||
}
|
||||
|
||||
@Override public int available() throws IOException {
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return (int) Math.min(in.available(), left);
|
||||
}
|
||||
|
||||
// it's okay to mark even if mark isn't supported, as reset won't work
|
||||
@Override public synchronized void mark(int readLimit) {
|
||||
@Override
|
||||
public synchronized void mark(int readLimit) {
|
||||
in.mark(readLimit);
|
||||
mark = left;
|
||||
}
|
||||
|
||||
@Override public int read() throws IOException {
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (left == 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -80,7 +65,8 @@ public class ChunkedInputStream {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public int read(byte[] b, int off, int len) throws IOException {
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (left == 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -93,7 +79,8 @@ public class ChunkedInputStream {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public synchronized void reset() throws IOException {
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
if (!in.markSupported()) {
|
||||
throw new IOException("Mark not supported");
|
||||
}
|
||||
@@ -105,12 +92,18 @@ public class ChunkedInputStream {
|
||||
left = mark;
|
||||
}
|
||||
|
||||
@Override public long skip(long n) throws IOException {
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
n = Math.min(n, left);
|
||||
long skipped = in.skip(n);
|
||||
left -= skipped;
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -16,7 +16,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
@@ -32,8 +31,12 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
||||
}
|
||||
|
||||
public DeviceContact read() throws IOException {
|
||||
long detailsLength = readRawVarint32();
|
||||
byte[] detailsSerialized = new byte[(int)detailsLength];
|
||||
int detailsLength = (int) readRawVarint32();
|
||||
if (detailsLength == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] detailsSerialized = new byte[(int) detailsLength];
|
||||
Util.readFully(in, detailsSerialized);
|
||||
|
||||
SignalServiceProtos.ContactDetails details = SignalServiceProtos.ContactDetails.parseFrom(detailsSerialized);
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package org.whispersystems.signalservice.api.util;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.util.FlagUtil;
|
||||
|
||||
public final class AttachmentPointerUtil {
|
||||
public static SignalServiceAttachmentPointer createSignalAttachmentPointer(byte[] pointer) throws InvalidMessageStructureException, InvalidProtocolBufferException {
|
||||
return createSignalAttachmentPointer(SignalServiceProtos.AttachmentPointer.parseFrom(pointer));
|
||||
}
|
||||
|
||||
public static SignalServiceAttachmentPointer createSignalAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws InvalidMessageStructureException {
|
||||
return new SignalServiceAttachmentPointer(pointer.getCdnNumber(),
|
||||
SignalServiceAttachmentRemoteId.from(pointer),
|
||||
pointer.getContentType(),
|
||||
pointer.getKey().toByteArray(),
|
||||
pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.<Integer>absent(),
|
||||
pointer.hasThumbnail() ? Optional.of(pointer.getThumbnail().toByteArray()): Optional.<byte[]>absent(),
|
||||
pointer.getWidth(), pointer.getHeight(),
|
||||
pointer.hasDigest() ? Optional.of(pointer.getDigest().toByteArray()) : Optional.<byte[]>absent(),
|
||||
pointer.hasFileName() ? Optional.of(pointer.getFileName()) : Optional.<String>absent(),
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE)) != 0,
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE)) != 0,
|
||||
(pointer.getFlags() & FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.GIF_VALUE)) != 0,
|
||||
pointer.hasCaption() ? Optional.of(pointer.getCaption()) : Optional.<String>absent(),
|
||||
pointer.hasBlurHash() ? Optional.of(pointer.getBlurHash()) : Optional.<String>absent(),
|
||||
pointer.hasUploadTimestamp() ? pointer.getUploadTimestamp() : 0);
|
||||
|
||||
}
|
||||
|
||||
public static SignalServiceProtos.AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) {
|
||||
SignalServiceProtos.AttachmentPointer.Builder builder = SignalServiceProtos.AttachmentPointer.newBuilder()
|
||||
.setCdnNumber(attachment.getCdnNumber())
|
||||
.setContentType(attachment.getContentType())
|
||||
.setKey(ByteString.copyFrom(attachment.getKey()))
|
||||
.setDigest(ByteString.copyFrom(attachment.getDigest().get()))
|
||||
.setSize(attachment.getSize().get())
|
||||
.setUploadTimestamp(attachment.getUploadTimestamp());
|
||||
|
||||
if (attachment.getRemoteId().getV2().isPresent()) {
|
||||
builder.setCdnId(attachment.getRemoteId().getV2().get());
|
||||
}
|
||||
|
||||
if (attachment.getRemoteId().getV3().isPresent()) {
|
||||
builder.setCdnKey(attachment.getRemoteId().getV3().get());
|
||||
}
|
||||
|
||||
if (attachment.getFileName().isPresent()) {
|
||||
builder.setFileName(attachment.getFileName().get());
|
||||
}
|
||||
|
||||
if (attachment.getPreview().isPresent()) {
|
||||
builder.setThumbnail(ByteString.copyFrom(attachment.getPreview().get()));
|
||||
}
|
||||
|
||||
if (attachment.getWidth() > 0) {
|
||||
builder.setWidth(attachment.getWidth());
|
||||
}
|
||||
|
||||
if (attachment.getHeight() > 0) {
|
||||
builder.setHeight(attachment.getHeight());
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
|
||||
if (attachment.getVoiceNote()) {
|
||||
flags |= FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.isBorderless()) {
|
||||
flags |= FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.BORDERLESS_VALUE);
|
||||
}
|
||||
|
||||
if (attachment.isGif()) {
|
||||
flags |= FlagUtil.toBinaryFlag(SignalServiceProtos.AttachmentPointer.Flags.GIF_VALUE);
|
||||
}
|
||||
|
||||
builder.setFlags(flags);
|
||||
|
||||
if (attachment.getCaption().isPresent()) {
|
||||
builder.setCaption(attachment.getCaption().get());
|
||||
}
|
||||
|
||||
if (attachment.getBlurHash().isPresent()) {
|
||||
builder.setBlurHash(attachment.getBlurHash().get());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DeviceContactsInputStreamTest {
|
||||
|
||||
@Test
|
||||
public void read() throws IOException, InvalidInputException {
|
||||
ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
|
||||
DeviceContactsOutputStream output = new DeviceContactsOutputStream(byteArrayOut);
|
||||
SignalServiceAddress addressFirst = new SignalServiceAddress(ACI.from(UUID.randomUUID()), "+1404555555");
|
||||
SignalServiceAddress addressSecond = new SignalServiceAddress(ACI.from(UUID.randomUUID()), "+1444555555");
|
||||
|
||||
DeviceContact first = new DeviceContact(
|
||||
addressFirst,
|
||||
Optional.of("Teal'c"),
|
||||
Optional.absent(),
|
||||
Optional.of("ultramarine"),
|
||||
Optional.of(new VerifiedMessage(addressFirst, generateIdentityKey(), VerifiedMessage.VerifiedState.DEFAULT, System.currentTimeMillis())),
|
||||
Optional.of(generateProfileKey()),
|
||||
false,
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
false
|
||||
);
|
||||
|
||||
DeviceContact second = new DeviceContact(
|
||||
addressSecond,
|
||||
Optional.of("Bra'tac"),
|
||||
Optional.absent(),
|
||||
Optional.of("ultramarine"),
|
||||
Optional.of(new VerifiedMessage(addressSecond, generateIdentityKey(), VerifiedMessage.VerifiedState.DEFAULT, System.currentTimeMillis())),
|
||||
Optional.of(generateProfileKey()),
|
||||
false,
|
||||
Optional.of(0),
|
||||
Optional.of(0),
|
||||
false
|
||||
);
|
||||
|
||||
output.write(first);
|
||||
output.write(second);
|
||||
|
||||
output.close();
|
||||
|
||||
ByteArrayInputStream byteArrayIn = new ByteArrayInputStream(byteArrayOut.toByteArray());
|
||||
|
||||
DeviceContactsInputStream input = new DeviceContactsInputStream(byteArrayIn);
|
||||
DeviceContact readFirst = input.read();
|
||||
DeviceContact readSecond = input.read();
|
||||
|
||||
assertEquals(first.getAddress(), readFirst.getAddress());
|
||||
assertEquals(first.getName(), readFirst.getName());
|
||||
assertEquals(first.getColor(), readFirst.getColor());
|
||||
assertEquals(first.getVerified().get().getIdentityKey(), readFirst.getVerified().get().getIdentityKey());
|
||||
assertEquals(first.isArchived(), readFirst.isArchived());
|
||||
|
||||
assertEquals(second.getAddress(), readSecond.getAddress());
|
||||
assertEquals(second.getName(), readSecond.getName());
|
||||
assertEquals(second.getColor(), readSecond.getColor());
|
||||
assertEquals(second.getVerified().get().getIdentityKey(), readSecond.getVerified().get().getIdentityKey());
|
||||
assertEquals(second.isArchived(), readSecond.isArchived());
|
||||
}
|
||||
|
||||
private static IdentityKey generateIdentityKey() {
|
||||
ECKeyPair djbKeyPair = Curve.generateKeyPair();
|
||||
return new IdentityKey(djbKeyPair.getPublicKey());
|
||||
}
|
||||
|
||||
private static ProfileKey generateProfileKey() throws InvalidInputException {
|
||||
return new ProfileKey(Util.getSecretBytes(32));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user