diff --git a/app/build.gradle b/app/build.gradle index 593cc562e3..45d5a18a68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,8 +157,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 143 -def canonicalVersionName = "1.8.0" +def canonicalVersionCode = 147 +def canonicalVersionName = "1.9.0" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, @@ -235,7 +235,7 @@ android { buildTypes { release { - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard/proguard-dagger.pro', diff --git a/app/proguard/proguard.pro b/app/proguard/proguard.pro index 2634d86b94..863701970c 100644 --- a/app/proguard/proguard.pro +++ b/app/proguard/proguard.pro @@ -2,6 +2,7 @@ -keepattributes SourceFile,LineNumberTable -keep class org.whispersystems.** { *; } -keep class org.thoughtcrime.securesms.** { *; } +-keep class org.session.** { *; } -keepclassmembers class ** { public void onEvent*(**); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index f9a583fdea..f40b4e78aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -32,9 +32,10 @@ import androidx.multidex.MultiDexApplication; import org.conscrypt.Conscrypt; import org.session.libsession.messaging.MessagingConfiguration; import org.session.libsession.messaging.avatars.AvatarHelper; +import org.session.libsession.snode.SnodeConfiguration; +import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.threads.Address; -import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; @@ -54,7 +55,7 @@ import org.session.libsignal.service.loki.utilities.mentions.MentionsManager; import org.session.libsignal.utilities.logging.Log; import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.session.libsession.utilities.IdentityKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; @@ -175,6 +176,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc DatabaseFactory.getStorage(this), DatabaseFactory.getAttachmentProvider(this), new SessionProtocolImpl(this)); + SnodeConfiguration.Companion.configure(apiDB, broadcaster); if (userPublicKey != null) { SwarmAPI.Companion.configureIfNeeded(apiDB); SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster); @@ -441,6 +443,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this); FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB); + org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java index 988890ae05..f78c899974 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -37,7 +37,11 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.Loader; - +import org.session.libsession.messaging.messages.visible.LinkPreview; +import org.session.libsession.messaging.messages.visible.Quote; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus; import org.session.libsession.utilities.color.MaterialColor; import org.thoughtcrime.securesms.conversation.ConversationItem; @@ -50,12 +54,12 @@ import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.session.libsignal.utilities.logging.Log; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; -import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.DateUtils; import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.Util; @@ -441,7 +445,28 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity } private void onResendClicked(View v) { - MessageSender.resend(MessageDetailsActivity.this, messageRecord); + Recipient recipient = messageRecord.getRecipient(); + VisibleMessage message = new VisibleMessage(); + message.setId(messageRecord.getId()); + message.setText(messageRecord.getBody()); + message.setSentTimestamp(messageRecord.getTimestamp()); + if (recipient.isGroupRecipient()) { + message.setGroupPublicKey(recipient.getAddress().toGroupString()); + } else { + message.setRecipient(messageRecord.getRecipient().getAddress().serialize()); + } + message.setThreadID(messageRecord.getThreadId()); + if (messageRecord.isMms()) { + MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; + if (!mmsMessageRecord.getLinkPreviews().isEmpty()) { + message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0))); + } + if (mmsMessageRecord.getQuote() != null) { + message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel())); + } + message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments()); + } + MessageSender.send(message, recipient.getAddress()); resendButton.setVisibility(View.GONE); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java index 193ebb2fe5..9c873eb6db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ShareActivity.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; +import org.session.libsession.messaging.threads.DistributionTypes; import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.session.libsession.messaging.threads.Address; @@ -249,7 +250,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity public void onContactSelected(String number) { Recipient recipient = Recipient.from(this, Address.fromExternal(this, number), true); long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); - createConversation(existingThread, recipient.getAddress(), ThreadDatabase.DistributionTypes.DEFAULT); + createConversation(existingThread, recipient.getAddress(), DistributionTypes.DEFAULT); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index b1b1cdab4e..85c3acbe49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -1,22 +1,29 @@ package org.thoughtcrime.securesms.attachments import android.content.Context +import android.text.TextUtils import com.google.protobuf.ByteString import org.greenrobot.eventbus.EventBus import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsession.utilities.Util import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.logging.Log +import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.events.PartProgressEvent -import org.thoughtcrime.securesms.jobs.AttachmentUploadJob +import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.util.MediaUtil +import java.io.IOException import java.io.InputStream @@ -40,6 +47,14 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return databaseAttachment.toSignalAttachmentStream(context) } + override fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? { + val database = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = database.getAttachment(AttachmentId(attachmentId, 0)) ?: return null + val mediaConstraints = MediaConstraints.getPushMediaConstraints() + val scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment) ?: return null + return getAttachmentFor(scaledAttachment) + } + override fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null @@ -51,12 +66,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value) } - @Throws(Exception::class) - override fun uploadAttachment(attachmentId: Long) { - val attachmentUploadJob = AttachmentUploadJob(AttachmentId(attachmentId, 0), null) - attachmentUploadJob.onRun() - } - override fun getMessageForQuote(timestamp: Long, author: Address): Long? { val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context) return messagingDatabase.getMessageFor(timestamp, author)?.id @@ -72,6 +81,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return messagingDatabase.getMessage(messageID).body } + override fun getAttachmentIDsFor(messageID: Long): List { + return DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageID).mapNotNull { + if (it.isQuote) return@mapNotNull null + it.attachmentId.rowId + } + } + + override fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? { + val message = DatabaseFactory.getMmsDatabase(context).getOutgoingMessage(messageID) + return message.linkPreviews.firstOrNull()?.attachmentId?.rowId + } + override fun insertAttachment(messageId: Long, attachmentId: Long, stream: InputStream) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId, 0), stream) @@ -79,7 +100,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) override fun isOutgoingMessage(timestamp: Long): Boolean { val smsDatabase = DatabaseFactory.getSmsDatabase(context) - return smsDatabase.isOutgoingMessage(timestamp) + val mmsDatabase = DatabaseFactory.getMmsDatabase(context) + return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp) + } + + override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { + val database = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return + val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, + attachmentStream.contentType, + attachmentKey, + Optional.of(Util.toIntExact(attachmentStream.length)), + attachmentStream.preview, + attachmentStream.width, attachmentStream.height, + Optional.fromNullable(uploadResult.digest), + attachmentStream.fileName, + attachmentStream.voiceNote, + attachmentStream.caption, + uploadResult.url); + val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get() + database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) + } + + override fun updateAttachmentAfterUploadFailed(attachmentId: Long) { + val database = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return + database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId) } override fun getMessageID(serverID: Long): Long? { @@ -92,6 +138,52 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) messagingDatabase.deleteMessage(messageID) } + override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? { + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + return attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) + } + + private fun scaleAndStripExif(attachmentDatabase: AttachmentDatabase, constraints: MediaConstraints, attachment: Attachment): Attachment? { + return try { + if (constraints.isSatisfied(context, attachment)) { + if (MediaUtil.isJpeg(attachment)) { + val stripped = constraints.getResizedMedia(context, attachment) + attachmentDatabase.updateAttachmentData(attachment, stripped) + } else { + attachment + } + } else if (constraints.canResize(attachment)) { + val resized = constraints.getResizedMedia(context, attachment) + attachmentDatabase.updateAttachmentData(attachment, resized) + } else { + throw Exception("Size constraints could not be met!") + } + } catch (e: Exception) { + return null + } + } + + private fun getAttachmentFor(attachment: Attachment): SignalServiceAttachmentStream? { + try { + if (attachment.dataUri == null || attachment.size == 0L) throw IOException("Assertion failed, outgoing attachment has no data!") + val `is` = PartAuthority.getAttachmentStream(context, attachment.dataUri!!) + return SignalServiceAttachment.newStreamBuilder() + .withStream(`is`) + .withContentType(attachment.contentType) + .withLength(attachment.size) + .withFileName(attachment.fileName) + .withVoiceNote(attachment.isVoiceNote) + .withWidth(attachment.width) + .withHeight(attachment.height) + .withCaption(attachment.caption) + .withListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(attachment, total, progress)) } + .build() + } catch (ioe: IOException) { + Log.w("Loki", "Couldn't open attachment", ioe) + } + return null + } + } fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer { @@ -117,8 +209,28 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac return attachmentStream } -fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer { - return SignalServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url) +fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? { + if (TextUtils.isEmpty(location)) { return null } + if (TextUtils.isEmpty(key)) { return null } + + return try { + val id: Long = location!!.toLong() + val key: ByteArray = Base64.decode(key!!) + SignalServiceAttachmentPointer(id, + contentType, + key, + Optional.of(Util.toIntExact(size)), + Optional.absent(), + width, + height, + Optional.fromNullable(digest), + Optional.fromNullable(fileName), + isVoiceNote, + Optional.fromNullable(caption), + url) + } catch (e: Exception) { + null + } } fun DatabaseAttachment.toSignalAttachmentStream(context: Context): SignalServiceAttachmentStream { diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index a68cf58db9..46dd4367f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.attachments; import android.net.Uri; import androidx.annotation.Nullable; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; @@ -30,11 +31,11 @@ public class MmsNotificationAttachment extends Attachment { if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED || status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY) { - return AttachmentDatabase.TRANSFER_PROGRESS_PENDING; + return AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING; } else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) { - return AttachmentDatabase.TRANSFER_PROGRESS_STARTED; + return AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED; } else { - return AttachmentDatabase.TRANSFER_PROGRESS_FAILED; + return AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt index 1e2738db2c..9d1c012fb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.kt @@ -18,7 +18,6 @@ import org.session.libsession.utilities.Conversions import org.thoughtcrime.securesms.backup.BackupProtos.* import org.thoughtcrime.securesms.crypto.AttachmentSecret import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream import org.thoughtcrime.securesms.database.* import org.session.libsignal.utilities.logging.Log @@ -91,7 +90,7 @@ object FullBackupExporter { } } } - for (preference in IdentityKeyUtil.getBackupRecords(context)) { + for (preference in BackupUtil.getBackupRecords(context)) { EventBus.getDefault().post(BackupEvent.createProgress(++count)) outputStream.writePreferenceEntry(preference) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java b/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java index 3627572054..1301fa7286 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DocumentView.java @@ -20,6 +20,8 @@ import com.pnikosis.materialishprogress.ProgressWheel; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import network.loki.messenger.R; + +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.mms.DocumentSlide; @@ -94,7 +96,7 @@ public class DocumentView extends FrameLayout { controlToggle.displayQuick(downloadButton); downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide)); if (downloadProgress.isSpinning()) downloadProgress.stopSpinning(); - } else if (showControls && documentSlide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) { + } else if (showControls && documentSlide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) { controlToggle.displayQuick(downloadProgress); downloadProgress.spin(); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 2340df43c3..2628f031dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.UiThread; import android.util.AttributeSet; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsignal.utilities.logging.Log; import android.view.View; import android.view.ViewGroup; @@ -248,7 +249,7 @@ public class ThumbnailView extends FrameLayout { } if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() && - (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview)) + (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) { this.playOverlay.setVisibility(View.VISIBLE); } else { @@ -402,7 +403,7 @@ public class ThumbnailView extends FrameLayout { if (thumbnailClickListener != null && slide != null && slide.asAttachment().getDataUri() != null && - slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) + slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) { thumbnailClickListener.onClick(view, slide); } else if (parentClickListener != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java b/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java index 5828641dc0..36a607c819 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TransferControlView.java @@ -17,6 +17,7 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.mms.Slide; @@ -105,17 +106,17 @@ public class TransferControlView extends FrameLayout { } for (Slide slide : slides) { - if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { + if (slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) { downloadProgress.put(slide.asAttachment(), 1f); } } switch (getTransferState(slides)) { - case AttachmentDatabase.TRANSFER_PROGRESS_STARTED: + case AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED: showProgressSpinner(calculateProgress(downloadProgress)); break; - case AttachmentDatabase.TRANSFER_PROGRESS_PENDING: - case AttachmentDatabase.TRANSFER_PROGRESS_FAILED: + case AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING: + case AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED: downloadDetailsText.setText(getDownloadText(this.slides)); display(downloadDetails); break; @@ -174,9 +175,9 @@ public class TransferControlView extends FrameLayout { } private int getTransferState(@NonNull List slides) { - int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE; + int transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE; for (Slide slide : slides) { - if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { + if (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) { transferState = slide.getTransferState(); } else { transferState = Math.max(transferState, slide.getTransferState()); @@ -189,7 +190,7 @@ public class TransferControlView extends FrameLayout { if (slides.size() == 1) { return slides.get(0).getContentDescription(); } else { - int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE ? count + 1 : count); + int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count); return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java index 2b4b820ce8..fc6208dbd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/TypingStatusSender.java @@ -4,25 +4,21 @@ import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.ApplicationContext; -import org.session.libsession.messaging.threads.Address; +import org.session.libsession.messaging.messages.control.TypingIndicator; +import org.session.libsession.messaging.sending_receiving.MessageSender; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.utilities.Util; import java.util.HashMap; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; @SuppressLint("UseSparseArrays") public class TypingStatusSender { - private static final String TAG = TypingStatusSender.class.getSimpleName(); - private static final long REFRESH_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); private static final long PAUSE_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(3); @@ -82,12 +78,16 @@ public class TypingStatusSender { private void sendTyping(long threadId, boolean typingStarted) { ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); Recipient recipient = threadDatabase.getRecipientForThreadId(threadId); + if (recipient == null) { return; } // Loki - Check whether we want to send a typing indicator to this user if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; } - // Loki - Take into account multi device - if (recipient == null) { return; } - long threadID = threadDatabase.getOrCreateThreadIdFor(recipient); - ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadID, typingStarted)); + TypingIndicator typingIndicator; + if (typingStarted) { + typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED); + } else { + typingIndicator = new TypingIndicator(TypingIndicator.Kind.STOPPED); + } + MessageSender.send(typingIndicator, recipient.getAddress()); } private class StartRunnable implements Runnable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 58727424ba..ca14153ed3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -85,6 +85,10 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.MediaTypes; import org.session.libsignal.libsignal.InvalidMessageException; @@ -137,7 +141,7 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; @@ -153,9 +157,10 @@ import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; +import org.thoughtcrime.securesms.mms.MmsException; +import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteId; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -168,9 +173,8 @@ import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.RecipientFormattingException; import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.search.model.MessageResult; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; @@ -197,7 +201,6 @@ import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -571,16 +574,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity final Context context = ConversationActivity.this.getApplicationContext(); - sendMediaMessage(false, - message, + sendMediaMessage(message, slideDeck, inputPanel.getQuote().orNull(), - Collections.emptyList(), - Collections.emptyList(), - expiresIn, - subscriptionId, - initiating, - true).addListener(new AssertedSuccessListener() { + Optional.absent(), + initiating).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Void result) { AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { @@ -648,7 +646,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (!isPushGroupConversation()) { inflater.inflate(R.menu.conversation_mms_group_options, menu); - if (distributionType == ThreadDatabase.DistributionTypes.BROADCAST) { + if (distributionType == DistributionTypes.BROADCAST) { menu.findItem(R.id.menu_distribution_broadcast).setChecked(true); } else { menu.findItem(R.id.menu_distribution_conversation).setChecked(true); @@ -808,9 +806,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected Void doInBackground(Void... params) { DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); - OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L); - MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null); - + ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime); + message.setSentTimestamp(System.currentTimeMillis()); + OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient); + try { + message.setId(DatabaseFactory.getMmsDatabase(ConversationActivity.this).insertMessageOutbox(outgoingMessage, getAllocatedThreadId(ConversationActivity.this), false, null)); + MessageSender.send(message, recipient.getAddress()); + } catch (MmsException e) { + Log.w(TAG, e); + } return null; } @@ -839,17 +843,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity }); } - private void handleConversationSettings() { - /* - Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class); - intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress()); - intent.putExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA, - isSecureText && !isSelfConversation()); - - startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar"); - */ - } - private void handleUnmuteNotifications() { recipient.setMuted(0); @@ -1018,7 +1011,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } try { if (isClosedGroup) { - ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey); + MessageSender.explicitLeave(groupPublicKey); initializeEnabledCheck(); } else { Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); @@ -1040,7 +1033,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void handleDistributionBroadcastEnabled(MenuItem item) { - distributionType = ThreadDatabase.DistributionTypes.BROADCAST; + distributionType = DistributionTypes.BROADCAST; item.setChecked(true); if (threadId != -1) { @@ -1048,7 +1041,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) - .setDistributionType(threadId, ThreadDatabase.DistributionTypes.BROADCAST); + .setDistributionType(threadId, DistributionTypes.BROADCAST); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -1056,7 +1049,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void handleDistributionConversationEnabled(MenuItem item) { - distributionType = ThreadDatabase.DistributionTypes.CONVERSATION; + distributionType = DistributionTypes.CONVERSATION; item.setChecked(true); if (threadId != -1) { @@ -1064,7 +1057,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(ConversationActivity.this) - .setDistributionType(threadId, ThreadDatabase.DistributionTypes.CONVERSATION); + .setDistributionType(threadId, DistributionTypes.CONVERSATION); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -1335,7 +1328,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (address == null) { finish(); return; } recipient = Recipient.from(this, address, true); threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1); - distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); + distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT); glideRequests = GlideApp.with(this); recipient.addListener(this); @@ -1483,14 +1476,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity else if (contactData.numbers.size() > 1) selectContactInfo(contactData); } - private void sendSharedContact(List contacts) { - int subscriptionId = -1; - long expiresIn = recipient.getExpireMessages() * 1000L; - boolean initiating = threadId == -1; - - sendMediaMessage(false, "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, subscriptionId, initiating, false); - } - private void selectContactInfo(ContactData contactData) { final CharSequence[] numbers = new CharSequence[contactData.numbers.size()]; final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()]; @@ -1677,7 +1662,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); - if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { + if (!SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { for (MarkedMessageInfo messageInfo : messageIds) { MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo()); } @@ -1735,8 +1720,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } String message = getMessage(); - int subscriptionId = -1; - long expiresIn = recipient.getExpireMessages() * 1000L; boolean initiating = threadId == -1; boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize; boolean isMediaMessage = attachmentManager.isAttachmentPresent() || @@ -1747,9 +1730,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity needsSplit; if (isMediaMessage) { - sendMediaMessage(expiresIn, subscriptionId, initiating); + sendMediaMessage(initiating); } else { - sendTextMessage(expiresIn, subscriptionId, initiating); + sendTextMessage(initiating); } } catch (RecipientFormattingException ex) { Log.w(TAG, ex); @@ -1764,23 +1747,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } } - private void sendMediaMessage(final long expiresIn, final int subscriptionId, boolean initiating) + private void sendMediaMessage(boolean initiating) throws InvalidMessageException { Log.i(TAG, "Sending media message..."); - sendMediaMessage(false, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, subscriptionId, initiating, true); + sendMediaMessage(getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), linkPreviewViewModel.getActiveLinkPreview(), initiating); } - private ListenableFuture sendMediaMessage(final boolean forceSms, - String body, + private ListenableFuture sendMediaMessage(String body, SlideDeck slideDeck, QuoteModel quote, - List contacts, - List previews, - final long expiresIn, - final int subscriptionId, - final boolean initiating, - final boolean clearComposeBox) + Optional linkPreview, + final boolean initiating) { Pair> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize); @@ -1790,7 +1768,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity slideDeck.addSlide(splitMessage.second.get()); } - OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, quote, contacts, previews); + List attachments = slideDeck.asAttachments(); + + VisibleMessage message = new VisibleMessage(); + message.setSentTimestamp(System.currentTimeMillis()); + message.setText(body); + OutgoingMediaMessage outgoingMessageCandidate = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview.orNull()); final SettableFuture future = new SettableFuture<>(); final Context context = getApplicationContext(); @@ -1800,11 +1783,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate); ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); - if (clearComposeBox) { - inputPanel.clearQuote(); - attachmentManager.clear(glideRequests, false); - silentlySetComposeText(""); - } + inputPanel.clearQuote(); + attachmentManager.clear(glideRequests, false); + silentlySetComposeText(""); final long id = fragment.stageOutgoingMessage(outgoingMessage); @@ -1812,43 +1793,44 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); - - sendComplete(result); + try { + long allocatedThreadId = getAllocatedThreadId(context); + message.setId(DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMessage, allocatedThreadId, false, ()->fragment.releaseOutgoingMessage(id))); + MessageSender.send(message, recipient.getAddress(), attachments, quote, linkPreview.orNull()); + sendComplete(allocatedThreadId); + } catch (MmsException e) { + Log.w(TAG, e); + sendComplete(threadId); + } future.set(null); return future; } - private void sendTextMessage(final long expiresIn, final int subscriptionId, final boolean initiatingConversation) + private void sendTextMessage(final boolean initiating) throws InvalidMessageException { final Context context = getApplicationContext(); final String messageBody = getMessage(); - OutgoingTextMessage message; - - message = new OutgoingEncryptedMessage(recipient, messageBody, expiresIn); + VisibleMessage message = new VisibleMessage(); + message.setSentTimestamp(System.currentTimeMillis()); + message.setText(messageBody); + OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.from(message, recipient); ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId); silentlySetComposeText(""); - final long id = fragment.stageOutgoingMessage(message); + final long id = fragment.stageOutgoingMessage(outgoingTextMessage); - if (initiatingConversation) { + if (initiating) { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - long result = MessageSender.send(context, message, threadId, false, () -> fragment.releaseOutgoingMessage(id)); + long allocatedThreadId = getAllocatedThreadId(context); + message.setId(DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(allocatedThreadId, outgoingTextMessage, false, message.getSentTimestamp(), ()->fragment.releaseOutgoingMessage(id))); + MessageSender.send(message, recipient.getAddress()); - sendComplete(result); - } - - private void showDefaultSmsPrompt() { - new AlertDialog.Builder(this) - .setMessage(R.string.ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app) - .setNegativeButton(R.string.ConversationActivity_no, (dialog, which) -> dialog.dismiss()) - .setPositiveButton(R.string.ConversationActivity_yes, (dialog, which) -> handleMakeDefaultSms()) - .show(); + sendComplete(allocatedThreadId); } private void updateToggleButtonState() { @@ -1934,7 +1916,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity SlideDeck slideDeck = new SlideDeck(); slideDeck.addSlide(audioSlide); - sendMediaMessage(false, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating, true).addListener(new AssertedSuccessListener() { + sendMediaMessage("", slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Void nothing) { new AsyncTask() { @@ -2319,6 +2301,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } // region Loki + private long getAllocatedThreadId(Context context) { + long allocatedThreadId; + if (threadId == -1) { + allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); + } else { + allocatedThreadId = threadId; + } + return allocatedThreadId; + } + private void updateTitleTextView(Recipient recipient) { String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (recipient == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 773ea2d97e..0d30b8ce97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -57,6 +57,9 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Stream; +import org.session.libsession.messaging.messages.visible.Quote; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.MessageDetailsActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; @@ -77,22 +80,21 @@ import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.longmessage.LongMessageActivity; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.permissions.Permissions; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.session.libsession.utilities.task.ProgressDialogAsyncTask; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.loki.api.opengroups.PublicChat; -import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; -import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.ViewUtil; @@ -405,7 +407,7 @@ public class ConversationFragment extends Fragment menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser); menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - boolean userCanModerate = isPublicChat && PublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()); + boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()); boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate); // allow banning if moderating a public chat and only one user's messages are selected boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1; @@ -521,7 +523,6 @@ public class ConversationFragment extends Fragment ArrayList ignoredMessages = new ArrayList<>(); ArrayList failedMessages = new ArrayList<>(); boolean isSentByUser = true; - PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI(); for (MessageRecord messageRecord : messageRecords) { isSentByUser = isSentByUser && messageRecord.isOutgoing(); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id); @@ -531,8 +532,8 @@ public class ConversationFragment extends Fragment ignoredMessages.add(messageRecord.getId()); } } - if (publicChat != null && publicChatAPI != null) { - publicChatAPI + if (publicChat != null) { + OpenGroupAPI .deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser) .success(l -> { for (MessageRecord messageRecord : messageRecords) { @@ -602,9 +603,7 @@ public class ConversationFragment extends Fragment protected Void doInBackground(String... userPublicKeyParam) { String userPublicKey = userPublicKeyParam[0]; if (publicChat != null) { - PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI(); - if (publicChat != null && publicChatAPI != null) { - publicChatAPI + OpenGroupAPI .ban(userPublicKey, publicChat.getServer()) .success(l -> { Log.d("Loki", "User banned"); @@ -613,7 +612,6 @@ public class ConversationFragment extends Fragment Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + "."); return null; }); - } } else { Log.d("Loki", "Tried to ban user from a non-public chat"); } @@ -696,11 +694,32 @@ public class ConversationFragment extends Fragment } private void handleResendMessage(final MessageRecord message) { - final Context context = getActivity().getApplicationContext(); new AsyncTask() { @Override protected Void doInBackground(MessageRecord... messageRecords) { - MessageSender.resend(context, messageRecords[0]); + MessageRecord messageRecord = messageRecords[0]; + Recipient recipient = messageRecord.getRecipient(); + VisibleMessage message = new VisibleMessage(); + message.setId(messageRecord.getId()); + message.setText(messageRecord.getBody()); + message.setSentTimestamp(messageRecord.getTimestamp()); + if (recipient.isGroupRecipient()) { + message.setGroupPublicKey(recipient.getAddress().toGroupString()); + } else { + message.setRecipient(messageRecord.getRecipient().getAddress().serialize()); + } + message.setThreadID(messageRecord.getThreadId()); + if (messageRecord.isMms()) { + MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord; + if (!mmsMessageRecord.getLinkPreviews().isEmpty()) { + message.setLinkPreview(org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0))); + } + if (mmsMessageRecord.getQuote() != null) { + message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel())); + } + message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments()); + } + MessageSender.send(message, recipient.getAddress()); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 964e7834d6..650a708e53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -53,6 +53,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.loki.api.opengroups.PublicChat; import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; @@ -1003,10 +1004,10 @@ public class ConversationItem extends LinearLayout if (messageRecord.isMms()) { TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide(); - if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { + if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) { message = getResources().getString(R.string.ConversationItem_read_more); action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms()); - } else if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) { + } else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) { message = getResources().getString(R.string.ConversationItem_pending); action = () -> {}; } else if (slide != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java deleted file mode 100644 index 27505c08e0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.thoughtcrime.securesms.crypto; - - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import org.session.libsession.utilities.preferences.ProfileKeyUtil; -import org.session.libsignal.metadata.SignalProtos; -import org.session.libsignal.utilities.logging.Log; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.session.libsignal.service.api.push.SignalServiceAddress; - -public class UnidentifiedAccessUtil { - - private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName(); - - @WorkerThread - public static Optional getAccessFor(@NonNull Context context, - @NonNull Recipient recipient) - { - byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); - byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); - byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); - - if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = Util.getSecretBytes(16); - } - - Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + - " | Our access key present? " + (ourUnidentifiedAccessKey != null) + - " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null)); - - if (theirUnidentifiedAccessKey != null && - ourUnidentifiedAccessKey != null && - ourUnidentifiedAccessCertificate != null) - { - return Optional.of(new UnidentifiedAccess(theirUnidentifiedAccessKey)); - } - - return Optional.absent(); - } - - public static Optional getAccessForSync(@NonNull Context context) { - byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); - byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); - - if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = Util.getSecretBytes(16); - } - - if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { - return Optional.of(new UnidentifiedAccess(ourUnidentifiedAccessKey)); - } - - return Optional.absent(); - } - - public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) { - return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context)); - } - - private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { - byte[] theirProfileKey = recipient.resolve().getProfileKey(); - - if (theirProfileKey == null) return Util.getSecretBytes(16); - else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); - - } - - private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) { - String ourNumber = TextSecurePreferences.getLocalNumber(context); - if (ourNumber != null) { - SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder() - .setSender(ourNumber) - .setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID) - .build(); - return certificate.toByteArray(); - } - - return null; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java deleted file mode 100644 index 6752d6be6c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.thoughtcrime.securesms.crypto.storage; - -import android.content.Context; - -import org.session.libsignal.libsignal.IdentityKeyPair; -import org.session.libsignal.libsignal.state.IdentityKeyStore; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; - -public class SignalProtocolStoreImpl implements IdentityKeyStore { - - private final Context context; - - public SignalProtocolStoreImpl(Context context) { - this.context = context; - } - - @Override - public IdentityKeyPair getIdentityKeyPair() { - return IdentityKeyUtil.getIdentityKeyPair(context); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 7b67856c19..887ea913bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -38,6 +38,7 @@ import net.sqlcipher.database.SQLiteDatabase; import org.json.JSONArray; import org.json.JSONException; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.utilities.MediaTypes; import org.session.libsignal.utilities.logging.Log; @@ -119,12 +120,8 @@ public class AttachmentDatabase extends Database { static final String AUDIO_VISUAL_SAMPLES = "audio_visual_samples"; // Small amount of audio byte samples to visualise the content (e.g. draw waveform). static final String AUDIO_DURATION = "audio_duration"; // Duration of the audio track in milliseconds. - public static final int TRANSFER_PROGRESS_DONE = 0; - public static final int TRANSFER_PROGRESS_STARTED = 1; - public static final int TRANSFER_PROGRESS_PENDING = 2; - public static final int TRANSFER_PROGRESS_FAILED = 3; - private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; + private static final String ROW_ID_WHERE = ROW_ID + " = ?"; private static final String PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\""; private static final String[] PROJECTION = new String[] {ROW_ID, @@ -199,21 +196,12 @@ public class AttachmentDatabase extends Database { } } - public boolean containsStickerPackId(@NonNull String stickerPackId) { - String selection = STICKER_PACK_ID + " = ?"; - String[] args = new String[] { stickerPackId }; - - try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) throws MmsException { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED); + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId)); @@ -225,7 +213,7 @@ public class AttachmentDatabase extends Database { Cursor cursor = null; try { - cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); + cursor = database.query(TABLE_NAME, PROJECTION, ROW_ID_WHERE, new String[]{String.valueOf(attachmentId.getRowId())}, null, null, null); if (cursor != null && cursor.moveToFirst()) { List list = getAttachment(cursor); @@ -268,7 +256,7 @@ public class AttachmentDatabase extends Database { Cursor cursor = null; try { - cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); + cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED)}, null, null, null); while (cursor != null && cursor.moveToNext()) { attachments.addAll(getAttachment(cursor)); } @@ -372,7 +360,7 @@ public class AttachmentDatabase extends Database { values.put(DATA_RANDOM, dataInfo.random); } - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); values.put(CONTENT_LOCATION, (String)null); values.put(CONTENT_DISPOSITION, (String)null); values.put(DIGEST, (byte[])null); @@ -395,7 +383,7 @@ public class AttachmentDatabase extends Database { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); values.put(CONTENT_LOCATION, attachment.getLocation()); values.put(DIGEST, attachment.getDigest()); values.put(CONTENT_DISPOSITION, attachment.getKey()); @@ -411,7 +399,7 @@ public class AttachmentDatabase extends Database { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED); + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED); database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); } @@ -506,37 +494,17 @@ public class AttachmentDatabase extends Database { databaseAttachment.getUrl()); } - - public void updateAttachmentFileName(@NonNull AttachmentId attachmentId, - @Nullable String fileName) - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - ContentValues contentValues = new ContentValues(1); - contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(fileName)); - - database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings()); - } - public void markAttachmentUploaded(long messageId, Attachment attachment) { ContentValues values = new ContentValues(1); SQLiteDatabase database = databaseHelper.getWritableDatabase(); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE); database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); ((DatabaseAttachment) attachment).setUploaded(true); } - public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) { - if (!(attachment instanceof DatabaseAttachment)) { - throw new AssertionError("Attempt to update attachment that doesn't belong to DB!"); - } - - setTransferState(messageId, ((DatabaseAttachment) attachment).getAttachmentId(), transferState); - } - public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) { final ContentValues values = new ContentValues(1); final SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -546,14 +514,6 @@ public class AttachmentDatabase extends Database { notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); } - public boolean hasStickerAttachments() { - String selection = STICKER_PACK_ID + " NOT NULL"; - - try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, null, null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - @SuppressWarnings("WeakerAccess") @VisibleForTesting protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ContentValuesBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/database/ContentValuesBuilder.java deleted file mode 100644 index 87c161fea3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ContentValuesBuilder.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; - -import com.google.android.mms.pdu_alt.EncodedStringValue; - -import org.session.libsession.utilities.Util; - -public class ContentValuesBuilder { - - private final ContentValues contentValues; - - public ContentValuesBuilder(ContentValues contentValues) { - this.contentValues = contentValues; - } - - public void add(String key, String charsetKey, EncodedStringValue value) { - if (value != null) { - contentValues.put(key, Util.toIsoString(value.getTextString())); - contentValues.put(charsetKey, value.getCharacterSet()); - } - } - - public void add(String contentKey, byte[] value) { - if (value != null) { - contentValues.put(contentKey, Util.toIsoString(value)); - } - } - - public void add(String contentKey, int b) { - if (b != 0) - contentValues.put(contentKey, b); - } - - public void add(String contentKey, long value) { - if (value != -1L) - contentValues.put(contentKey, value); - } - - public ContentValues getContentValues() { - return contentValues; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index 72d4ef6df0..5c92b976f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -7,9 +7,9 @@ import android.text.TextUtils; import net.sqlcipher.database.SQLiteDatabase; -import org.thoughtcrime.securesms.database.documents.Document; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; +import org.session.libsession.database.documents.Document; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.libsignal.IdentityKey; @@ -38,27 +38,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn public abstract void markAsSent(long messageId, boolean secure); public abstract void markUnidentified(long messageId, boolean unidentified); - public void setMismatchedIdentity(long messageId, final Address address, final IdentityKey identityKey) { - List items = new ArrayList() {{ - add(new IdentityKeyMismatch(address, identityKey)); - }}; - - IdentityKeyMismatchList document = new IdentityKeyMismatchList(items); - - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - database.beginTransaction(); - - try { - setDocument(database, messageId, MISMATCHED_IDENTITIES, document); - - database.setTransactionSuccessful(); - } catch (IOException ioe) { - Log.w(TAG, ioe); - } finally { - database.endTransaction(); - } - } - public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) { try { addToDocument(messageId, MISMATCHED_IDENTITIES, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 3d2300bfb2..6e8b85a2d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -19,9 +19,7 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.text.TextUtils; -import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -38,22 +36,22 @@ import org.json.JSONException; import org.json.JSONObject; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; -import org.thoughtcrime.securesms.database.documents.NetworkFailureList; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.IdentityKeyMismatchList; +import org.session.libsession.database.documents.NetworkFailure; +import org.session.libsession.database.documents.NetworkFailureList; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.jobs.TrimThreadJob; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; -import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage; +import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 3b222404e0..d4861570f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -86,15 +86,6 @@ public class MmsSmsDatabase extends Database { } } - public @Nullable MessageRecord getMessageFor(long messageId) { - MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); - - try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.ID + " = " + messageId, null, null)) { - MmsSmsDatabase.Reader reader = db.readerFor(cursor); - return reader.getNext(); - } - } - public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 20ebbac6b7..300a5c55f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -23,8 +23,6 @@ import android.database.Cursor; import android.text.TextUtils; import android.util.Pair; -import androidx.annotation.NonNull; - import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; @@ -32,15 +30,15 @@ import net.sqlcipher.database.SQLiteStatement; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.jobs.TrimThreadJob; -import org.thoughtcrime.securesms.sms.IncomingGroupMessage; -import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.messages.signal.IncomingGroupMessage; +import org.session.libsession.messaging.messages.signal.IncomingTextMessage; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.recipients.Recipient; @@ -51,7 +49,6 @@ import org.session.libsignal.libsignal.util.guava.Optional; import java.io.IOException; import java.security.SecureRandom; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 3a5fe5147f..3cb1887381 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -13,7 +13,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment -import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachment import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.threads.Address @@ -30,20 +29,22 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.loki.api.opengroups.PublicChat import org.session.libsignal.utilities.logging.Log -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.get import org.thoughtcrime.securesms.loki.utilities.getString -import org.thoughtcrime.securesms.mms.IncomingMediaMessage -import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.thoughtcrime.securesms.mms.PartAuthority -import org.thoughtcrime.securesms.sms.IncomingGroupMessage -import org.thoughtcrime.securesms.sms.IncomingTextMessage -import org.thoughtcrime.securesms.sms.OutgoingTextMessage +import org.session.libsession.messaging.messages.signal.IncomingGroupMessage +import org.session.libsession.messaging.messages.signal.IncomingTextMessage +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.utilities.preferences.ProfileKeyUtil +import org.session.libsignal.service.loki.utilities.prettifiedDescription class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { override fun getUserPublicKey(): String? { @@ -65,8 +66,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun getUserProfileKey(): ByteArray? { - val profileKey = TextSecurePreferences.getProfileKey(context) ?: return null - return profileKey.toByteArray() + return ProfileKeyUtil.getProfileKey(context) } override fun getUserProfilePictureURL(): String? { @@ -81,7 +81,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun getOrGenerateRegistrationID(): Int { var registrationID = TextSecurePreferences.getLocalRegistrationId(context) - if (registrationID == null) { + if (registrationID == 0) { registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(context, registrationID) } @@ -90,7 +90,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun persistAttachments(messageId: Long, attachments: List): List { val database = DatabaseFactory.getAttachmentDatabase(context) - val databaseAttachments = attachments.map { it.toDatabaseAttachment() } + val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() } return database.insertAttachments(messageId, databaseAttachments) } @@ -125,7 +125,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, }.mapNotNull { PointerAttachment.forPointer(Optional.of(it)).orNull() } - val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull()) + val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull().firstOrNull()) mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) } else { // It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment @@ -331,9 +331,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID) } - override fun markAsSent(messageID: Long) { + override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? { + return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey) + } + + override fun markAsSent(timestamp: Long, author: String) { val database = DatabaseFactory.getMmsSmsDatabase(context) - val messageRecord = database.getMessageFor(messageID)!! + val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseFactory.getMmsDatabase(context) mmsDatabase.markAsSent(messageRecord.getId(), true) @@ -343,9 +347,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } - override fun markUnidentified(messageID: Long) { + override fun markUnidentified(timestamp: Long, author: String) { val database = DatabaseFactory.getMmsSmsDatabase(context) - val messageRecord = database.getMessageFor(messageID)!! + val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseFactory.getMmsDatabase(context) mmsDatabase.markUnidentified(messageRecord.getId(), true) @@ -355,15 +359,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } - override fun setErrorMessage(messageID: Long, error: Exception) { + override fun setErrorMessage(timestamp: Long, author: String, error: Exception) { val database = DatabaseFactory.getMmsSmsDatabase(context) - val messageRecord = database.getMessageFor(messageID) ?: return + val messageRecord = database.getMessageFor(timestamp, author) ?: return if (messageRecord.isMms) { val mmsDatabase = DatabaseFactory.getMmsDatabase(context) - mmsDatabase.markAsSentFailed(messageID) + mmsDatabase.markAsSentFailed(messageRecord.getId()) } else { val smsDatabase = DatabaseFactory.getSmsDatabase(context) - smsDatabase.markAsSentFailed(messageID) + smsDatabase.markAsSentFailed(messageRecord.getId()) + } + if (error.localizedMessage != null) { + DatabaseFactory.getLokiMessageDatabase(context).setErrorMessage(messageRecord.getId(), error.localizedMessage!!) + } else { + DatabaseFactory.getLokiMessageDatabase(context).setErrorMessage(messageRecord.getId(), error.javaClass.simpleName) } } @@ -388,7 +397,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) } - override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection) { + override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) { val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) .setType(type0) @@ -396,13 +405,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .addAllMembers(members) .addAllAdmins(admins) val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) - val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true) + val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") val smsDB = DatabaseFactory.getSmsDatabase(context) smsDB.insertMessageInbox(infoMessage) } - override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long) { + override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { + val userPublicKey = getUserPublicKey() val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) @@ -410,8 +420,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) + val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) + if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 7162c4caf9..258070f4b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -30,6 +30,7 @@ import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.GroupRecord; @@ -598,14 +599,6 @@ public class ThreadDatabase extends Database { return new Reader(cursor); } - public static class DistributionTypes { - public static final int DEFAULT = 2; - public static final int BROADCAST = 1; - public static final int CONVERSATION = 2; - public static final int ARCHIVE = 3; - public static final int INBOX_ZERO = 4; - } - public class Reader implements Closeable { private final Cursor cursor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java b/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java deleted file mode 100644 index a206825a01..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/XmlBackup.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.text.TextUtils; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.BufferedWriter; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class XmlBackup { - - private static final String PROTOCOL = "protocol"; - private static final String ADDRESS = "address"; - private static final String CONTACT_NAME = "contact_name"; - private static final String DATE = "date"; - private static final String READABLE_DATE = "readable_date"; - private static final String TYPE = "type"; - private static final String SUBJECT = "subject"; - private static final String BODY = "body"; - private static final String SERVICE_CENTER = "service_center"; - private static final String READ = "read"; - private static final String STATUS = "status"; - private static final String TOA = "toa"; - private static final String SC_TOA = "sc_toa"; - private static final String LOCKED = "locked"; - - private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); - - private final XmlPullParser parser; - - public XmlBackup(String path) throws XmlPullParserException, FileNotFoundException { - this.parser = XmlPullParserFactory.newInstance().newPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); - parser.setInput(new FileInputStream(path), null); - } - - public XmlBackupItem getNext() throws IOException, XmlPullParserException { - while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.getEventType() != XmlPullParser.START_TAG) { - continue; - } - - String name = parser.getName(); - - if (!name.equalsIgnoreCase("sms")) { - continue; - } - - int attributeCount = parser.getAttributeCount(); - - if (attributeCount <= 0) { - continue; - } - - XmlBackupItem item = new XmlBackupItem(); - - for (int i=0;i"; - private static final String CREATED_BY = ""; - private static final String OPEN_TAG_SMSES = ""; - private static final String CLOSE_TAG_SMSES = ""; - private static final String OPEN_TAG_SMS = " void appendAttribute(StringBuilder stringBuilder, String name, T value) { - stringBuilder.append(name).append(OPEN_ATTRIBUTE).append(value).append(CLOSE_ATTRIBUTE); - } - - public void close() throws IOException { - bufferedWriter.newLine(); - bufferedWriter.write(CLOSE_TAG_SMSES); - bufferedWriter.close(); - } - - private String escapeXML(String s) { - if (TextUtils.isEmpty(s)) return s; - - Matcher matcher = PATTERN.matcher( s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'")); - StringBuffer st = new StringBuffer(); - - while (matcher.find()) { - String escaped=""; - for (char ch: matcher.group(0).toCharArray()) { - escaped += ("&#" + ((int) ch) + ";"); - } - matcher.appendReplacement(st, escaped); - } - matcher.appendTail(st); - return st.toString(); - } - - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 8a629b4e74..88d8c679f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; +import org.thoughtcrime.securesms.loki.database.SessionJobDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -52,9 +53,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV19 = 40; private static final int lokiV20 = 41; private static final int lokiV21 = 42; + private static final int lokiV22 = 43; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV21; + private static final int DATABASE_VERSION = lokiV22; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -121,6 +123,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand()); + db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -255,6 +258,20 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { "ResetThreadSessionJob"); } + if (oldVersion < lokiV22) { + db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand()); + deleteJobRecords(db, + "PushGroupSendJob", + "PushMediaSendJob", + "PushTextSendJob", + "SendReadReceiptJob", + "TypingSendJob", + "AttachmentUploadJob", + "RequestGroupInfoJob", + "ClosedGroupUpdateMessageSendJobV2", + "SendDeliveryReceiptJob"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java index f8e3fe882e..0f17ddb39c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/PagingMediaLoader.java @@ -45,6 +45,7 @@ public class PagingMediaLoader extends AsyncLoader> { return new Pair<>(cursor, leftIsRecent ? cursor.getPosition() : cursor.getCount() - 1 - cursor.getPosition()); } } + cursor.close(); return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index dc86623cf1..7088818889 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -26,8 +26,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase.Status; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.NetworkFailure; import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.messaging.threads.recipients.Recipient; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index a0a8bb3fe6..b41e4ae78e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -26,8 +26,8 @@ import android.text.style.StyleSpan; import network.loki.messenger.R; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.NetworkFailure; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.utilities.ExpirationUtil; @@ -158,18 +158,10 @@ public abstract class MessageRecord extends DisplayRecord { return SmsDatabase.Types.isIdentityDefault(type); } - public boolean isIdentityMismatchFailure() { - return mismatches != null && !mismatches.isEmpty(); - } - public boolean isBundleKeyExchange() { return SmsDatabase.Types.isBundleKeyExchange(type); } - public boolean isContentBundleKeyExchange() { - return SmsDatabase.Types.isContentBundleKeyExchange(type); - } - public boolean isIdentityUpdate() { return SmsDatabase.Types.isIdentityUpdate(type); } @@ -195,10 +187,6 @@ public abstract class MessageRecord extends DisplayRecord { return individualRecipient; } - public int getRecipientDeviceId() { - return recipientDeviceId; - } - public long getType() { return type; } @@ -211,10 +199,6 @@ public abstract class MessageRecord extends DisplayRecord { return networkFailures; } - public boolean hasNetworkFailures() { - return networkFailures != null && !networkFailures.isEmpty(); - } - protected SpannableString emphasisAdded(String sequence) { SpannableString spannable = new SpannableString(sequence); spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -234,10 +218,6 @@ public abstract class MessageRecord extends DisplayRecord { return (int)getId(); } - public int getSubscriptionId() { - return subscriptionId; - } - public long getExpiresIn() { return expiresIn; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 0ad53ab397..6b88fdfbd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -8,8 +8,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.NetworkFailure; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 370d2adb84..523aa0ad80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -23,8 +23,8 @@ import android.text.SpannableString; import network.loki.messenger.R; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase.Status; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.NetworkFailure; import org.thoughtcrime.securesms.mms.SlideDeck; import org.session.libsession.messaging.threads.recipients.Recipient; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java index fe01e2da41..eb2ec381e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java @@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -42,4 +43,8 @@ public class Quote { public @NonNull SlideDeck getAttachment() { return attachment; } + + public QuoteModel getQuoteModel() { + return new QuoteModel(id, author, text, missing, attachment.asAttachments()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index bd03f0eb0e..9faabc77c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -24,7 +24,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.messaging.threads.recipients.Recipient; import java.util.LinkedList; diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index ec48ac6594..f9d7fa6834 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -3,42 +3,20 @@ package org.thoughtcrime.securesms.dependencies; import android.content.Context; import org.session.libsignal.service.api.SignalServiceMessageReceiver; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; -import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; -import org.thoughtcrime.securesms.jobs.AttachmentUploadJob; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.PushDecryptJob; -import org.thoughtcrime.securesms.jobs.PushGroupSendJob; -import org.thoughtcrime.securesms.jobs.PushMediaSendJob; -import org.thoughtcrime.securesms.jobs.PushTextSendJob; -import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; -import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; -import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; -import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; -import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; -import org.session.libsession.utilities.TextSecurePreferences; import dagger.Module; import dagger.Provides; -@Module(complete = false, injects = {PushGroupSendJob.class, - PushTextSendJob.class, - PushMediaSendJob.class, - AttachmentDownloadJob.class, - RequestGroupInfoJob.class, +@Module(complete = false, injects = {AttachmentDownloadJob.class, AvatarDownloadJob.class, RetrieveProfileAvatarJob.class, - SendReadReceiptJob.class, AppProtectionPreferenceFragment.class, - SendDeliveryReceiptJob.class, - TypingSendJob.class, - AttachmentUploadJob.class, PushDecryptJob.class, LinkPreviewRepository.class}) @@ -46,30 +24,12 @@ public class SignalCommunicationModule { private final Context context; - private SignalServiceMessageSender messageSender; private SignalServiceMessageReceiver messageReceiver; public SignalCommunicationModule(Context context) { this.context = context; } - @Provides - public synchronized SignalServiceMessageSender provideSignalMessageSender() { - if (this.messageSender == null) { - this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context), - TextSecurePreferences.getLocalNumber(context), - DatabaseFactory.getLokiAPIDatabase(context), - DatabaseFactory.getLokiThreadDatabase(context), - DatabaseFactory.getLokiMessageDatabase(context), - new SessionProtocolImpl(context), - DatabaseFactory.getLokiUserDatabase(context), - DatabaseFactory.getGroupDatabase(context), - ((ApplicationContext)context.getApplicationContext()).broadcaster); - } - - return this.messageSender; - } - @Provides synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() { if (this.messageReceiver == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index f33ffa29e9..69988b6568 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -2,36 +2,23 @@ package org.thoughtcrime.securesms.groups; import android.content.Context; import android.graphics.Bitmap; -import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.protobuf.ByteString; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; import org.session.libsession.messaging.threads.Address; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.utilities.GroupUtil; -import org.session.libsession.utilities.MediaTypes; import org.session.libsession.utilities.TextSecurePreferences; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; -import org.thoughtcrime.securesms.providers.BlobProvider; -import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.BitmapUtil; -import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; -import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; -import java.util.List; import java.util.Objects; import java.util.Set; @@ -72,7 +59,7 @@ public class GroupManager { groupDatabase.updateProfilePicture(groupId, avatarBytes); long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor( - groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION); + groupRecipient, DistributionTypes.CONVERSATION); return new GroupActionResult(groupRecipient, threadID); } @@ -95,82 +82,6 @@ public class GroupManager { return groupDatabase.delete(groupId); } - public static GroupActionResult updateGroup(@NonNull Context context, - @NonNull String groupId, - @NonNull Set members, - @Nullable Bitmap avatar, - @Nullable String name, - @NonNull Set admins) - { - final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - final Set
memberAddresses = getMemberAddresses(members); - final Set
adminAddresses = getMemberAddresses(admins); - final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); - - memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context))); - groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); - groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses)); - groupDatabase.updateTitle(groupId, name); - groupDatabase.updateProfilePicture(groupId, avatarBytes); - - if (!GroupUtil.isMmsGroup(groupId)) { - return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses); - } else { - Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true); - long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient); - return new GroupActionResult(groupRecipient, threadId); - } - } - - private static GroupActionResult sendGroupUpdate(@NonNull Context context, - @NonNull String groupId, - @NonNull Set
members, - @Nullable String groupName, - @Nullable byte[] avatar, - @NonNull Set
admins) - { - Attachment avatarAttachment = null; - Address groupAddress = Address.fromSerialized(groupId); - Recipient groupRecipient = Recipient.from(context, groupAddress, false); - - List numbers = new LinkedList<>(); - for (Address member : members) { - numbers.add(member.serialize()); - } - - List adminNumbers = new LinkedList<>(); - for (Address admin : admins) { - adminNumbers.add(admin.serialize()); - } - - GroupContext.Builder groupContextBuilder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId))) - .setType(GroupContext.Type.UPDATE) - .addAllMembers(numbers) - .addAllAdmins(adminNumbers); - if (groupName != null) groupContextBuilder.setName(groupName); - GroupContext groupContext = groupContextBuilder.build(); - - if (avatar != null) { - Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory(); - avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null); - } - - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList()); - long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); - - return new GroupActionResult(groupRecipient, threadId); - } - - private static Set
getMemberAddresses(Collection recipients) { - final Set
results = new HashSet<>(); - for (Recipient recipient : recipients) { - results.add(recipient.getAddress()); - } - - return results; - } - public static class GroupActionResult { private Recipient groupRecipient; private long threadId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 794926a15a..82b452d85f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting; import org.greenrobot.eventbus.EventBus; import org.session.libsession.messaging.jobs.Data; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream; @@ -96,11 +97,11 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType { final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); final DatabaseAttachment attachment = database.getAttachment(attachmentId); - final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE; + final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE; if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) { Log.i(TAG, "onAdded() Marking attachment progress as 'started'"); - database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED); } } @@ -129,12 +130,12 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType { if (!manual && !AttachmentUtil.isAutoDownloadPermitted(context, attachment)) { Log.w(TAG, "Attachment can't be auto downloaded..."); - database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_PENDING); + database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING); return; } Log.i(TAG, "Downloading push part " + attachmentId); - database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED); retrieveAttachment(messageId, attachmentId, attachment); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java deleted file mode 100644 index 76dc26572b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.greenrobot.eventbus.EventBus; -import org.session.libsession.messaging.jobs.Data; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment; -import org.session.libsession.messaging.threads.Address; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.events.PartProgressEvent; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.mms.MediaStream; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.transport.UndeliverableMessageException; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SignalServiceAttachment; -import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.loki.api.utilities.HTTP; - -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class AttachmentUploadJob extends BaseJob implements InjectableType { - - public static final String KEY = "AttachmentUploadJob"; - - private static final String TAG = AttachmentUploadJob.class.getSimpleName(); - - private static final String KEY_ROW_ID = "row_id"; - private static final String KEY_UNIQUE_ID = "unique_id"; - private static final String KEY_DESTINATION = "destination"; - - private AttachmentId attachmentId; - private Address destination; - @Inject SignalServiceMessageSender messageSender; - - public AttachmentUploadJob(AttachmentId attachmentId, Address destination) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(10) - .build(), - attachmentId, destination); - } - - private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId, Address destination) { - super(parameters); - this.attachmentId = attachmentId; - this.destination = destination; - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId()) - .putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId()) - .putString(KEY_DESTINATION, destination.serialize()) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws Exception { - AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); - DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId); - - if (databaseAttachment == null) { - throw new IllegalStateException("Cannot find the specified attachment."); - } - - // Only upload attachment if necessary - if (databaseAttachment.getUrl().isEmpty()) { - final Attachment attachment; - try { - MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints(); - Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment); - SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment); - SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), false, new SignalServiceAddress(destination.serialize())); - attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), databaseAttachment.getFastPreflightId()).get(); - } catch (Exception e) { - // On any error make sure we mark the related DB record's transfer state as failed. - database.updateAttachmentAfterUploadFailed(databaseAttachment.getAttachmentId()); - throw e; - } - - database.updateAttachmentAfterUploadSucceeded(databaseAttachment.getAttachmentId(), attachment); - } - } - - @Override - public void onCanceled() { } - - @Override - protected boolean onShouldRetry(@NonNull Exception exception) { - return exception instanceof IOException || - exception instanceof HTTP.HTTPRequestFailedException; - } - - private SignalServiceAttachment getAttachmentFor(Attachment attachment) { - try { - if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); - InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri()); - return SignalServiceAttachment.newStreamBuilder() - .withStream(is) - .withContentType(attachment.getContentType()) - .withLength(attachment.getSize()) - .withFileName(attachment.getFileName()) - .withVoiceNote(attachment.isVoiceNote()) - .withWidth(attachment.getWidth()) - .withHeight(attachment.getHeight()) - .withCaption(attachment.getCaption()) - .withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress))) - .build(); - } catch (IOException ioe) { - Log.w(TAG, "Couldn't open attachment", ioe); - } - return null; - } - - private Attachment scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase, - @NonNull MediaConstraints constraints, - @NonNull Attachment attachment) - throws UndeliverableMessageException - { - try { - if (constraints.isSatisfied(context, attachment)) { - if (MediaUtil.isJpeg(attachment)) { - MediaStream stripped = constraints.getResizedMedia(context, attachment); - return attachmentDatabase.updateAttachmentData(attachment, stripped); - } else { - return attachment; - } - } else if (constraints.canResize(attachment)) { - MediaStream resized = constraints.getResizedMedia(context, attachment); - return attachmentDatabase.updateAttachmentData(attachment, resized); - } else { - throw new UndeliverableMessageException("Size constraints could not be met!"); - } - } catch (IOException | MmsException e) { - throw new UndeliverableMessageException(e); - } - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull AttachmentUploadJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new AttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)), Address.fromSerialized(data.getString(KEY_DESTINATION))); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 26640d514d..a3644d8ba7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob; -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2; import java.util.ArrayList; import java.util.Arrays; @@ -31,21 +30,12 @@ public final class JobManagerFactories { public static Map getJobFactories(@NonNull Application application) { HashMap factoryHashMap = new HashMap() {{ put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); - put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory()); - put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory()); put(PushDecryptJob.KEY, new PushDecryptJob.Factory()); - put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory()); - put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); - put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); - put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory()); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application)); - put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); - put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory()); put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); - put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory()); }}; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 2e966993a3..6ab83939bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -15,6 +15,7 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.session.libsession.messaging.jobs.Data; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsignal.metadata.InvalidMetadataMessageException; import org.session.libsignal.metadata.ProtocolInvalidMessageException; import org.session.libsignal.service.api.crypto.SignalServiceCipher; @@ -33,8 +34,7 @@ import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.session.libsession.utilities.IdentityKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; @@ -59,15 +59,14 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; -import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage; +import org.session.libsession.messaging.messages.signal.IncomingTextMessage; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.SignalServiceMessageSender; import org.session.libsignal.service.api.messages.SignalServiceContent; import org.session.libsignal.service.api.messages.SignalServiceDataMessage; import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview; @@ -84,8 +83,6 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; -import javax.inject.Inject; - import network.loki.messenger.R; public class PushDecryptJob extends BaseJob implements InjectableType { @@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { private MessageNotifier messageNotifier; - @Inject SignalServiceMessageSender messageSender; - public PushDecryptJob(Context context) { this(context, -1); } @@ -232,17 +227,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType { handleTextMessage(content, message, smsMessageId, Optional.absent()); } - if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) { - handleUnknownGroupMessage(content, message.getGroupInfo().get()); - } - if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { SessionMetaProtocol.handleProfileKeyUpdate(context, content); } - - if (SessionMetaProtocol.shouldSendDeliveryReceipt(message, Address.fromSerialized(content.getSender()))) { - handleNeedsDeliveryReceipt(content, message); - } } else if (content.getReceiptMessage().isPresent()) { SignalServiceReceiptMessage message = content.getReceiptMessage().get(); @@ -268,16 +255,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, - @NonNull SignalServiceGroup group) - { - if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new RequestGroupInfoJob(content.getSender(), group.getGroupId())); - } - } - private void handleExpirationUpdate(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId) @@ -346,7 +323,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { attachments, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000, - ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(), + DistributionTypes.DEFAULT, quote.orNull(), sharedContacts.or(Collections.emptyList()), linkPreviews.or(Collections.emptyList()), Collections.emptyList(), Collections.emptyList()); @@ -476,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Recipient masterRecipient = getMessageMasterDestination(content.getSender()); String syncTarget = message.getSyncTarget().orNull(); - if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) { - handleExpirationUpdate(content, message, Optional.absent()); - } - Long threadId = null; if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { @@ -608,14 +581,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content, - @NonNull SignalServiceDataMessage message) - { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new SendDeliveryReceiptJob(Address.fromSerialized(content.getSender()), message.getTimestamp())); - } - @SuppressLint("DefaultLocale") private void handleDeliveryReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceReceiptMessage message) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java deleted file mode 100644 index 848ff045eb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ /dev/null @@ -1,298 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; - -import org.session.libsession.messaging.jobs.Data; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.threads.Address; -import org.session.libsession.utilities.GroupUtil; - -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SendMessageResult; -import org.session.libsignal.service.api.messages.SignalServiceAttachment; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Quote; -import org.session.libsignal.service.api.messages.SignalServiceGroup; -import org.session.libsignal.service.api.messages.shared.SharedContact; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; - -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class PushGroupSendJob extends PushSendJob implements InjectableType { - - public static final String KEY = "PushGroupSendJob"; - - private static final String TAG = PushGroupSendJob.class.getSimpleName(); - - @Inject SignalServiceMessageSender messageSender; - - private static final String KEY_MESSAGE_ID = "message_id"; - private static final String KEY_FILTER_ADDRESS = "filter_address"; - - private long messageId; - private String filterAddress; - - public PushGroupSendJob(long messageId, @NonNull Address destination, @Nullable Address filterAddress) { - this(new Job.Parameters.Builder() - .setQueue(destination.toGroupString()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - messageId, filterAddress); - - } - - private PushGroupSendJob(@NonNull Job.Parameters parameters, long messageId, @Nullable Address filterAddress) { - super(parameters); - - this.messageId = messageId; - this.filterAddress = filterAddress == null ? null :filterAddress.toString(); - } - - @WorkerThread - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, @Nullable Address filterAddress) { - try { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - OutgoingMediaMessage message = database.getOutgoingMessage(messageId); - List attachments = new LinkedList<>(); - - attachments.addAll(message.getAttachments()); - attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList()); - attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList()); - - List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList(); - - if (attachmentJobs.isEmpty()) { - jobManager.add(new PushGroupSendJob(messageId, destination, filterAddress)); - } else { - jobManager.startChain(attachmentJobs) - .then(new PushGroupSendJob(messageId, destination, filterAddress)) - .enqueue(); - } - - } catch (NoSuchMessageException | MmsException e) { - Log.w(TAG, "Failed to enqueue message.", e); - DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); - notifyMediaMessageDeliveryFailed(context, messageId); - } - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_FILTER_ADDRESS, filterAddress) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onAdded() { - DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); - } - - @Override - public void onPushSend() - throws MmsException, NoSuchMessageException - { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - OutgoingMediaMessage message = database.getOutgoingMessage(messageId); - List existingNetworkFailures = message.getNetworkFailures(); - List existingIdentityMismatches = message.getIdentityKeyMismatches(); - - if (database.isSent(messageId)) { - log(TAG, "Message " + messageId + " was already sent. Ignoring."); - return; - } - - try { - log(TAG, "Sending message: " + messageId); - - List
targets; - - if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress)); - else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList(); - else targets = ClosedGroupsProtocolV2.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString()); - - List results = deliver(message, targets); - List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList(); - List identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList(); - Set
successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet()); - List resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successAddresses.contains(failure.getAddress())).toList(); - List resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successAddresses.contains(failure.getAddress())).toList(); - List successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList(); - - for (NetworkFailure resolvedFailure : resolvedNetworkFailures) { - database.removeFailure(messageId, resolvedFailure); - existingNetworkFailures.remove(resolvedFailure); - } - - for (IdentityKeyMismatch resolvedIdentity : resolvedIdentityFailures) { - database.removeMismatchedIdentity(messageId, resolvedIdentity.getAddress(), resolvedIdentity.getIdentityKey()); - existingIdentityMismatches.remove(resolvedIdentity); - } - - if (!networkFailures.isEmpty()) { - database.addFailures(messageId, networkFailures); - } - - for (IdentityKeyMismatch mismatch : identityMismatches) { - database.addMismatchedIdentity(messageId, mismatch.getAddress(), mismatch.getIdentityKey()); - } - - for (SendMessageResult success : successes) { - DatabaseFactory.getGroupReceiptDatabase(context).setUnidentified(Address.fromSerialized(success.getAddress().getNumber()), - messageId, - success.getSuccess().isUnidentified()); - } - - if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) { - database.markAsSent(messageId, true); - - markAttachmentsUploaded(messageId, message.getAttachments()); - - if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { - database.markExpireStarted(messageId); - ApplicationContext.getInstance(context) - .getExpiringMessageManager() - .scheduleDeletion(messageId, true, message.getExpiresIn()); - } - } else if (!networkFailures.isEmpty()) { - throw new RetryLaterException(); - } else if (!identityMismatches.isEmpty()) { - database.markAsSentFailed(messageId); - notifyMediaMessageDeliveryFailed(context, messageId); - } - } catch (Exception e) { - warn(TAG, e); - database.markAsSentFailed(messageId); - notifyMediaMessageDeliveryFailed(context, messageId); - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception exception) { - if (exception instanceof IOException) return true; - // Loki - Disable since we have our own retrying - return false; - } - - @Override - public void onCanceled() { - DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); - } - - private List deliver(OutgoingMediaMessage message, @NonNull List
destinations) - throws IOException { - - Address address = message.getRecipient().getAddress(); - - List addresses = Stream.of(destinations).map(this::getPushAddress).toList(); - List> unidentifiedAccess = Stream.of(addresses) - .map(a -> Address.fromSerialized(a.getNumber())) - .map(a -> Recipient.from(context, a, false)) - .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) - .toList(); - - if (message.isGroup() && address.isClosedGroup()) { - SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; - String groupId = address.toGroupString(); - List attachments = Stream.of(message.getAttachments()).toList(); - List attachmentPointers = getAttachmentPointersFor(attachments); - // Loki - Only send GroupUpdate or GroupQuit messages to closed groups - OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; - GroupContext groupContext = groupMessage.getGroupContext(); - SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); - SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; - SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList()); - SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getSentTimeMillis()) - .withExpiration(message.getRecipient().getExpireMessages()) - .asGroupMessage(group) - .build(); - - return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage); - } else { - SignalServiceDataMessage groupMessage = getDataMessage(address, message).build(); - - return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage); - } - } - - public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) { - - SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; - - String groupId = address.toGroupString(); - Optional profileKey = getProfileKey(message.getRecipient()); - Optional quote = getQuoteFor(message); - List sharedContacts = getSharedContactsFor(message); - List previews = getPreviewsFor(message); - List attachments = Stream.of(message.getAttachments()).toList(); - List attachmentPointers = getAttachmentPointersFor(attachments); - - SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType); - return SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getSentTimeMillis()) - .asGroupMessage(group) - .withAttachments(attachmentPointers) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .asExpirationUpdate(message.isExpirationUpdate()) - .withProfileKey(profileKey.orNull()) - .withQuote(quote.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews); - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) { - String address = data.getString(KEY_FILTER_ADDRESS); - Address filter = address != null ? Address.fromSerialized(data.getString(KEY_FILTER_ADDRESS)) : null; - - return new PushGroupSendJob(parameters, data.getLong(KEY_MESSAGE_ID), filter); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java deleted file mode 100644 index 08cc05da9f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ /dev/null @@ -1,308 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; - -import com.annimon.stream.Stream; - -import org.session.libsession.messaging.jobs.Data; -import org.session.libsession.messaging.threads.Address; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; -import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode; -import org.session.libsession.utilities.TextSecurePreferences; - -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; -import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.thoughtcrime.securesms.transport.UndeliverableMessageException; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SendMessageResult; -import org.session.libsignal.service.api.messages.SignalServiceAttachment; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview; -import org.session.libsignal.service.api.messages.shared.SharedContact; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.loki.api.SnodeAPI; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import javax.inject.Inject; - -public class PushMediaSendJob extends PushSendJob implements InjectableType { - - public static final String KEY = "PushMediaSendJob"; - - private static final String TAG = PushMediaSendJob.class.getSimpleName(); - - private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id"; - private static final String KEY_MESSAGE_ID = "message_id"; - private static final String KEY_DESTINATION = "destination"; - - @Inject SignalServiceMessageSender messageSender; - - private long messageId; - private long templateMessageId; - private Address destination; - - public PushMediaSendJob(long templateMessageId, long messageId, Address destination) { - this(constructParameters(destination), templateMessageId, messageId, destination); - } - - private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) { - super(parameters); - this.templateMessageId = templateMessageId; - this.messageId = messageId; - this.destination = destination; - } - - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) { - enqueue(context, jobManager, messageId, messageId, destination); - } - - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) { - enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination))); - } - - @WorkerThread - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, List jobs) { - if (jobs.size() == 0) { return; } - PushMediaSendJob first = jobs.get(0); - long messageId = first.templateMessageId; - try { - List attachmentJobs = getAttachmentUploadJobs(context, messageId, first.destination); - - if (attachmentJobs.isEmpty()) { - for (PushMediaSendJob job : jobs) { jobManager.add(job); } - } else { - jobManager.startChain(attachmentJobs) - .then((List)(List)jobs) - .enqueue(); - } - } catch (NoSuchMessageException | MmsException e) { - Log.w(TAG, "Failed to enqueue message.", e); - DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); - notifyMediaMessageDeliveryFailed(context, messageId); - } - } - - public static List getAttachmentUploadJobs(@NonNull Context context, long messageId, @NonNull Address destination) - throws NoSuchMessageException, MmsException - { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - OutgoingMediaMessage message = database.getOutgoingMessage(messageId); - List attachments = new LinkedList<>(); - - attachments.addAll(message.getAttachments()); - attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList()); - attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList()); - - return Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList(); - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder() - .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) - .putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_DESTINATION, destination.serialize()).build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onAdded() { - DatabaseFactory.getMmsDatabase(context).markAsSending(messageId); - } - - @Override - public void onPushSend() - throws RetryLaterException, MmsException, NoSuchMessageException, - UndeliverableMessageException - { - ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - OutgoingMediaMessage message = database.getOutgoingMessage(templateMessageId); - - if (messageId >= 0 && database.isSent(messageId)) { - warn(TAG, "Message " + messageId + " was already sent. Ignoring."); - return; - } - - try { - log(TAG, "Sending message: " + messageId); - - Recipient recipient = Recipient.from(context, destination, false); - byte[] profileKey = recipient.getProfileKey(); - UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); - - boolean unidentified = deliver(message); - - if (messageId >= 0) { - database.markAsSent(messageId, true); - markAttachmentsUploaded(messageId, message.getAttachments()); - database.markUnidentified(messageId, unidentified); - } - - if (recipient.isLocalNumber()) { - SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis()); - DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis()); - DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis()); - } - - if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { - log(TAG, "Marking recipient as UD-unrestricted following a UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED); - } else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { - log(TAG, "Marking recipient as UD-enabled following a UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED); - } else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { - log(TAG, "Marking recipient as UD-disabled following a non-UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED); - } - - if (messageId > 0 && message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { - database.markExpireStarted(messageId); - expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn()); - } - - log(TAG, "Sent message: " + messageId); - - } catch (SnodeAPI.Error e) { - Log.d("Loki", "Couldn't send message due to error: " + e.getDescription()); - if (messageId >= 0) { - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - lokiMessageDatabase.setErrorMessage(messageId, e.getDescription()); - database.markAsSentFailed(messageId); - } - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception exception) { - // Loki - Disable since we have our own retrying - return false; - } - - @Override - public void onCanceled() { - if (messageId >= 0) { - DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); - notifyMediaMessageDeliveryFailed(context, messageId); - } - } - - private boolean deliver(OutgoingMediaMessage message) - throws RetryLaterException, UndeliverableMessageException, SnodeAPI.Error - { - try { - Recipient recipient = Recipient.from(context, destination, false); - String userPublicKey = TextSecurePreferences.getLocalNumber(context); - SignalServiceAddress address = getPushAddress(recipient.getAddress()); - SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); - List attachments = Stream.of(message.getAttachments()).toList(); - List serviceAttachments = getAttachmentPointersFor(attachments); - Optional profileKey = getProfileKey(message.getRecipient()); - Optional quote = getQuoteFor(message); - List sharedContacts = getSharedContactsFor(message); - List previews = getPreviewsFor(message); - - Optional unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient); - - SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() - .withBody(message.getBody()) - .withAttachments(serviceAttachments) - .withTimestamp(message.getSentTimeMillis()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) - .withQuote(quote.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews) - .asExpirationUpdate(message.isExpirationUpdate()) - .build(); - - SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder() - .withBody(message.getBody()) - .withAttachments(serviceAttachments) - .withTimestamp(message.getSentTimeMillis()) - .withSyncTarget(destination.serialize()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) - .withQuote(quote.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews) - .asExpirationUpdate(message.isExpirationUpdate()) - .build(); - - if (userPublicKey == address.getNumber()) { - // Loki - Device link messages don't go through here - SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, true); - if (result.getLokiAPIError() != null) { - throw result.getLokiAPIError(); - } else { - return result.getSuccess().isUnidentified(); - } - } else { - SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, false); - if (result.getLokiAPIError() != null) { - throw result.getLokiAPIError(); - } else { - boolean isUnidentified = result.getSuccess().isUnidentified(); - - try { - // send to ourselves to sync multi-device - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); - SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage, true); - if (selfSendResult.getLokiAPIError() != null) { - throw selfSendResult.getLokiAPIError(); - } - } catch (Exception e) { - Log.e("Loki", "Error sending message to ourselves", e); - } - - return isUnidentified; - } - } - } catch (FileNotFoundException e) { - warn(TAG, e); - throw new UndeliverableMessageException(e); - } catch (IOException e) { - warn(TAG, e); - throw new RetryLaterException(e); - } - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) { - long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID); - long messageID = data.getLong(KEY_MESSAGE_ID); - Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); - return new PushMediaSendJob(parameters, templateMessageID, messageID, destination); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java deleted file mode 100644 index d99ab36c55..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ /dev/null @@ -1,230 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; -import android.graphics.Bitmap; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextUtils; - -import com.annimon.stream.Stream; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; -import org.session.libsession.utilities.MediaTypes; -import org.session.libsignal.utilities.Base64; -import org.session.libsession.utilities.Util; - -import org.greenrobot.eventbus.EventBus; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.contactshare.ContactModelMapper; -import org.session.libsession.utilities.preferences.ProfileKeyUtil; -import org.session.libsession.messaging.threads.Address; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.events.PartProgressEvent; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; -import org.session.libsignal.utilities.Hex; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.messages.SignalServiceAttachment; -import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview; -import org.session.libsignal.service.api.messages.shared.SharedContact; -import org.session.libsignal.service.api.push.SignalServiceAddress; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -public abstract class PushSendJob extends SendJob { - - private static final String TAG = PushSendJob.class.getSimpleName(); - - protected PushSendJob(Job.Parameters parameters) { - super(parameters); - } - - protected static Job.Parameters constructParameters(Address destination) { - return new Parameters.Builder() - .setQueue(destination.serialize()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(1) - .build(); - } - - @Override - protected final void onSend() throws Exception { - onPushSend(); - } - - @Override - public void onRetry() { - super.onRetry(); - Log.i(TAG, "onRetry()"); - } - - protected Optional getProfileKey(@NonNull Recipient recipient) { - if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) { - return Optional.absent(); - } - - return Optional.of(ProfileKeyUtil.getProfileKey(context)); - } - - protected SignalServiceAddress getPushAddress(Address address) { - String relay = null; - return new SignalServiceAddress(address.toString(), Optional.fromNullable(relay)); - } - - protected SignalServiceAttachment getAttachmentFor(Attachment attachment) { - try { - if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); - InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri()); - return SignalServiceAttachment.newStreamBuilder() - .withStream(is) - .withContentType(attachment.getContentType()) - .withLength(attachment.getSize()) - .withFileName(attachment.getFileName()) - .withVoiceNote(attachment.isVoiceNote()) - .withWidth(attachment.getWidth()) - .withHeight(attachment.getHeight()) - .withCaption(attachment.getCaption()) - .withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress))) - .build(); - } catch (IOException ioe) { - Log.w(TAG, "Couldn't open attachment", ioe); - } - return null; - } - - protected @NonNull List getAttachmentPointersFor(List attachments) { - return Stream.of(attachments).map(this::getAttachmentPointerFor).filter(a -> a != null).toList(); - } - - protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) { - if (TextUtils.isEmpty(attachment.getLocation())) { - Log.w(TAG, "empty content id"); - return null; - } - - if (TextUtils.isEmpty(attachment.getKey())) { - Log.w(TAG, "empty encrypted key"); - return null; - } - - try { - long id = Long.parseLong(attachment.getLocation()); - byte[] key = Base64.decode(attachment.getKey()); - - return new SignalServiceAttachmentPointer(id, - attachment.getContentType(), - key, - Optional.of(Util.toIntExact(attachment.getSize())), - Optional.absent(), - attachment.getWidth(), - attachment.getHeight(), - Optional.fromNullable(attachment.getDigest()), - Optional.fromNullable(attachment.getFileName()), - attachment.isVoiceNote(), - Optional.fromNullable(attachment.getCaption()), attachment.getUrl()); - } catch (IOException | ArithmeticException e) { - Log.w(TAG, e); - return null; - } - } - - protected static void notifyMediaMessageDeliveryFailed(Context context, long messageId) { - long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId); - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - - if (threadId != -1 && recipient != null) { - ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); - } - } - - protected Optional getQuoteFor(OutgoingMediaMessage message) { - if (message.getOutgoingQuote() == null) return Optional.absent(); - - long quoteId = message.getOutgoingQuote().getId(); - String quoteBody = message.getOutgoingQuote().getText(); - Address quoteAuthor = message.getOutgoingQuote().getAuthor(); - List quoteAttachments = new LinkedList<>(); - - for (Attachment attachment : message.getOutgoingQuote().getAttachments()) { - BitmapUtil.ScaleResult thumbnailData = null; - SignalServiceAttachment thumbnail = null; - String thumbnailType = MediaTypes.IMAGE_JPEG; - - try { - if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) { - Bitmap.CompressFormat format = BitmapUtil.getCompressFormatForContentType(attachment.getContentType()); - - thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024, format); - thumbnailType = attachment.getContentType(); - } else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) { - thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024); - } - - if (thumbnailData != null) { - thumbnail = SignalServiceAttachment.newStreamBuilder() - .withContentType(thumbnailType) - .withWidth(thumbnailData.getWidth()) - .withHeight(thumbnailData.getHeight()) - .withLength(thumbnailData.getBitmap().length) - .withStream(new ByteArrayInputStream(thumbnailData.getBitmap())) - .build(); - } - - quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(), - attachment.getFileName(), - thumbnail)); - } catch (BitmapDecodingException e) { - Log.w(TAG, e); - } - } - - return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments)); - } - - List getSharedContactsFor(OutgoingMediaMessage mediaMessage) { - List sharedContacts = new LinkedList<>(); - - for (Contact contact : mediaMessage.getSharedContacts()) { - SharedContact.Builder builder = ContactModelMapper.localToRemoteBuilder(contact); - SharedContact.Avatar avatar = null; - - if (contact.getAvatar() != null && contact.getAvatar().getAttachment() != null) { - avatar = SharedContact.Avatar.newBuilder().withAttachment(getAttachmentFor(contact.getAvatarAttachment())) - .withProfileFlag(contact.getAvatar().isProfile()) - .build(); - } - - builder.setAvatar(avatar); - sharedContacts.add(builder.build()); - } - - return sharedContacts; - } - - List getPreviewsFor(OutgoingMediaMessage mediaMessage) { - return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> { - SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null; - return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment)); - }).toList(); - } - - protected abstract void onPushSend() throws Exception; -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java deleted file mode 100644 index ef6e43b31d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ /dev/null @@ -1,241 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.jobs.Data; -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.session.libsignal.utilities.logging.Log; - -import org.session.libsession.messaging.threads.Address; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode; -import org.session.libsession.utilities.TextSecurePreferences; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; -import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SendMessageResult; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.loki.api.SnodeAPI; - -import java.io.IOException; - -import javax.inject.Inject; - -public class PushTextSendJob extends PushSendJob implements InjectableType { - - public static final String KEY = "PushTextSendJob"; - - private static final String TAG = PushTextSendJob.class.getSimpleName(); - - private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id"; - private static final String KEY_MESSAGE_ID = "message_id"; - private static final String KEY_DESTINATION = "destination"; - - @Inject SignalServiceMessageSender messageSender; - - private long messageId; - private long templateMessageId; - private Address destination; - - public PushTextSendJob(long messageId, Address destination) { - this(messageId, messageId, destination); - } - - public PushTextSendJob(long templateMessageId, long messageId, Address destination) { - this(constructParameters(destination), templateMessageId, messageId, destination); - } - - private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) { - super(parameters); - this.templateMessageId = templateMessageId; - this.messageId = messageId; - this.destination = destination; - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder() - .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) - .putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_DESTINATION, destination.serialize()).build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onAdded() { - if (messageId >= 0) { - DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); - } - } - - @Override - public void onPushSend() throws NoSuchMessageException, RetryLaterException { - ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); - SmsDatabase database = DatabaseFactory.getSmsDatabase(context); - SmsMessageRecord record = database.getMessage(templateMessageId); - - Recipient recordRecipient = record.getRecipient().resolve(); - boolean hasSameDestination = destination.equals(recordRecipient.getAddress()); - - if (hasSameDestination && !record.isPending() && !record.isFailed()) { - Log.d("Loki", "Message with ID: " + templateMessageId + " was already sent; ignoring."); - return; - } - - try { - log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device.")); - - Recipient recipient = Recipient.from(context, destination, false); - byte[] profileKey = recipient.getProfileKey(); - UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); - - boolean unidentified = deliver(record); - - if (messageId >= 0) { - database.markAsSent(messageId, true); - database.markUnidentified(messageId, unidentified); - } - - if (recipient.isLocalNumber()) { - SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent()); - DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis()); - DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis()); - } - - if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { - log(TAG, "Marking recipient as UD-unrestricted following a UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED); - } else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { - log(TAG, "Marking recipient as UD-enabled following a UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED); - } else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { - log(TAG, "Marking recipient as UD-disabled following a non-UD send."); - DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED); - } - - if (record.getExpiresIn() > 0 && messageId >= 0) { - database.markExpireStarted(messageId); - expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn()); - } - - log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device.")); - - } catch (SnodeAPI.Error e) { - Log.d("Loki", "Couldn't send message due to error: " + e.getDescription()); - if (messageId >= 0) { - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - lokiMessageDatabase.setErrorMessage(record.getId(), e.getDescription()); - database.markAsSentFailed(record.getId()); - } - } - } - - @Override - public boolean onShouldRetry(@NonNull Exception exception) { - // Loki - Disable since we have our own retrying - return false; - } - - @Override - public void onCanceled() { - if (messageId >= 0) { - DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); - - long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - - if (threadId != -1 && recipient != null) { - ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); - } - } - } - - private boolean deliver(SmsMessageRecord message) throws RetryLaterException, SnodeAPI.Error - { - try { - String userPublicKey = TextSecurePreferences.getLocalNumber(context); - Recipient recipient = Recipient.from(context, destination, false); - SignalServiceAddress address = getPushAddress(recipient.getAddress()); - SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); - Optional profileKey = getProfileKey(recipient); - Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); - - log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent()); - - SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getDateSent()) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) - .build(); - - SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getDateSent()) - .withBody(message.getBody()) - .withSyncTarget(destination.serialize()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) - .build(); - - if (userPublicKey.equals(address.getNumber())) { - // Loki - Device link messages don't go through here - SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, true); - if (result.getLokiAPIError() != null) { - throw result.getLokiAPIError(); - } else { - return result.getSuccess().isUnidentified(); - } - } else { - SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, false); - if (result.getLokiAPIError() != null) { - throw result.getLokiAPIError(); - } else { - boolean isUnidentified = result.getSuccess().isUnidentified(); - - try { - // send to ourselves to sync multi-device - Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); - SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage, true); - if (selfSendResult.getLokiAPIError() != null) { - throw selfSendResult.getLokiAPIError(); - } - } catch (Exception e) { - Log.e("Loki", "Error sending message to ourselves", e); - } - return isUnidentified; - } - } - } catch (IOException e) { - warn(TAG, "Failure", e); - throw new RetryLaterException(e); - } - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) { - long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID); - long messageID = data.getLong(KEY_MESSAGE_ID); - Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); - return new PushTextSendJob(parameters, templateMessageID, messageID, destination); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java deleted file mode 100644 index fc216d68fb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.jobs.Data; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.threads.Address; -import org.session.libsession.utilities.GroupUtil; - -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.messages.SignalServiceGroup; -import org.session.libsignal.service.api.messages.SignalServiceGroup.Type; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class RequestGroupInfoJob extends BaseJob implements InjectableType { - - public static final String KEY = "RequestGroupInfoJob"; - - @SuppressWarnings("unused") - private static final String TAG = RequestGroupInfoJob.class.getSimpleName(); - - private static final String KEY_SOURCE = "source"; - private static final String KEY_GROUP_ID = "group_id"; - - @Inject SignalServiceMessageSender messageSender; - - private String source; - private byte[] groupId; - - public RequestGroupInfoJob(@NonNull String source, @NonNull byte[] groupId) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - source, - groupId); - - } - - private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull String source, @NonNull byte[] groupId) { - super(parameters); - - this.source = source; - this.groupId = groupId; - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder().putString(KEY_SOURCE, source) - .putString(KEY_GROUP_ID, GroupUtil.getEncodedClosedGroupID(groupId)) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException { - SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO) - .withId(groupId, SignalServiceGroup.GroupType.SIGNAL) - .build(); - - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group) - .withTimestamp(System.currentTimeMillis()) - .build(); - - messageSender.sendMessage(0, new SignalServiceAddress(source), - UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromExternal(context, source), false)), - message, false); - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - return e instanceof PushNetworkException; - } - - @Override - public void onCanceled() { - - } - - public static final class Factory implements Job.Factory { - - @Override - public @NonNull RequestGroupInfoJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new RequestGroupInfoJob(parameters, - data.getString(KEY_SOURCE), - GroupUtil.getDecodedGroupIDAsData(data.getString(KEY_GROUP_ID))); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java deleted file mode 100644 index 638d155655..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.jobs.Data; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.session.libsession.messaging.threads.Address; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.logging.Log; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class SendDeliveryReceiptJob extends BaseJob implements InjectableType { - - public static final String KEY = "SendDeliveryReceiptJob"; - - private static final String KEY_ADDRESS = "address"; - private static final String KEY_MESSAGE_ID = "message_id"; - private static final String KEY_TIMESTAMP = "timestamp"; - - private static final String TAG = SendReadReceiptJob.class.getSimpleName(); - - @Inject - transient SignalServiceMessageSender messageSender; - - private String address; - private long messageId; - private long timestamp; - - public SendDeliveryReceiptJob(@NonNull Address address, long messageId) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - address, - messageId, - System.currentTimeMillis()); - } - - private SendDeliveryReceiptJob(@NonNull Job.Parameters parameters, - @NonNull Address address, - long messageId, - long timestamp) - { - super(parameters); - - this.address = address.serialize(); - this.messageId = messageId; - this.timestamp = timestamp; - } - - @Override - public @NonNull - Data serialize() { - return new Data.Builder().putString(KEY_ADDRESS, address) - .putLong(KEY_MESSAGE_ID, messageId) - .putLong(KEY_TIMESTAMP, timestamp) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException { - Log.d("Loki", "Sending delivery receipt."); - SignalServiceAddress remoteAddress = new SignalServiceAddress(address); - SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - Collections.singletonList(messageId), - timestamp); - - messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)), - receiptMessage); - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - if (e instanceof PushNetworkException) return true; - return false; - } - - @Override - public void onCanceled() { - Log.w(TAG, "Failed to send delivery receipt to: " + address); - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull SendDeliveryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new SendDeliveryReceiptJob(parameters, - Address.fromSerialized(data.getString(KEY_ADDRESS)), - data.getLong(KEY_MESSAGE_ID), - data.getLong(KEY_TIMESTAMP)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java deleted file mode 100644 index f71471b74e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.JobLogger; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.mms.MediaStream; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.transport.UndeliverableMessageException; -import org.thoughtcrime.securesms.util.MediaUtil; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -public abstract class SendJob extends BaseJob { - - @SuppressWarnings("unused") - private final static String TAG = SendJob.class.getSimpleName(); - - public SendJob(Job.Parameters parameters) { - super(parameters); - } - - @Override - public final void onRun() throws Exception { - Log.i(TAG, "Starting message send attempt"); - onSend(); - Log.i(TAG, "Message send completed"); - } - - protected abstract void onSend() throws Exception; - - protected void markAttachmentsUploaded(long messageId, @NonNull List attachments) { - AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); - - for (Attachment attachment : attachments) { - database.markAttachmentUploaded(messageId, attachment); - } - } - - protected void log(@NonNull String tag, @NonNull String message) { - Log.i(tag, JobLogger.format(this, message)); - } - - protected void warn(@NonNull String tag, @NonNull String message) { - warn(tag, message, null); - } - - protected void warn(@NonNull String tag, @Nullable Throwable t) { - warn(tag, "", t); - } - - protected void warn(@NonNull String tag, @NonNull String message, @Nullable Throwable t) { - Log.w(tag, JobLogger.format(this, message), t); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java deleted file mode 100644 index 98d1cbb80d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - - -import androidx.annotation.NonNull; - -import org.session.libsession.messaging.jobs.Data; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.session.libsession.messaging.threads.Address; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.session.libsignal.utilities.logging.Log; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.api.push.exceptions.PushNetworkException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class SendReadReceiptJob extends BaseJob implements InjectableType { - - public static final String KEY = "SendReadReceiptJob"; - - private static final String TAG = SendReadReceiptJob.class.getSimpleName(); - - private static final String KEY_ADDRESS = "address"; - private static final String KEY_MESSAGE_IDS = "message_ids"; - private static final String KEY_TIMESTAMP = "timestamp"; - - @Inject SignalServiceMessageSender messageSender; - - private String address; - private List messageIds; - private long timestamp; - - public SendReadReceiptJob(Address address, List messageIds) { - this(new Job.Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), - address, - messageIds, - System.currentTimeMillis()); - } - - private SendReadReceiptJob(@NonNull Job.Parameters parameters, - @NonNull Address address, - @NonNull List messageIds, - long timestamp) - { - super(parameters); - - this.address = address.serialize(); - this.messageIds = messageIds; - this.timestamp = timestamp; - } - - @Override - public @NonNull - Data serialize() { - long[] ids = new long[messageIds.size()]; - for (int i = 0; i < ids.length; i++) { - ids[i] = messageIds.get(i); - } - - return new Data.Builder().putString(KEY_ADDRESS, address) - .putLongArray(KEY_MESSAGE_IDS, ids) - .putLong(KEY_TIMESTAMP, timestamp) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws IOException { - if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return; - - SignalServiceAddress remoteAddress = new SignalServiceAddress(address); - SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp); - - messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)), - receiptMessage); - } - - @Override - public boolean onShouldRetry(@NonNull Exception e) { - if (e instanceof PushNetworkException) return true; - return false; - } - - @Override - public void onCanceled() { - Log.w(TAG, "Failed to send read receipts to: " + address); - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { - Address address = Address.fromSerialized(data.getString(KEY_ADDRESS)); - long timestamp = data.getLong(KEY_TIMESTAMP); - long[] ids = data.hasLongArray(KEY_MESSAGE_IDS) ? data.getLongArray(KEY_MESSAGE_IDS) : new long[0]; - List messageIds = new ArrayList<>(ids.length); - - for (long id : ids) { - messageIds.add(id); - } - - return new SendReadReceiptJob(parameters, address, messageIds, timestamp); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java deleted file mode 100644 index ad0325045b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.session.libsession.messaging.jobs.Data; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.dependencies.InjectableType; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.session.libsignal.utilities.logging.Log; -import org.session.libsignal.service.api.SignalServiceMessageSender; -import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; -import org.session.libsignal.service.api.messages.SignalServiceTypingMessage.Action; -import org.session.libsignal.service.api.push.SignalServiceAddress; - -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -public class TypingSendJob extends BaseJob implements InjectableType { - - public static final String KEY = "TypingSendJob"; - - private static final String TAG = TypingSendJob.class.getSimpleName(); - - private static final String KEY_THREAD_ID = "thread_id"; - private static final String KEY_TYPING = "typing"; - - private long threadId; - private boolean typing; - - @Inject SignalServiceMessageSender messageSender; - - public TypingSendJob(long threadId, boolean typing) { - this(new Job.Parameters.Builder() - .setQueue("TYPING_" + threadId) - .setMaxAttempts(1) - .setLifespan(TimeUnit.SECONDS.toMillis(5)) - .build(), - threadId, - typing); - } - - private TypingSendJob(@NonNull Job.Parameters parameters, long threadId, boolean typing) { - super(parameters); - - this.threadId = threadId; - this.typing = typing; - } - - - @Override - public @NonNull - Data serialize() { - return new Data.Builder().putLong(KEY_THREAD_ID, threadId) - .putBoolean(KEY_TYPING, typing) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - public void onRun() throws Exception { - if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) { - return; - } - - Log.d(TAG, "Sending typing " + (typing ? "started" : "stopped") + " for thread " + threadId); - - Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - - if (recipient == null) { - throw new IllegalStateException("Tried to send a typing indicator to a non-existent thread."); - } - - List recipients = Collections.singletonList(recipient); - - if (recipient.isGroupRecipient()) { - recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.getAddress().toGroupString(), false); - } - - List addresses = Stream.of(recipients).map(r -> new SignalServiceAddress(r.getAddress().serialize())).toList(); - List> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList(); - SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis()); - - messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage); - } - - @Override - public void onCanceled() { - } - - @Override - protected boolean onShouldRetry(@NonNull Exception exception) { - return false; - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull TypingSendJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new TypingSendJob(parameters, data.getLong(KEY_THREAD_ID), data.getBoolean(KEY_TYPING)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java index 583a7d5465..eae25110b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewRepository.java @@ -9,9 +9,9 @@ import androidx.annotation.Nullable; import com.google.android.gms.common.util.IOUtils; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.utilities.MediaTypes; import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.net.CallRequestController; @@ -192,7 +192,7 @@ public class LinkPreviewRepository implements InjectableType { return Optional.of(new UriAttachment(uri, uri, contentType, - AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, bytes.length, bitmap.getWidth(), bitmap.getHeight(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index f18a54fa13..60c6431906 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -43,13 +43,13 @@ public class LinkPreviewViewModel extends ViewModel { return linkPreviewState.getValue() != null && linkPreviewState.getValue().getLinkPreview().isPresent(); } - public @NonNull List getActiveLinkPreviews() { + public Optional getActiveLinkPreview() { final LinkPreviewState state = linkPreviewState.getValue(); if (state == null || !state.getLinkPreview().isPresent()) { - return Collections.emptyList(); + return Optional.absent(); } else { - return Collections.singletonList(state.getLinkPreview().get()); + return state.getLinkPreview(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index af0f5cc57b..2ad317bdba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -13,18 +13,18 @@ import android.widget.Toast import kotlinx.android.synthetic.main.activity_create_closed_group.* import network.loki.messenger.R import nl.komponents.kovenant.ui.successUi +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.DistributionTypes import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.utilities.fadeIn import org.thoughtcrime.securesms.loki.utilities.fadeOut import org.thoughtcrime.securesms.mms.GlideApp import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol //TODO Refactor to avoid using kotlinx.android.synthetic class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks> { @@ -104,13 +104,13 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM if (selectedMembers.count() < 1) { return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() } - if (selectedMembers.count() >= ClosedGroupsProtocolV2.groupSizeLimit) { // Minus one because we're going to include self later + if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } val userPublicKey = TextSecurePreferences.getLocalNumber(this)!! isLoading = true loaderContainer.fadeIn() - ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> + MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> loaderContainer.fadeOut() isLoading = false val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) @@ -128,7 +128,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) { val intent = Intent(context, ConversationActivity::class.java) intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address) context.startActivity(intent) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt index 0fd6793423..b2eab44fb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/CreatePrivateChatActivity.kt @@ -18,6 +18,7 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.DistributionTypes import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment @@ -69,7 +70,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC intent.setDataAndType(getIntent().data, getIntent().type) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) startActivity(intent) finish() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt index 65bf5ef905..2a319c514f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt @@ -19,12 +19,12 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.groupSizeLimit import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.session.libsession.messaging.threads.Address import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 import org.thoughtcrime.securesms.loki.utilities.fadeIn import org.thoughtcrime.securesms.loki.utilities.fadeOut import org.thoughtcrime.securesms.mms.GlideApp @@ -260,7 +260,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() } - val maxGroupMembers = if (isClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit + val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit if (members.size >= maxGroupMembers) { return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() } @@ -277,17 +277,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { isLoading = true loaderContainer.fadeIn() val promise: Promise = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { - ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!) + MessageSender.explicitLeave(groupPublicKey!!) } else { task { if (hasNameChanged) { - ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity, groupPublicKey!!, name) + MessageSender.explicitNameChange(groupPublicKey!!, name) } members.filterNot { it in originalMembers }.let { adds -> - if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) + if (adds.isNotEmpty()) MessageSender.explicitAddMembers(groupPublicKey!!, adds.map { it.address.serialize() }) } originalMembers.filterNot { it in members }.let { removes -> - if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) + if (removes.isNotEmpty()) MessageSender.explicitRemoveMembers(groupPublicKey!!, removes.map { it.address.serialize() }) } } } @@ -296,13 +296,11 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { isLoading = false finish() }.failUi { exception -> - val message = if (exception is ClosedGroupsProtocolV2.Error) exception.description else "An error occurred" + val message = if (exception is MessageSender.Error) exception.description else "An error occurred" Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show() loaderContainer.fadeOut() isLoading = false } - } else { - GroupManager.updateGroup(this, groupID, members, null, name, admins) } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index f5c062350a..281d42b552 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -26,13 +26,12 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.MessageSender import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.ProfilePictureModifiedEvent -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.Util +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.utilities.* import org.session.libsignal.service.loki.utilities.mentions.MentionsManager import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.utilities.ThreadUtils @@ -42,7 +41,6 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.loki.dialogs.* -import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.views.ConversationView @@ -142,6 +140,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), if (userPublicKey != null) { MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB) application.publicChatManager.startPollersIfNeeded() + JobQueue.shared.resumePendingJobs() } IP2Country.configureIfNeeded(this) application.registerForFCMIfNeeded(false) @@ -344,7 +343,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), isClosedGroup = false } if (isClosedGroup) { - ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!) + MessageSender.explicitLeave(groupPublicKey!!) } else { Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() return@launch diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt index 882671bd67..50080956ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt @@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.loki.activities import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.Toast import network.loki.messenger.R -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo import org.thoughtcrime.securesms.loki.views.FakeChatView diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt index 6d6255e788..6f3ef464bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt @@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.session.libsession.utilities.KeyPairUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt index db98bd0989..f6b7e64433 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/QRCodeActivity.kt @@ -17,6 +17,7 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.DistributionTypes import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment @@ -60,7 +61,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF intent.setDataAndType(getIntent().data, getIntent().type) val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient) intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread) - intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT) + intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT) startActivity(intent) finish() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt index 2a3633729a..24d160f880 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RecoveryPhraseRestoreActivity.kt @@ -19,7 +19,7 @@ import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.Hex import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.session.libsession.utilities.KeyPairUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt index 4b38eedf6a..229edab444 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/RegisterActivity.kt @@ -23,7 +23,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.util.KeyHelper import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.session.libsession.utilities.KeyPairUtilities import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo import java.util.* diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt index 07d8d8f9db..2c79b4fc87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SeedActivity.kt @@ -12,7 +12,7 @@ import android.widget.Toast import kotlinx.android.synthetic.main.activity_seed.* import network.loki.messenger.R import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.session.libsession.utilities.TextSecurePreferences diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt index 989cb2a6fa..4b69f0019f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatPoller.kt @@ -7,12 +7,11 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.messaging.threads.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.PushDecryptJob import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob -import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.libsignal.util.guava.Optional diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt index e3c5b0aa6a..9be7b3e461 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt @@ -14,36 +14,12 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString -import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.session.libsession.utilities.KeyPairUtilities class SessionProtocolImpl(private val context: Context) : SessionProtocol { private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } - override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray { - val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair - val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) - - val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey - val signature = ByteArray(Sign.BYTES) - try { - sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) - } catch (exception: Exception) { - Log.d("Loki", "Couldn't sign message due to error: $exception.") - throw SessionProtocol.Exception.SigningFailed - } - val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature - val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES) - try { - sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey) - } catch (exception: Exception) { - Log.d("Loki", "Couldn't encrypt message due to error: $exception.") - throw SessionProtocol.Exception.EncryptionFailed - } - - return ciphertext - } - override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair { val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index 077715f7e6..ccd5fa88b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsignal.utilities.Hex import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.service.loki.utilities.PublicKeyValidation diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt index 30960a9c96..dda3b7d7eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt @@ -12,22 +12,22 @@ import org.thoughtcrime.securesms.loki.utilities.* class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { companion object { - private val sessionJobTable = "loki_thread_session_reset_database" - val jobID = "job_id" - val jobType = "job_type" - val failureCount = "failure_count" - val serializedData = "serialized_data" + private const val sessionJobTable = "session_job_database" + const val jobID = "job_id" + const val jobType = "job_type" + const val failureCount = "failure_count" + const val serializedData = "serialized_data" @JvmStatic val createSessionJobTableCommand = "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);" } fun persistJob(job: Job) { val database = databaseHelper.writableDatabase - val contentValues = ContentValues(2) + val contentValues = ContentValues(4) contentValues.put(jobID, job.id) contentValues.put(jobType, job.getFactoryKey()) contentValues.put(failureCount, job.failureCount) contentValues.put(serializedData, SessionJobHelper.dataSerializer.serialize(job.serialize())) - database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID.toString())) + database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID)) } fun markJobAsSucceeded(job: Job) { @@ -51,7 +51,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa database.getAll(sessionJobTable, "$jobType = ?", arrayOf(AttachmentUploadJob.KEY)) { cursor -> result.add(jobFromCursor(cursor) as AttachmentUploadJob) } - return result.first { job -> job.attachmentID == attachmentID } + return result.firstOrNull { job -> job.attachmentID == attachmentID } } fun getMessageSendJob(messageSendJobID: String): MessageSendJob? { @@ -85,10 +85,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } -class SessionJobHelper() { - - companion object { - val dataSerializer: Data.Serializer = JsonDataSerializer() - val sessionJobInstantiator: SessionJobInstantiator = SessionJobInstantiator(SessionJobManagerFactories.getSessionJobFactories()) - } +object SessionJobHelper { + val dataSerializer: Data.Serializer = JsonDataSerializer() + val sessionJobInstantiator: SessionJobInstantiator = SessionJobInstantiator(SessionJobManagerFactories.getSessionJobFactories()) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt index 4a14d23ced..cb1bf22566 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/ClearAllDataDialog.kt @@ -11,7 +11,7 @@ import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol -import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities +import org.session.libsession.utilities.KeyPairUtilities class ClearAllDataDialog : DialogFragment() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt index dd97e832e9..12446cf57b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/dialogs/SeedDialog.kt @@ -13,7 +13,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import kotlinx.android.synthetic.main.dialog_seed.view.* import network.loki.messenger.R -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.session.libsession.utilities.IdentityKeyUtil import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt deleted file mode 100644 index ba692a041e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ /dev/null @@ -1,253 +0,0 @@ -package org.thoughtcrime.securesms.loki.protocol - -import com.google.protobuf.ByteString -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.session.libsession.messaging.jobs.Data -import org.session.libsession.messaging.threads.Address -import org.session.libsession.messaging.threads.recipients.Recipient -import org.session.libsignal.libsignal.ecc.DjbECPrivateKey -import org.session.libsignal.libsignal.ecc.DjbECPublicKey -import org.session.libsignal.libsignal.ecc.ECKeyPair -import org.session.libsignal.libsignal.util.guava.Optional -import org.session.libsignal.service.api.push.SignalServiceAddress -import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage -import org.session.libsignal.service.loki.utilities.TTLUtilities -import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded -import org.session.libsignal.service.loki.utilities.toHexString -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint -import org.thoughtcrime.securesms.jobs.BaseJob -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.utilities.Hex - -import java.util.concurrent.TimeUnit - -class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, - private val destination: String, - private val kind: Kind, - private val sentTime: Long) : BaseJob(parameters) { - - sealed class Kind { - class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection, val admins: Collection) : Kind() - class Update(val name: String, val members: Collection) : Kind() - object Leave : Kind() - class RemoveMembers(val members: Collection) : Kind() - class AddMembers(val members: Collection) : Kind() - class NameChange(val name: String) : Kind() - class EncryptionKeyPair(val wrappers: Collection, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually - } - - companion object { - const val KEY = "ClosedGroupUpdateMessageSendJobV2" - } - - @Serializable - data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) { - - companion object { - - fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper { - return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray()) - } - } - - fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper { - val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder() - result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey)) - result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair) - return result.build() - } - } - - constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(20) - .build(), - destination, - kind, - sentTime) - - override fun getFactoryKey(): String { return KEY } - - override fun serialize(): Data { - val builder = Data.Builder() - builder.putString("destination", destination) - builder.putLong("sentTime", sentTime) - when (kind) { - is Kind.New -> { - builder.putString("kind", "New") - builder.putByteArray("publicKey", kind.publicKey) - builder.putString("name", kind.name) - builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) - builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize()) - val members = kind.members.joinToString(" - ") { it.toHexString() } - builder.putString("members", members) - val admins = kind.admins.joinToString(" - ") { it.toHexString() } - builder.putString("admins", admins) - } - is Kind.Update -> { - builder.putString("kind", "Update") - builder.putString("name", kind.name) - val members = kind.members.joinToString(" - ") { it.toHexString() } - builder.putString("members", members) - } - is Kind.RemoveMembers -> { - builder.putString("kind", "RemoveMembers") - val members = kind.members.joinToString(" - ") { it.toHexString() } - builder.putString("members", members) - } - Kind.Leave -> { - builder.putString("kind", "Leave") - } - is Kind.AddMembers -> { - builder.putString("kind", "AddMembers") - val members = kind.members.joinToString(" - ") { it.toHexString() } - builder.putString("members", members) - } - is Kind.NameChange -> { - builder.putString("kind", "NameChange") - builder.putString("name", kind.name) - } - is Kind.EncryptionKeyPair -> { - builder.putString("kind", "EncryptionKeyPair") - val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) } - builder.putString("wrappers", wrappers) - builder.putString("targetUser", kind.targetUser) - } - } - return builder.build() - } - - class Factory : Job.Factory { - - override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 { - val destination = data.getString("destination") - val rawKind = data.getString("kind") - val sentTime = data.getLong("sentTime") - val kind: Kind - when (rawKind) { - "New" -> { - val publicKey = data.getByteArray("publicKey") - val name = data.getString("name") - val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey") - val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey") - val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey)) - val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } - val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } - kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins) - } - "Update" -> { - val name = data.getString("name") - val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } - kind = Kind.Update(name, members) - } - "EncryptionKeyPair" -> { - val wrappers: Collection = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) } - val targetUser = data.getString("targetUser") - kind = Kind.EncryptionKeyPair(wrappers, targetUser) - } - "RemoveMembers" -> { - val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } - kind = Kind.RemoveMembers(members) - } - "AddMembers" -> { - val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } - kind = Kind.AddMembers(members) - } - "NameChange" -> { - val name = data.getString("name") - kind = Kind.NameChange(name) - } - "Leave" -> { - kind = Kind.Leave - } - else -> throw Exception("Invalid closed group update message kind: $rawKind.") - } - return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime) - } - } - - public override fun onRun() { - val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) { - kind.targetUser - } else { - destination - } - val contentMessage = SignalServiceProtos.Content.newBuilder() - val dataMessage = DataMessage.newBuilder() - val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder() - when (kind) { - is Kind.New -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW - closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey) - closedGroupUpdate.name = kind.name - val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder() - encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) - encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize()) - closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build() - closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) - closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) - } - is Kind.Update -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE - closedGroupUpdate.name = kind.name - closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) - } - is Kind.EncryptionKeyPair -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR - closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) - if (kind.targetUser != null) { - closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination)) - } - } - Kind.Leave -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT - } - is Kind.RemoveMembers -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED - closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) - } - is Kind.AddMembers -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED - closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) - } - is Kind.NameChange -> { - closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE - closedGroupUpdate.name = kind.name - } - } - dataMessage.closedGroupControlMessage = closedGroupUpdate.build() - contentMessage.dataMessage = dataMessage.build() - val serializedContentMessage = contentMessage.build().toByteArray() - val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() - val address = SignalServiceAddress(sendDestination) - val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false) - val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) - val ttl = when (kind) { - is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000 - else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) - } - try { - // isClosedGroup can always be false as it's only used in the context of legacy closed groups - messageSender.sendMessage(0, address, udAccess, - sentTime, serializedContentMessage, false, ttl, - true, false, false, Optional.absent()) - } catch (e: Exception) { - Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.") - } - } - - public override fun onShouldRetry(e: Exception): Boolean { - return true - } - - override fun onCanceled() { } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 9fba4b66ef..d2b0ad11aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -3,32 +3,25 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred -import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.ECKeyPair -import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext -import org.session.libsignal.utilities.ThreadUtils -import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase -import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage -import org.thoughtcrime.securesms.sms.IncomingGroupMessage -import org.thoughtcrime.securesms.sms.IncomingTextMessage -import org.session.libsignal.utilities.Hex +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair +import org.session.libsession.messaging.sending_receiving.pendingKeyPair +import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.GroupRecord @@ -37,258 +30,8 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import java.util.* -import java.util.concurrent.ConcurrentHashMap object ClosedGroupsProtocolV2 { - const val groupSizeLimit = 100 - - private val pendingKeyPair = ConcurrentHashMap>() - - sealed class Error(val description: String) : Exception() { - object NoThread : Error("Couldn't find a thread associated with the given group public key") - object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.") - object InvalidUpdate : Error("Invalid group update.") - } - - fun createClosedGroup(context: Context, name: String, members: Collection): Promise { - val deferred = deferred() - ThreadUtils.queue { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val membersAsData = members.map { Hex.fromStringCondensed(it) } - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - // Generate the group's public key - val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix - val sentTime = System.currentTimeMillis() - // Generate the key pair that'll be used for encryption and decryption - val encryptionKeyPair = Curve.generateKeyPair() - // Create the group - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val admins = setOf( userPublicKey ) - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis()) - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) - // Send a closed group update message to all members individually - // Add the group to the user's set of public keys to poll for - apiDB.addClosedGroupPublicKey(groupPublicKey) - // Store the encryption key pair - apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) - // Notify the user - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime) - - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) - for (member in members) { - val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately to make all of this sync - } - // Notify the PN server - LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) - // Fulfill the promise - deferred.resolve(groupID) - } - // Return - return deferred.promise - } - - /** - * @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if - * you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation] - */ - @JvmStatic @JvmOverloads - fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise { - val deferred = deferred() - ThreadUtils.queue { - val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey - val admins = group.admins.map { it.serialize() } - val name = group.title - val sentTime = System.currentTimeMillis() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - return@queue deferred.reject(Error.NoThread) - } - // Send the update to the group - @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Notify the user - val infoType = GroupContext.Type.QUIT - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - if (notifyUser) { - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) - } - // Remove the group private key and unsubscribe from PNs - disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey) - deferred.resolve(Unit) - } - return deferred.promise - } - - @JvmStatic - fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - throw Error.NoThread - } - val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd - val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) } - val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val sentTime = System.currentTimeMillis() - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() ?: Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)).orNull() - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - throw Error.NoKeyPair - } - val name = group.title - // Send the update to the group - val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Save the new group members - groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) - // Send closed group update messages to any new members individually - for (member in membersToAdd) { - @Suppress("NAME_SHADOWING") - val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) - @Suppress("NAME_SHADOWING") - val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) - ApplicationContext.getInstance(context).jobManager.add(newMemberJob) - } - } - - @JvmStatic - fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - throw Error.NoThread - } - val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove - val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val sentTime = System.currentTimeMillis() - val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) - if (encryptionKeyPair == null) { - Log.d("Loki", "Couldn't get encryption key pair for closed group.") - throw Error.NoKeyPair - } - if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) { - Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.") - throw Error.InvalidUpdate - } - val name = group.title - // Send the update to the group - val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Save the new group members - groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) - val isCurrentUserAdmin = admins.contains(userPublicKey) - if (isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) - } - } - - @JvmStatic - fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) { - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - val members = group.members.map { it.serialize() }.toSet() - val admins = group.admins.map { it.serialize() } - val sentTime = System.currentTimeMillis() - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - throw Error.NoThread - } - // Send the update to the group - val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) - job.setContext(context) - job.onRun() // Run the job immediately - // Notify the user - val infoType = GroupContext.Type.UPDATE - val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) - // Update the group - groupDB.updateTitle(groupID, newName) - } - - private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection) { - // Prepare - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - val groupDB = DatabaseFactory.getGroupDatabase(context) - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Can't update nonexistent closed group.") - return - } - if (!group.admins.map { it.toString() }.contains(userPublicKey)) { - Log.d("Loki", "Can't distribute new encryption key pair as non-admin.") - return - } - // Generate the new encryption key pair - val newKeyPair = Curve.generateKeyPair() - // replace call will not succeed if no value already set - pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent()) - do { - // make sure we set the pendingKeyPair or wait until it is not null - } while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair))) - // Distribute it - sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers) - // Store it * after * having sent out the message to the group - apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) - pendingKeyPair[groupPublicKey] = Optional.absent() - } - - private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection, targetUser: String? = null, force: Boolean = true) { - val proto = SignalServiceProtos.KeyPair.newBuilder() - proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) - proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) - val plaintext = proto.build().toByteArray() - val wrappers = targetMembers.mapNotNull { publicKey -> - val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) - ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) - } - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis()) - if (force) { - job.setContext(context) - job.onRun() // Run the job immediately - } else { - ApplicationContext.getInstance(context).jobManager.add(job) - } - } - @JvmStatic fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return } @@ -361,11 +104,11 @@ object ClosedGroupsProtocolV2 { apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user (if we didn't make the group) if (userPublicKey != senderPublicKey) { - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } else if (prevGroup == null) { // only notify if we created this group val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) @@ -410,7 +153,7 @@ object ClosedGroupsProtocolV2 { val isCurrentUserAdmin = admins.contains(userPublicKey) groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) if (isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers) + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers) } } val (contextType, signalType) = @@ -418,9 +161,9 @@ object ClosedGroupsProtocolV2 { else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp) } } @@ -448,9 +191,9 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } if (userPublicKey in admins) { // send current encryption key to the latest added members @@ -460,7 +203,7 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { for (user in updateMembers) { - sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) + MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) } } } @@ -487,9 +230,9 @@ object ClosedGroupsProtocolV2 { // Notify the user if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } } @@ -523,15 +266,15 @@ object ClosedGroupsProtocolV2 { val isCurrentUserAdmin = admins.contains(userPublicKey) groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) if (isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList) + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList) } } // Notify user if (userLeft) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) } } @@ -598,59 +341,6 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Received a new closed group encryption key pair") } - private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, - name: String, members: Collection, admins: Collection, sentTimestamp: Long) { - val groupContextBuilder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) - .setType(type0) - .setName(name) - .addAllMembers(members) - .addAllAdmins(admins) - val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) - val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) - val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") - val smsDB = DatabaseFactory.getSmsDatabase(context) - smsDB.insertMessageInbox(infoMessage) - } - - private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, - members: Collection, admins: Collection, threadID: Long, - sentTimestamp: Long) { - val userPublicKey = TextSecurePreferences.getLocalNumber(context) - val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) - val groupContextBuilder = GroupContext.newBuilder() - .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) - .setType(type) - .setName(name) - .addAllMembers(members) - .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf()) - val mmsDB = DatabaseFactory.getMmsDatabase(context) - val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) - if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTimestamp) - mmsDB.markAsSent(infoMessageID, true) - } - - @JvmStatic - fun getMessageDestinations(context: Context, groupID: String): List
{ - return if (GroupUtil.isOpenGroup(groupID)) { - listOf(Address.fromSerialized(groupID)) - } else { - var groupPublicKey: String? = null - try { - groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() // TODO: The toHexString() here might be unnecessary - } catch (exception: Exception) { - // Do nothing - } - if (groupPublicKey != null && DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)) { - listOf(Address.fromSerialized(groupPublicKey)) - } else { - DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address } - } - } - } - // region Deprecated private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Prepare @@ -685,7 +375,7 @@ object ClosedGroupsProtocolV2 { val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet()) val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey) if (wasAnyUserRemoved && isCurrentUserAdmin) { - generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members) + MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members) } // Update the group groupDB.updateTitle(groupID, name) @@ -700,9 +390,9 @@ object ClosedGroupsProtocolV2 { val admins = group.admins.map { it.toString() } if (userPublicKey == senderPublicKey) { val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) - insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) + DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) } else { - insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) + DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt index d6f3ce408e..69dfc70126 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -3,30 +3,26 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import com.google.protobuf.ByteString import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.control.ConfigurationMessage +import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.preferences.ProfileKeyUtil -import org.session.libsignal.libsignal.util.guava.Optional -import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.loki.utilities.ContactUtilities import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities -import java.util.* object MultiDeviceProtocol { - // TODO: refactor this to use new message sending job @JvmStatic fun syncConfigurationIfNeeded(context: Context) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return @@ -39,22 +35,10 @@ object MultiDeviceProtocol { ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) } val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return - val serializedMessage = configurationMessage.toProto()!!.toByteArray() - val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() - val address = SignalServiceAddress(userPublicKey) - val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false) - val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) - try { - messageSender.sendMessage(0, address, udAccess, - Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), - true, false, false, Optional.absent()) - TextSecurePreferences.setLastConfigurationSyncTime(context, now) - } catch (e: Exception) { - Log.d("Loki", "Failed to send configuration message due to error: $e.") - } + MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey)) + TextSecurePreferences.setLastConfigurationSyncTime(context, now) } - // TODO: refactor this to use new message sending job fun forceSyncConfigurationNowIfNeeded(context: Context) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return val contacts = ContactUtilities.getAllContacts(context).filter { recipient -> @@ -63,19 +47,8 @@ object MultiDeviceProtocol { ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey) } val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return - val serializedMessage = configurationMessage.toProto()!!.toByteArray() - val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() - val address = SignalServiceAddress(userPublicKey) - val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false) - val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) - try { - messageSender.sendMessage(0, address, udAccess, - Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), - true, false, false, Optional.absent()) - TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) - } catch (e: Exception) { - Log.d("Loki", "Failed to send configuration message due to error: $e.") - } + MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey))) + TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) } // TODO: remove this after we migrate to new message receiving pipeline @@ -106,8 +79,8 @@ object MultiDeviceProtocol { closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey)) closedGroupUpdate.name = closedGroup.name val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder() - encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) - encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.privateKey.serialize()) + encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded()) + encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.privateKey.serialize()) closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build() closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt index 350dddaa3d..9c79d2dc3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MessageAudioView.kt @@ -20,6 +20,7 @@ import network.loki.messenger.R import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.thoughtcrime.securesms.ApplicationContext import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.audio.AudioSlidePlayer @@ -152,7 +153,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener { downloadProgress.progress = 0 } } - (showControls && audio.transferState == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) -> { + (showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> { controlToggle.displayQuick(downloadProgress) seekBar.isEnabled = false downloadProgress.isIndeterminate = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index 1e5ceca61d..a092841633 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -145,14 +145,14 @@ class ProfilePictureView : RelativeLayout { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { if (publicKey.isNotEmpty()) { - if (imagesCached.contains(publicKey)) return val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) + if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return val signalProfilePicture = recipient.contactPhoto if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { glide.clear(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView) - imagesCached.add(publicKey) + imagesCached.add(recipient.profileAvatar.orEmpty()) } else { val sizeInPX = resources.getDimensionPixelSize(sizeResId) glide.clear(imageView) @@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout { publicKey, displayName )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) - imagesCached.add(publicKey) + imagesCached.add(recipient.profileAvatar.orEmpty()) } } else { imageView.setImageDrawable(null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java index 877a68f76d..9a1885ab90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import network.loki.messenger.R; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; import org.session.libsession.utilities.MediaTypes; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -38,7 +39,7 @@ public class AudioSlide extends Slide { } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null)); + super(context, new UriAttachment(uri, null, contentType, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java index 0c036b96ca..2eecd9a0de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.java @@ -23,7 +23,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; import org.thoughtcrime.securesms.util.MediaUtil; import org.session.libsignal.libsignal.util.guava.Optional; @@ -135,8 +135,8 @@ public abstract class Slide { } public boolean isPendingDownload() { - return getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED || - getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING; + return getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED || + getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING; } public int getTransferState() { @@ -172,7 +172,7 @@ public abstract class Slide { return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType, - AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, size, width, height, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index d89acb0273..4aacc56cac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -25,18 +25,19 @@ import android.os.AsyncTask; import android.os.Bundle; import androidx.core.app.RemoteInput; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; import org.thoughtcrime.securesms.ApplicationContext; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.mms.MmsException; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsignal.utilities.logging.Log; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -72,17 +73,29 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { long replyThreadId; - int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); - long expiresIn = recipient.getExpireMessages() * 1000L; + if (threadId == -1) { + replyThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); + } else { + replyThreadId = threadId; + } + + VisibleMessage message = new VisibleMessage(); + message.setText(responseText.toString()); + message.setSentTimestamp(System.currentTimeMillis()); + MessageSender.send(message, recipient.getAddress()); if (recipient.isGroupRecipient()) { Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message"); - OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - replyThreadId = MessageSender.send(context, reply, threadId, false, null); + OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null); + try { + DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, replyThreadId, false, null); + } catch (MmsException e) { + Log.w(TAG, e); + } } else { Log.w("AndroidAutoReplyReceiver", "Sending regular message "); - OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId); - replyThreadId = MessageSender.send(context, reply, threadId, false, null); + OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient); + DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null); } List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index fd2c2be969..042f106c55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -11,13 +11,15 @@ import androidx.core.app.NotificationManagerCompat; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import org.session.libsession.messaging.messages.control.ReadReceipt; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; -import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -25,7 +27,6 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; public class MarkReadReceiver extends BroadcastReceiver { @@ -68,8 +69,7 @@ public class MarkReadReceiver extends BroadcastReceiver { public static void process(@NonNull Context context, @NonNull List markedReadMessages) { if (markedReadMessages.isEmpty()) return; - - List syncMessageIds = new LinkedList<>(); + if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return; for (MarkedMessageInfo messageInfo : markedReadMessages) { scheduleDeletion(context, messageInfo.getExpirationInfo()); @@ -83,10 +83,9 @@ public class MarkReadReceiver extends BroadcastReceiver { List timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); // Loki - Check whether we want to send a read receipt to this user if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; } - // Loki - Take into account multi device - ApplicationContext.getInstance(context) - .getJobManager() - .add(new SendReadReceiptJob(address, timestamps)); + ReadReceipt readReceipt = new ReadReceipt(timestamps); + readReceipt.setSentTimestamp(System.currentTimeMillis()); + MessageSender.send(readReceipt, address); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 2110440610..f49f5893e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -25,17 +25,19 @@ import android.os.AsyncTask; import android.os.Bundle; import androidx.core.app.RemoteInput; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.mms.MmsException; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -68,21 +70,27 @@ public class RemoteReplyReceiver extends BroadcastReceiver { new AsyncTask() { @Override protected Void doInBackground(Void... params) { - long threadId; - Recipient recipient = Recipient.from(context, address, false); - int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); - long expiresIn = recipient.getExpireMessages() * 1000L; + long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); + VisibleMessage message = new VisibleMessage(); + message.setSentTimestamp(System.currentTimeMillis()); + message.setText(responseText.toString()); switch (replyMethod) { case GroupMessage: { - OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - threadId = MessageSender.send(context, reply, -1, false, null); + OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null); + try { + DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, threadId, false, null); + MessageSender.send(message, address); + } catch (MmsException e) { + Log.w(TAG, e); + } break; } case SecureMessage: { - OutgoingEncryptedMessage reply = new OutgoingEncryptedMessage(recipient, responseText.toString(), expiresIn); - threadId = MessageSender.send(context, reply, -1, false, null); + OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient); + DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null); + MessageSender.send(message, address); break; } default: diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index 401a6c7e27..77eee247cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; +import org.session.libsession.messaging.messages.signal.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import java.util.Comparator; @@ -70,6 +70,9 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Address address = Address.fromSerialized(senderPublicKey); Recipient recipient = Recipient.from(context, address, false); + + if (recipient.isBlocked()) return; + Optional groupInfo = Optional.absent(); if (content.getDataMessage().hasGroup()) { GroupContext groupContext = content.getDataMessage().getGroup(); @@ -103,17 +106,18 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM } @Override - public void startAnyExpiration(long messageID) { - MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(messageID); + public void startAnyExpiration(long timestamp, @NotNull String author) { + MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author); if (messageRecord != null) { boolean mms = messageRecord.isMms(); Recipient recipient = messageRecord.getRecipient(); + if (recipient.getExpireMessages() <= 0) return; if (mms) { - mmsDatabase.markExpireStarted(messageID); + mmsDatabase.markExpireStarted(messageRecord.getId()); } else { - smsDatabase.markExpireStarted(messageID); + smsDatabase.markExpireStarted(messageRecord.getId()); } - scheduleDeletion(messageID, mms, recipient.getExpireMessages()); + scheduleDeletion(messageRecord.getId(), mms, recipient.getExpireMessages() * 1000); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java index d657be284b..ada9bb2513 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/QuickResponseService.java @@ -7,11 +7,13 @@ import android.text.TextUtils; import android.widget.Toast; import network.loki.messenger.R; + +import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.threads.Address; import org.session.libsignal.utilities.logging.Log; import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.thoughtcrime.securesms.util.Rfc5724Uri; import java.net.URISyntaxException; @@ -47,13 +49,11 @@ public class QuickResponseService extends IntentService { number = URLDecoder.decode(number); } - Address address = Address.fromExternal(this, number); - Recipient recipient = Recipient.from(this, address, false); - int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); - long expiresIn = recipient.getExpireMessages() * 1000L; - if (!TextUtils.isEmpty(content)) { - MessageSender.send(this, new OutgoingTextMessage(recipient, content, expiresIn, subscriptionId), -1, false, null); + VisibleMessage message = new VisibleMessage(); + message.setText(content); + message.setSentTimestamp(System.currentTimeMillis()); + MessageSender.send(message, Address.fromExternal(this, number)); } } catch (URISyntaxException e) { Toast.makeText(this, R.string.QuickResponseService_problem_sending_message, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java deleted file mode 100644 index a1ff5dcd79..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.sms; - -import android.content.Context; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.session.libsession.messaging.threads.Address; -import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.PushGroupSendJob; -import org.thoughtcrime.securesms.jobs.PushMediaSendJob; -import org.thoughtcrime.securesms.jobs.PushTextSendJob; -import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.thoughtcrime.securesms.service.ExpiringMessageManager; -import org.session.libsession.utilities.TextSecurePreferences; - -public class MessageSender { - - private static final String TAG = MessageSender.class.getSimpleName(); - - public static long send(final Context context, - final OutgoingTextMessage message, - final long threadId, - final boolean forceSms, - final SmsDatabase.InsertListener insertListener) - { - SmsDatabase database = DatabaseFactory.getSmsDatabase(context); - Recipient recipient = message.getRecipient(); - - long allocatedThreadId; - - if (threadId == -1) { - allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - } else { - allocatedThreadId = threadId; - } - - long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener); - - sendTextMessage(context, recipient, forceSms, messageId); - - return allocatedThreadId; - } - - public static long send(final Context context, - final OutgoingMediaMessage message, - final long threadId, - final boolean forceSms, - final SmsDatabase.InsertListener insertListener) - { - try { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - - long allocatedThreadId; - - if (threadId == -1) { - allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType()); - } else { - allocatedThreadId = threadId; - } - - Recipient recipient = message.getRecipient(); - long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); - - sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn()); - return allocatedThreadId; - } catch (MmsException e) { - Log.w(TAG, e); - return threadId; - } - } - - public static void resend(Context context, MessageRecord messageRecord) { - long messageId = messageRecord.getId(); - boolean forceSms = messageRecord.isForcedSms(); - long expiresIn = messageRecord.getExpiresIn(); - Recipient recipient = messageRecord.getRecipient(); - - if (messageRecord.isMms()) { - sendMediaMessage(context, recipient, forceSms, messageId, expiresIn); - } else { - sendTextMessage(context, recipient, forceSms, messageId); - } - } - - private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, long expiresIn) - { - if (isLocalSelfSend(context, recipient, forceSms)) { - sendLocalMediaSelf(context, messageId); - } else if (isGroupPushSend(recipient)) { - sendGroupPush(context, recipient, messageId, null); - } else { - sendMediaPush(context, recipient, messageId); - } - } - - private static void sendTextMessage(Context context, Recipient recipient, - boolean forceSms, long messageId) - { - if (isLocalSelfSend(context, recipient, forceSms)) { - sendLocalTextSelf(context, messageId); - } else { - sendTextPush(context, recipient, messageId); - } - } - - private static void sendTextPush(Context context, Recipient recipient, long messageId) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); - } - - private static void sendMediaPush(Context context, Recipient recipient, long messageId) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); - } - - private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), filterAddress); - } - - private static boolean isGroupPushSend(Recipient recipient) { - return recipient.getAddress().isGroup() && - !recipient.getAddress().isMmsGroup(); - } - - private static boolean isLocalSelfSend(@NonNull Context context, @NonNull Recipient recipient, boolean forceSms) { - return recipient.isLocalNumber() && - !forceSms && - TextSecurePreferences.isPushRegistered(context); - } - - private static void sendLocalMediaSelf(Context context, long messageId) { - try { - ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); - AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); - MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); - MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); - OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId); - SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getSentTimeMillis()); - - for (Attachment attachment : message.getAttachments()) { - attachmentDatabase.markAttachmentUploaded(messageId, attachment); - } - - mmsDatabase.markAsSent(messageId, true); - mmsDatabase.markUnidentified(messageId, true); - - mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis()); - mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis()); - - if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { - mmsDatabase.markExpireStarted(messageId); - expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn()); - } - } catch (NoSuchMessageException | MmsException e) { - Log.w("Failed to update self-sent message.", e); - } - } - - private static void sendLocalTextSelf(Context context, long messageId) { - try { - ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); - SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); - SmsMessageRecord message = smsDatabase.getMessage(messageId); - SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getDateSent()); - - smsDatabase.markAsSent(messageId, true); - smsDatabase.markUnidentified(messageId, true); - - mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis()); - mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis()); - - if (message.getExpiresIn() > 0) { - smsDatabase.markExpireStarted(messageId); - expirationManager.scheduleDeletion(message.getId(), message.isMms(), message.getExpiresIn()); - } - } catch (NoSuchMessageException e) { - Log.w("Failed to update self-sent message.", e); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java deleted file mode 100644 index 6d64c7140e..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingEncryptedMessage.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.thoughtcrime.securesms.sms; - -import org.session.libsession.messaging.threads.recipients.Recipient; - -public class OutgoingEncryptedMessage extends OutgoingTextMessage { - - public OutgoingEncryptedMessage(Recipient recipient, String body, long expiresIn) { - super(recipient, body, expiresIn, -1); - } - - @Override - public boolean isSecureMessage() { - return true; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java index 100ede3702..40ee73983a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/TypingStatusRepository.java @@ -51,6 +51,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr return; } + if (Recipient.from(context, author, false).isBlocked()) { + return; + } + Set typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>()); Typist typist = new Typist(Recipient.from(context, author, false), device, threadId); @@ -76,6 +80,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr return; } + if (Recipient.from(context, author, false).isBlocked()) { + return; + } + Set typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>()); Typist typist = new Typist(Recipient.from(context, author, false), device, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/transport/RetryLaterException.java b/app/src/main/java/org/thoughtcrime/securesms/transport/RetryLaterException.java deleted file mode 100644 index 626bbafcdf..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/transport/RetryLaterException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.thoughtcrime.securesms.transport; - -public class RetryLaterException extends Exception { - public RetryLaterException() {} - - public RetryLaterException(Exception e) { - super(e); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/transport/UndeliverableMessageException.java b/app/src/main/java/org/thoughtcrime/securesms/transport/UndeliverableMessageException.java deleted file mode 100644 index 49e0f743dc..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/transport/UndeliverableMessageException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.transport; - -public class UndeliverableMessageException extends Exception { - - public UndeliverableMessageException(String detailMessage) { - super(detailMessage); - } - - public UndeliverableMessageException(Throwable throwable) { - super(throwable); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt index 8b8d899b72..236b37a8a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -7,35 +7,73 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.DocumentsContract -import org.session.libsignal.utilities.logging.Log import android.widget.Toast import androidx.annotation.WorkerThread import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import network.loki.messenger.R import org.greenrobot.eventbus.EventBus +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.libsignal.util.ByteUtil +import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.backup.BackupEvent import org.thoughtcrime.securesms.backup.BackupPassphrase +import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference import org.thoughtcrime.securesms.backup.FullBackupExporter import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider +import org.session.libsession.utilities.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.database.BackupFileRecord import org.thoughtcrime.securesms.service.LocalBackupListener -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.libsignal.util.ByteUtil import java.io.IOException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.* -import kotlin.jvm.Throws object BackupUtil { + private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences" private const val TAG = "BackupUtil" const val BACKUP_FILE_MIME_TYPE = "application/session-backup" const val BACKUP_PASSPHRASE_LENGTH = 30 + fun getBackupRecords(context: Context): List { + val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME + val preferences = context.getSharedPreferences(prefName, 0) + val prefList = LinkedList() + prefList.add(SharedPreference.newBuilder() + .setFile(prefName) + .setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF) + .setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null)) + .build()) + prefList.add(SharedPreference.newBuilder() + .setFile(prefName) + .setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF) + .setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null)) + .build()) + if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) { + prefList.add(SharedPreference.newBuilder() + .setFile(prefName) + .setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY) + .setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null)) + .build()) + } + if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) { + prefList.add(SharedPreference.newBuilder() + .setFile(prefName) + .setKey(IdentityKeyUtil.ED25519_SECRET_KEY) + .setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null)) + .build()) + } + prefList.add(SharedPreference.newBuilder() + .setFile(prefName) + .setKey(IdentityKeyUtil.LOKI_SEED) + .setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null)) + .build()) + return prefList + } + /** * Set app-wide configuration to enable the backups and schedule them. * @@ -91,7 +129,7 @@ object BackupUtil { @JvmStatic fun generateBackupPassphrase(): Array { val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) } - return Array(6) {i -> + return Array(6) { i -> String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000) } } diff --git a/app/src/main/res/layout/conversation_activity_attachment_editor_stub.xml b/app/src/main/res/layout/conversation_activity_attachment_editor_stub.xml index 3ee19aac0b..2367ee4d6b 100644 --- a/app/src/main/res/layout/conversation_activity_attachment_editor_stub.xml +++ b/app/src/main/res/layout/conversation_activity_attachment_editor_stub.xml @@ -13,13 +13,6 @@ android:layout_height="wrap_content" android:layout_gravity="center"> - - fun getMessageBodyFor(messageID: Long): String + fun getAttachmentIDsFor(messageID: Long): List + fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/Document.java b/libsession/src/main/java/org/session/libsession/database/documents/Document.java similarity index 66% rename from app/src/main/java/org/thoughtcrime/securesms/database/documents/Document.java rename to libsession/src/main/java/org/session/libsession/database/documents/Document.java index 2b226f66da..2167e0ee2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/Document.java +++ b/libsession/src/main/java/org/session/libsession/database/documents/Document.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.documents; +package org.session.libsession.database.documents; import java.util.List; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java b/libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatch.java similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java rename to libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatch.java index 075996f030..3140ba2bdf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java +++ b/libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatch.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.documents; +package org.session.libsession.database.documents; import org.session.libsignal.utilities.logging.Log; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatchList.java b/libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatchList.java similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatchList.java rename to libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatchList.java index eaceb4d93f..e270522de6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatchList.java +++ b/libsession/src/main/java/org/session/libsession/database/documents/IdentityKeyMismatchList.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.documents; +package org.session.libsession.database.documents; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java b/libsession/src/main/java/org/session/libsession/database/documents/NetworkFailure.java similarity index 93% rename from app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java rename to libsession/src/main/java/org/session/libsession/database/documents/NetworkFailure.java index ce16b50ff8..993cf6f832 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailure.java +++ b/libsession/src/main/java/org/session/libsession/database/documents/NetworkFailure.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.documents; +package org.session.libsession.database.documents; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailureList.java b/libsession/src/main/java/org/session/libsession/database/documents/NetworkFailureList.java similarity index 92% rename from app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailureList.java rename to libsession/src/main/java/org/session/libsession/database/documents/NetworkFailureList.java index 3347c42846..9fdea4d7d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/documents/NetworkFailureList.java +++ b/libsession/src/main/java/org/session/libsession/database/documents/NetworkFailureList.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.database.documents; +package org.session.libsession.database.documents; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index e3062f73ee..2c54c38387 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -59,6 +59,8 @@ interface StorageProtocol { fun getThreadID(openGroupID: String): String? fun getAllOpenGroups(): Map fun addOpenGroup(server: String, channel: Long) + fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) + fun getQuoteServerID(quoteID: Long, publicKey: String): Long? // Open Group Public Keys fun getOpenGroupPublicKey(server: String): String? @@ -94,10 +96,9 @@ interface StorageProtocol { fun persistAttachments(messageId: Long, attachments: List): List fun getMessageIdInDatabase(timestamp: Long, author: String): Long? - fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) - fun markAsSent(messageID: Long) - fun markUnidentified(messageID: Long) - fun setErrorMessage(messageID: Long, error: Exception) + fun markAsSent(timestamp: Long, author: String) + fun markUnidentified(timestamp: Long, author: String) + fun setErrorMessage(timestamp: Long, author: String, error: Exception) // Closed Groups fun getGroup(groupID: String): GroupRecord? @@ -112,9 +113,9 @@ interface StorageProtocol { fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, - name: String, members: Collection, admins: Collection) + name: String, members: Collection, admins: Collection, sentTimestamp: Long) fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, - members: Collection, admins: Collection, threadID: Long) + members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) fun isClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? diff --git a/libsession/src/main/java/org/session/libsession/messaging/avatars/ContactColorsLegacy.java b/libsession/src/main/java/org/session/libsession/messaging/avatars/ContactColorsLegacy.java deleted file mode 100644 index b605c24fcb..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/avatars/ContactColorsLegacy.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.session.libsession.messaging.avatars; - -import androidx.annotation.NonNull; - -import org.session.libsession.utilities.color.MaterialColor; - - -/** - * Used for migrating legacy colors to modern colors. For normal color generation, use - * {@link ContactColors}. - */ -public class ContactColorsLegacy { - - private static final String[] LEGACY_PALETTE = new String[] { - "red", - "pink", - "purple", - "deep_purple", - "indigo", - "blue", - "light_blue", - "cyan", - "teal", - "green", - "light_green", - "orange", - "deep_orange", - "amber", - "blue_grey" - }; - - public static MaterialColor generateFor(@NonNull String name) { - String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length]; - try { - return MaterialColor.fromSerialized(serialized); - } catch (MaterialColor.UnknownColorException e) { - return ContactColors.generateFor(name); - } - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt index e72e0c0a33..37230d79d8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt @@ -29,7 +29,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat * possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. */ public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5? - val server = "https://file.getsession.org" public val fileStorageBucketURL = "https://file-static.lokinet.org" // endregion diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 37eafe8b1b..f46a124ca8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -17,7 +17,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024 // Error - internal sealed class Error(val description: String) : Exception() { + internal sealed class Error(val description: String) : Exception(description) { object NoAttachment : Error("No such attachment.") } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index e9c8c7d01d..94b7a9d26a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -8,11 +8,15 @@ import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.DotNetAPI -import org.session.libsignal.utilities.logging.Log +import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream +import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream +import org.session.libsignal.service.internal.crypto.PaddingInputStream import org.session.libsignal.service.internal.push.PushAttachmentData import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory +import org.session.libsignal.utilities.ThreadUtils +import org.session.libsignal.utilities.logging.Log class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { @@ -21,14 +25,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess override var failureCount: Int = 0 // Error - internal sealed class Error(val description: String) : Exception() { + internal sealed class Error(val description: String) : Exception(description) { object NoAttachment : Error("No such attachment.") } // Settings override val maxFailureCount: Int = 20 companion object { - val TAG = AttachmentUploadJob::class.qualifiedName + val TAG = AttachmentUploadJob::class.simpleName val KEY: String = "AttachmentUploadJob" val maxFailureCount: Int = 20 @@ -41,46 +45,54 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } override fun execute() { - try { - val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID) - ?: return handleFailure(Error.NoAttachment) + ThreadUtils.queue { + try { + val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID) + ?: return@queue handleFailure(Error.NoAttachment) - val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID) - val server = openGroup?.server ?: FileServerAPI.server + var server = FileServerAPI.shared.server + var shouldEncrypt = true + val usePadding = false + val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID) + openGroup?.let { + server = it.server + shouldEncrypt = false + } - //TODO add some encryption stuff here - val isEncryptionRequired = false - //val isEncryptionRequired = (server == FileServerAPI.server) + val attachmentKey = Util.getSecretBytes(64) + val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length + val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream + val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length - val attachmentKey = Util.getSecretBytes(64) - val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory() - val ciphertextLength = attachmentStream.length + val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory() + val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener) - val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener) + val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData) + handleSuccess(attachment, attachmentKey, uploadResult) - FileServerAPI.shared.uploadAttachment(server, attachmentData) - - } catch (e: java.lang.Exception) { - if (e is Error && e == Error.NoAttachment) { - this.handlePermanentFailure(e) - } else if (e is DotNetAPI.Error && !e.isRetryable) { - this.handlePermanentFailure(e) - } else { - this.handleFailure(e) + } catch (e: java.lang.Exception) { + if (e is Error && e == Error.NoAttachment) { + this.handlePermanentFailure(e) + } else if (e is DotNetAPI.Error && !e.isRetryable) { + this.handlePermanentFailure(e) + } else { + this.handleFailure(e) + } } } } - private fun handleSuccess() { + private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { Log.w(TAG, "Attachment uploaded successfully.") delegate?.handleJobSucceeded(this) + MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) - //TODO interaction stuff, not sure how to deal with that } private fun handlePermanentFailure(e: Exception) { Log.w(TAG, "Attachment upload failed permanently due to error: $this.") delegate?.handleJobFailedPermanently(this, e) + MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID) failAssociatedMessageSendJob(e) } @@ -95,7 +107,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess private fun failAssociatedMessageSendJob(e: Exception) { val storage = MessagingConfiguration.shared.storage val messageSendJob = storage.getMessageSendJob(messageSendJobID) - MessageSender.handleFailedMessageSend(this.message!!, e) + MessageSender.handleFailedMessageSend(this.message, e) if (messageSendJob != null) { storage.markJobAsFailed(messageSendJob) } @@ -119,7 +131,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } override fun getFactoryKey(): String { - return AttachmentDownloadJob.KEY + return KEY } class Factory: Job.Factory { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index a69e9162ea..d11bc2d3d2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -15,7 +15,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val // Settings override val maxFailureCount: Int = 10 companion object { - val TAG = MessageReceiveJob::class.qualifiedName + + val TAG = MessageReceiveJob::class.simpleName val KEY: String = "MessageReceiveJob" //keys used for database storage purpose @@ -75,7 +76,7 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val } override fun getFactoryKey(): String { - return AttachmentDownloadJob.KEY + return KEY } class Factory: Job.Factory { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index e00e0a43a5..1a630b55ae 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -19,7 +19,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { // Settings override val maxFailureCount: Int = 10 companion object { - val TAG = MessageSendJob::class.qualifiedName + val TAG = MessageSendJob::class.simpleName val KEY: String = "MessageSendJob" //keys used for database storage purpose @@ -32,13 +32,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { val message = message as? VisibleMessage message?.let { if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted - val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull() - val attachmentsToUpload = attachments.filter { !it.isUploaded } + val attachmentIDs = mutableListOf() + attachmentIDs.addAll(message.attachmentIDs) + message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } } + message.linkPreview?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } } + val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) } + val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() } attachmentsToUpload.forEach { - if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) { + if (MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) { // Wait for it to finish } else { - val job = AttachmentUploadJob(it.attachmentId, message.threadID!!.toString(), message, id!!) + val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!) JobQueue.shared.add(job) } } @@ -79,21 +83,21 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { //serialize Message and Destination properties val kryo = Kryo() kryo.isRegistrationRequired = false - val serializedMessage = ByteArray(4096) - val serializedDestination = ByteArray(4096) - var output = Output(serializedMessage) - kryo.writeObject(output, message) + val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message + kryo.writeClassAndObject(output, message) output.close() - output = Output(serializedDestination) - kryo.writeObject(output, destination) + val serializedMessage = output.toBytes() + output.clear() + kryo.writeClassAndObject(output, destination) output.close() + val serializedDestination = output.toBytes() return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage) .putByteArray(KEY_DESTINATION, serializedDestination) .build(); } override fun getFactoryKey(): String { - return AttachmentDownloadJob.KEY + return KEY } class Factory: Job.Factory { @@ -103,10 +107,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { //deserialize Message and Destination properties val kryo = Kryo() var input = Input(serializedMessage) - val message: Message = kryo.readObject(input, Message::class.java) + val message = kryo.readClassAndObject(input) as Message input.close() input = Input(serializedDestination) - val destination: Destination = kryo.readObject(input, Destination::class.java) + val destination = kryo.readClassAndObject(input) as Destination input.close() return MessageSendJob(message, destination) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 3c2665b636..f78425d6e1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -76,7 +76,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { } override fun getFactoryKey(): String { - return AttachmentDownloadJob.KEY + return KEY } class Factory: Job.Factory { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index 03fb189203..ef389b08b0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -3,26 +3,39 @@ package org.session.libsession.messaging.messages import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.threads.Address import org.session.libsession.utilities.GroupUtil +import org.session.libsignal.service.loki.utilities.toHexString sealed class Destination { - class Contact(val publicKey: String) : Destination() - class ClosedGroup(val groupPublicKey: String) : Destination() - class OpenGroup(val channel: Long, val server: String) : Destination() + class Contact(var publicKey: String) : Destination() { + internal constructor(): this("") + } + class ClosedGroup(var groupPublicKey: String) : Destination() { + internal constructor(): this("") + } + class OpenGroup(var channel: Long, var server: String) : Destination() { + internal constructor(): this(0, "") + } companion object { fun from(address: Address): Destination { - if (address.isContact) { - return Contact(address.contactIdentifier()) - } else if (address.isClosedGroup) { - val groupID = address.contactIdentifier() - val groupPublicKey = GroupUtil.getDecodedGroupID(groupID) - return ClosedGroup(groupPublicKey) - } else if (address.isOpenGroup) { - val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!! - return OpenGroup(openGroup.channel, openGroup.server) - } else { - throw Exception("TODO: Handle legacy closed groups.") + return when { + address.isContact -> { + Contact(address.contactIdentifier()) + } + address.isClosedGroup -> { + val groupID = address.toGroupString() + val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() + ClosedGroup(groupPublicKey) + } + address.isOpenGroup -> { + val threadID = MessagingConfiguration.shared.storage.getThreadID(address.contactIdentifier())!! + val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)!! + OpenGroup(openGroup.channel, openGroup.server) + } + else -> { + throw Exception("TODO: Handle legacy closed groups.") + } } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index 2cf27ed8ed..6f2a916351 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -1,5 +1,7 @@ package org.session.libsession.messaging.messages +import com.google.protobuf.ByteString +import org.session.libsession.utilities.GroupUtil import org.session.libsignal.service.internal.push.SignalServiceProtos abstract class Message { @@ -18,11 +20,23 @@ abstract class Message { // validation open fun isValid(): Boolean { - sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false - receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false + sentTimestamp?.let { + if (it <= 0) return false + } + receivedTimestamp?.let { + if (it <= 0) return false + } return sender != null && recipient != null } abstract fun toProto(): SignalServiceProtos.Content? + fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) { + val groupProto = SignalServiceProtos.GroupContext.newBuilder() + val groupID = GroupUtil.doubleEncodeGroupID(recipient!!) + groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)) + groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER + dataMessage.group = groupProto.build() + } + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt index 70c75ddce3..b7f5667ad9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt @@ -1,12 +1,17 @@ package org.session.libsession.messaging.messages.control import com.google.protobuf.ByteString +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.recipients.Recipient +import org.session.libsession.utilities.GroupUtil import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.utilities.Hex @@ -25,18 +30,30 @@ class ClosedGroupControlMessage() : ControlMessage() { // Kind enum sealed class Kind { - class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List, val admins: List) : Kind() + class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List, var admins: List) : Kind() { + internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf()) + } /// - Note: Deprecated in favor of more explicit group updates. - class Update(val name: String, val members: List) : Kind() + class Update(var name: String, var members: List) : Kind() { + internal constructor(): this("", listOf()) + } /// An encryption key pair encrypted for each member individually. /// /// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group). - class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection) : Kind() - class NameChange(val name: String) : Kind() - class MembersAdded(val members: List) : Kind() - class MembersRemoved( val members: List) : Kind() - object MemberLeft : Kind() - object EncryptionKeyPairRequest: Kind() + class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection) : Kind() { + internal constructor(): this(null, listOf()) + } + class NameChange(var name: String) : Kind() { + internal constructor(): this("") + } + class MembersAdded(var members: List) : Kind() { + internal constructor(): this(listOf()) + } + class MembersRemoved(var members: List) : Kind() { + internal constructor(): this(listOf()) + } + class MemberLeft() : Kind() + class EncryptionKeyPairRequest(): Kind() val description: String = when(this) { @@ -46,8 +63,8 @@ class ClosedGroupControlMessage() : ControlMessage() { is NameChange -> "nameChange" is MembersAdded -> "membersAdded" is MembersRemoved -> "membersRemoved" - MemberLeft -> "memberLeft" - EncryptionKeyPairRequest -> "encryptionKeyPairRequest" + is MemberLeft -> "memberLeft" + is EncryptionKeyPairRequest -> "encryptionKeyPairRequest" } } @@ -91,10 +108,10 @@ class ClosedGroupControlMessage() : ControlMessage() { kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList) } DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> { - kind = Kind.MemberLeft + kind = Kind.MemberLeft() } DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR_REQUEST -> { - kind = Kind.EncryptionKeyPairRequest + kind = Kind.EncryptionKeyPairRequest() } } return ClosedGroupControlMessage(kind) @@ -112,8 +129,8 @@ class ClosedGroupControlMessage() : ControlMessage() { val kind = kind ?: return false return when(kind) { is Kind.New -> { - !kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair.publicKey != null - && kind.encryptionKeyPair.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty() + !kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null + && kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty() } is Kind.Update -> kind.name.isNotEmpty() is Kind.EncryptionKeyPair -> true @@ -138,16 +155,10 @@ class ClosedGroupControlMessage() : ControlMessage() { closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW closedGroupControlMessage.publicKey = kind.publicKey closedGroupControlMessage.name = kind.name - val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder() - encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize()) - encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize()) - - try { - closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build() - } catch (e: Exception) { - Log.w(TAG, "Couldn't construct closed group update proto from: $this") - return null - } + val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder() + encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded()) + encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize()) + closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build() closedGroupControlMessage.addAllMembers(kind.members) closedGroupControlMessage.addAllAdmins(kind.admins) } @@ -184,6 +195,11 @@ class ClosedGroupControlMessage() : ControlMessage() { val dataMessageProto = DataMessage.newBuilder() dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build() // Group context + setGroupContext(dataMessageProto) + // Expiration timer + // TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation + // if it receives a message without the current expiration timer value attached to it... + dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages contentProto.dataMessage = dataMessageProto.build() return contentProto.build() } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt index ecb56edf02..fe06cc6c01 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -14,11 +14,13 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.utilities.Hex -class ConfigurationMessage(val closedGroups: List, val openGroups: List, val contacts: List, val displayName: String, val profilePicture: String?, val profileKey: ByteArray): ControlMessage() { +class ConfigurationMessage(var closedGroups: List, var openGroups: List, var contacts: List, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() { - class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List, val admins: List) { + class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List, var admins: List) { val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty() + internal constructor(): this("", "", null, listOf(), listOf()) + override fun toString(): String { return name } @@ -30,7 +32,7 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: val name = proto.name val encryptionKeyPairAsProto = proto.encryptionKeyPair val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), - DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) + DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) val members = proto.membersList.map { it.toByteArray().toHexString() } val admins = proto.adminsList.map { it.toByteArray().toHexString() } return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins) @@ -42,8 +44,8 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey)) result.name = name val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder() - encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) - encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize()) + encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded()) + encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize()) result.encryptionKeyPair = encryptionKeyPairAsProto.build() result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }) @@ -51,7 +53,10 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: } } - class Contact(val publicKey: String, val name: String, val profilePicture: String?, val profileKey: ByteArray?) { + class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) { + + internal constructor(): this("", "", null, null) + companion object { fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? { if (!proto.hasName() || !proto.hasProfileKey()) return null @@ -128,6 +133,8 @@ class ConfigurationMessage(val closedGroups: List, val openGroups: } } + internal constructor(): this(listOf(), listOf(), listOf(), "", null, byteArrayOf()) + override fun toProto(): SignalServiceProtos.Content? { val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder() configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() }) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 09b4f4e3d6..2571b103d4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -1,10 +1,13 @@ package org.session.libsession.messaging.messages.control +import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos class ExpirationTimerUpdate() : ControlMessage() { + var syncTarget: String? = null var duration: Int? = 0 companion object { @@ -12,7 +15,7 @@ class ExpirationTimerUpdate() : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { val dataMessageProto = proto.dataMessage ?: return null - val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 //TODO validate that 'and' operator equivalent to Swift '&' + val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 if (!isExpirationTimerUpdate) return null val duration = dataMessageProto.expireTimer return ExpirationTimerUpdate(duration) @@ -39,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() { val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE dataMessageProto.expireTimer = duration + syncTarget?.let { dataMessageProto.syncTarget = it } + // Group context + if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) { + try { + setGroupContext(dataMessageProto) + } catch(e: Exception) { + Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this") + return null + } + } val contentProto = SignalServiceProtos.Content.newBuilder() try { contentProto.dataMessage = dataMessageProto.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index b430f09c41..77818f14fa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -5,7 +5,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class ReadReceipt() : ControlMessage() { - var timestamps: LongArray? = null + var timestamps: List? = null companion object { const val TAG = "ReadReceipt" @@ -15,12 +15,12 @@ class ReadReceipt() : ControlMessage() { if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null val timestamps = receiptProto.timestampList if (timestamps.isEmpty()) return null - return ReadReceipt(timestamps = timestamps.toLongArray()) + return ReadReceipt(timestamps = timestamps) } } //constructor - internal constructor(timestamps: LongArray?) : this() { + internal constructor(timestamps: List?) : this() { this.timestamps = timestamps } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java similarity index 81% rename from app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java index 66586a68b9..f5a63d4ac5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingEncryptedMessage.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.sms; +package org.session.libsession.messaging.messages.signal; public class IncomingEncryptedMessage extends IncomingTextMessage { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java similarity index 91% rename from app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java index af17d4b963..b0a7de08ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingGroupMessage.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.sms; +package org.session.libsession.messaging.messages.signal; import static org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java similarity index 98% rename from app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java index 7c1451056f..aa330ce227 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingMediaMessage.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.mms; +package org.session.libsession.messaging.messages.signal; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java index 0acf91c242..31be71cf3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/IncomingTextMessage.java @@ -1,20 +1,14 @@ -package org.thoughtcrime.securesms.sms; +package org.session.libsession.messaging.messages.signal; -import android.content.Context; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.telephony.SmsMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.threads.Address; import org.session.libsession.utilities.GroupUtil; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.api.messages.SignalServiceGroup; -import org.session.libsignal.service.api.push.SignalServiceAddress; - -import java.util.List; public class IncomingTextMessage implements Parcelable { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java similarity index 51% rename from app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java index 91fcab901d..77996ba11e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingExpirationUpdateMessage.java @@ -1,7 +1,8 @@ -package org.thoughtcrime.securesms.mms; +package org.session.libsession.messaging.messages.signal; +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; -import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.threads.recipients.Recipient; import java.util.Collections; @@ -11,10 +12,15 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) { super(recipient, "", new LinkedList(), sentTimeMillis, - ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(), + DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(), Collections.emptyList()); } + public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message, + Recipient recipient) { + return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000); + } + @Override public boolean isExpirationUpdate() { return true; diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java similarity index 89% rename from app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java index cf63d5a058..45f6531608 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java @@ -1,9 +1,9 @@ -package org.thoughtcrime.securesms.mms; +package org.session.libsession.messaging.messages.signal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.session.libsession.messaging.threads.DistributionTypes; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; @@ -32,7 +32,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { throws IOException { super(recipient, encodedGroupContext, avatar, sentTimeMillis, - ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews); + DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews); this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); } @@ -48,7 +48,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { super(recipient, Base64.encodeBytes(group.toByteArray()), new LinkedList() {{if (avatar != null) add(avatar);}}, System.currentTimeMillis(), - ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); + DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); this.group = group; } @@ -65,7 +65,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { super(recipient, Base64.encodeBytes(group.toByteArray()), new LinkedList() {{if (avatar != null) add(avatar);}}, sentTime, - ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); + DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); this.group = group; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java similarity index 72% rename from app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java index fb22876181..8d818d956c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingMediaMessage.java @@ -1,13 +1,12 @@ -package org.thoughtcrime.securesms.mms; +package org.session.libsession.messaging.messages.signal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.session.libsession.messaging.threads.DistributionTypes; +import org.session.libsession.database.documents.IdentityKeyMismatch; +import org.session.libsession.database.documents.NetworkFailure; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; @@ -59,20 +58,6 @@ public class OutgoingMediaMessage { this.identityKeyMismatches.addAll(identityKeyMismatches); } - public OutgoingMediaMessage(Recipient recipient, SlideDeck slideDeck, String message, - long sentTimeMillis, int subscriptionId, long expiresIn, - int distributionType, @Nullable QuoteModel outgoingQuote, - @NonNull List contacts, - @NonNull List linkPreviews) - { - this(recipient, - buildMessage(slideDeck, message), - slideDeck.asAttachments(), - sentTimeMillis, subscriptionId, - expiresIn, distributionType, outgoingQuote, - contacts, linkPreviews, new LinkedList<>(), new LinkedList<>()); - } - public OutgoingMediaMessage(OutgoingMediaMessage that) { this.recipient = that.getRecipient(); this.body = that.body; @@ -93,11 +78,15 @@ public class OutgoingMediaMessage { Recipient recipient, List attachments, @Nullable QuoteModel outgoingQuote, - @NonNull List linkPreviews) + @Nullable LinkPreview linkPreview) { + List previews = Collections.emptyList(); + if (linkPreview != null) { + previews = Collections.singletonList(linkPreview); + } return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1, - recipient.getExpireMessages() * 1000, ThreadDatabase.DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(), - linkPreviews, Collections.emptyList(), Collections.emptyList()); + recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(), + previews, Collections.emptyList(), Collections.emptyList()); } public Recipient getRecipient() { @@ -112,12 +101,8 @@ public class OutgoingMediaMessage { return attachments; } - public int getDistributionType() { - return distributionType; - } - public boolean isSecure() { - return false; + return true; } public boolean isGroup() { @@ -152,19 +137,4 @@ public class OutgoingMediaMessage { return linkPreviews; } - public @NonNull List getNetworkFailures() { - return networkFailures; - } - - public @NonNull List getIdentityKeyMismatches() { - return identityKeyMismatches; - } - - private static String buildMessage(SlideDeck slideDeck, String message) { - if (!TextUtils.isEmpty(message)) { - return message; - } - return ""; - } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java index 527f7bdd0e..8b5e7ddef0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingSecureMediaMessage.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.mms; +package org.session.libsession.messaging.messages.signal; import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java similarity index 69% rename from app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java rename to libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java index dca0af749e..0d3cd92aa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingTextMessage.java @@ -1,7 +1,6 @@ -package org.thoughtcrime.securesms.sms; +package org.session.libsession.messaging.messages.signal; import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.session.libsession.messaging.threads.recipients.Recipient; public class OutgoingTextMessage { @@ -39,14 +38,6 @@ public class OutgoingTextMessage { } public boolean isSecureMessage() { - return false; - } - - public static OutgoingTextMessage from(SmsMessageRecord record) { - if (record.isSecure()) { - return new OutgoingEncryptedMessage(record.getRecipient(), record.getBody(), record.getExpiresIn()); - } else { - return new OutgoingTextMessage(record.getRecipient(), record.getBody(), record.getExpiresIn(), record.getSubscriptionId()); - } + return true; } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt index df292c65f8..edb9d7767b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -2,8 +2,11 @@ package org.session.libsession.messaging.messages.visible import android.util.Size import android.webkit.MimeTypeMap -import org.session.libsession.database.MessageDataProvider +import com.google.protobuf.ByteString +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer +import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment import org.session.libsignal.service.internal.push.SignalServiceProtos import java.io.File @@ -32,7 +35,7 @@ class Attachment { result.contentType = proto.contentType ?: inferContentType() result.key = proto.key.toByteArray() result.digest = proto.digest.toByteArray() - val kind: Kind = if (proto.hasFlags() && (proto.flags and SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { //TODO validate that 'and' operator = swift '&' + val kind: Kind = if (proto.hasFlags() && proto.flags.and(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { Kind.VOICE_MESSAGE } else { Kind.GENERIC @@ -42,13 +45,42 @@ class Attachment { val size: Size = if (proto.hasWidth() && proto.width > 0 && proto.hasHeight() && proto.height > 0) { Size(proto.width, proto.height) } else { - Size(0,0) //TODO check that it's equivalent to swift: CGSize.zero + Size(0,0) } result.size = size result.sizeInBytes = if (proto.size > 0) proto.size else null result. url = proto.url return result } + + fun createAttachmentPointer(attachment: SignalServiceAttachmentPointer): SignalServiceProtos.AttachmentPointer? { + val builder = SignalServiceProtos.AttachmentPointer.newBuilder() + .setContentType(attachment.contentType) + .setId(attachment.id) + .setKey(ByteString.copyFrom(attachment.key)) + .setDigest(ByteString.copyFrom(attachment.digest.get())) + .setSize(attachment.size.get()) + .setUrl(attachment.url) + if (attachment.fileName.isPresent) { + builder.fileName = attachment.fileName.get() + } + if (attachment.preview.isPresent) { + builder.thumbnail = ByteString.copyFrom(attachment.preview.get()) + } + if (attachment.width > 0) { + builder.width = attachment.width + } + if (attachment.height > 0) { + builder.height = attachment.height + } + if (attachment.voiceNote) { + builder.flags = SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE + } + if (attachment.caption.isPresent) { + builder.caption = attachment.caption.get() + } + return builder.build() + } } enum class Kind { @@ -66,8 +98,9 @@ class Attachment { TODO("Not implemented") } - fun toDatabaseAttachment(): org.session.libsession.messaging.sending_receiving.attachments.Attachment { - return DatabaseAttachment(null, 0, true, true, contentType, 0, + fun toSignalAttachment(): SignalAttachment? { + if (!isValid()) return null + return DatabaseAttachment(null, 0, false, false, contentType, 0, sizeInBytes?.toLong() ?: 0, fileName, null, key.toString(), null, digest, null, kind == Kind.VOICE_MESSAGE, size?.width ?: 0, size?.height ?: 0, false, caption, url) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index c62d0685bb..ddfb397173 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.messages.visible import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreiview import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -18,6 +19,14 @@ class LinkPreview() { val url = proto.url return LinkPreview(title, url, null) } + + fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? { + return if (signalLinkPreview == null) { + null + } else { + LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId) + } + } } //constructor @@ -44,8 +53,10 @@ class LinkPreview() { title?.let { linkPreviewProto.title = title } val attachmentID = attachmentID attachmentID?.let { - val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID) - attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() } + MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let { + val attachmentProto = Attachment.createAttachmentPointer(it) + linkPreviewProto.image = attachmentProto + } } // Build try { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 71f76a09e4..e1255c6ef0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -2,6 +2,8 @@ package org.session.libsession.messaging.messages.visible import com.goterl.lazycode.lazysodium.BuildConfig import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -21,6 +23,15 @@ class Quote() { val text = proto.text return Quote(timestamp, publicKey, text, null) } + + fun from(signalQuote: SignalQuote?): Quote? { + return if (signalQuote == null) { + null + } else { + val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId + Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID) + } + } } //constructor @@ -58,13 +69,13 @@ class Quote() { } private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) { - val attachmentID = attachmentID ?: return - val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID) - if (attachmentProto == null) { + if (attachmentID == null) return + val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!) + if (attachment == null) { Log.w(TAG, "Ignoring invalid attachment for quoted message.") return } - if (!attachmentProto.isUploaded) { + if (attachment.url.isNullOrEmpty()) { if (BuildConfig.DEBUG) { //TODO equivalent to iOS's preconditionFailure Log.d(TAG,"Sending a message before all associated attachments have been uploaded.") @@ -72,10 +83,9 @@ class Quote() { } } val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() - quotedAttachmentProto.contentType = attachmentProto.contentType - val fileName = attachmentProto.fileName?.get() - fileName?.let { quotedAttachmentProto.fileName = fileName } - quotedAttachmentProto.thumbnail = attachmentProto.toProto() + quotedAttachmentProto.contentType = attachment.contentType + if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get() + quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment) try { quoteProto.addAttachments(quotedAttachmentProto.build()) } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index 0f5f283c73..edb908e35c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -1,12 +1,15 @@ package org.session.libsession.messaging.messages.visible import com.goterl.lazycode.lazysodium.BuildConfig - import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Message - -import org.session.libsignal.utilities.logging.Log +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.session.libsession.messaging.threads.Address +import org.session.libsession.messaging.threads.recipients.Recipient +import org.session.libsession.utilities.GroupUtil import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.utilities.logging.Log +import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment class VisibleMessage : Message() { @@ -46,6 +49,14 @@ class VisibleMessage : Message() { } } + fun addSignalAttachments(signalAttachments: List) { + val attachmentIDs = signalAttachments.map { + val databaseAttachment = it as DatabaseAttachment + databaseAttachment.attachmentId.rowId + } + this.attachmentIDs = attachmentIDs as ArrayList + } + fun isMediaMessage(): Boolean { return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || contact != null } @@ -61,7 +72,6 @@ class VisibleMessage : Message() { override fun toProto(): SignalServiceProtos.Content? { val proto = SignalServiceProtos.Content.newBuilder() - var attachmentIDs = this.attachmentIDs val dataMessage: SignalServiceProtos.DataMessage.Builder // Profile val profile = profile @@ -74,44 +84,49 @@ class VisibleMessage : Message() { // Text text?.let { dataMessage.body = text } // Quote - val quotedAttachmentID = quote?.attachmentID - quotedAttachmentID?.let { - val index = attachmentIDs.indexOf(quotedAttachmentID) - if (index >= 0) { attachmentIDs.removeAt(index) } - } - val quote = quote quote?.let { - val quoteProto = quote.toProto() + val quoteProto = it.toProto() if (quoteProto != null) dataMessage.quote = quoteProto } //Link preview - val linkPreviewAttachmentID = linkPreview?.attachmentID - linkPreviewAttachmentID?.let { - val index = attachmentIDs.indexOf(quotedAttachmentID) - if (index >= 0) { attachmentIDs.removeAt(index) } - } - val linkPreview = linkPreview linkPreview?.let { - val linkPreviewProto = linkPreview.toProto() + val linkPreviewProto = it.toProto() linkPreviewProto?.let { dataMessage.addAllPreview(listOf(linkPreviewProto)) } } //Attachments - val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(it) } - if (!attachments.all { it.isUploaded }) { + val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) } + if (!attachments.all { !it.url.isNullOrEmpty() }) { if (BuildConfig.DEBUG) { //TODO equivalent to iOS's preconditionFailure - Log.d(TAG,"Sending a message before all associated attachments have been uploaded.") + Log.d(TAG, "Sending a message before all associated attachments have been uploaded.") + } + } + val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) } + dataMessage.addAllAttachments(attachmentPointers) + // TODO Contact + // Expiration timer + // TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation + // if it receives a message without the current expiration timer value attached to it... + val storage = MessagingConfiguration.shared.storage + val context = MessagingConfiguration.shared.context + val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages + else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages + dataMessage.expireTimer = expiration + // Group context + if (storage.isClosedGroup(recipient!!)) { + try { + setGroupContext(dataMessage) + } catch(e: Exception) { + Log.w(TAG, "Couldn't construct visible message proto from: $this") + return null } } - val attachmentProtos = attachments.mapNotNull { it.toProto() } - dataMessage.addAllAttachments(attachmentProtos) // Sync target if (syncTarget != null) { dataMessage.syncTarget = syncTarget } - // TODO Contact // Build try { proto.dataMessage = dataMessage.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt index 61f1ad1df1..c8c53dc6bd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt @@ -41,6 +41,7 @@ object OpenGroupAPI: DotNetAPI() { return listOf() // Don't auto-join any open groups right now } + @JvmStatic fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean { if (moderators[server] != null && moderators[server]!![channel] != null) { return moderators[server]!![channel]!!.contains(hexEncodedPublicKey) @@ -113,7 +114,7 @@ object OpenGroupAPI: DotNetAPI() { val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong() val contentType = attachmentAsJSON["contentType"] as String val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt() - val fileName = attachmentAsJSON["fileName"] as String + val fileName = attachmentAsJSON["fileName"] as? String val flags = 0 val url = attachmentAsJSON["url"] as String val caption = attachmentAsJSON["caption"] as? String @@ -209,8 +210,7 @@ object OpenGroupAPI: DotNetAPI() { format.timeZone = TimeZone.getTimeZone("GMT") val dateAsString = data["created_at"] as String val timestamp = format.parse(dateAsString).time - @Suppress("NAME_SHADOWING") val message = OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp) - message + OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp) } catch (exception: Exception) { Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.") throw exception @@ -238,6 +238,7 @@ object OpenGroupAPI: DotNetAPI() { } } + @JvmStatic fun deleteMessages(messageServerIDs: List, channel: Long, server: String, isSentByUser: Boolean): Promise, Exception> { return retryIfNeeded(maxRetryCount) { val isModerationRequest = !isSentByUser @@ -338,6 +339,7 @@ object OpenGroupAPI: DotNetAPI() { } } + @JvmStatic fun ban(publicKey: String, server: String): Promise { return retryIfNeeded(maxRetryCount) { execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then { diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt index 01b4b73a70..4205779058 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt @@ -26,7 +26,6 @@ data class OpenGroupMessage( fun from(message: VisibleMessage, server: String): OpenGroupMessage? { val storage = MessagingConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() ?: return null - var attachmentIDs = message.attachmentIDs // Validation if (!message.isValid()) { return null } // Should be valid at this point // Quote @@ -34,10 +33,8 @@ data class OpenGroupMessage( val quote = message.quote if (quote != null && quote.isValid()) { val quotedMessageBody = quote.text ?: quote.timestamp!!.toString() - val quotedAttachmentID = quote.attachmentID - if (quotedAttachmentID != null) { attachmentIDs.remove(quotedAttachmentID) } - // FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set... - Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, null) + val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!) + Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID) } else { null } @@ -51,18 +48,18 @@ data class OpenGroupMessage( linkPreview?.let { if (!linkPreview.isValid()) { return@let } val attachmentID = linkPreview.attachmentID ?: return@let - val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(attachmentID) ?: return@let + val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let val openGroupLinkPreview = Attachment( Attachment.Kind.LinkPreview, server, attachment.id, attachment.contentType!!, attachment.size.get(), - attachment.fileName.get(), + attachment.fileName.orNull(), 0, attachment.width, attachment.height, - attachment.caption.get(), + attachment.caption.orNull(), attachment.url, linkPreview.url, linkPreview.title) @@ -70,18 +67,18 @@ data class OpenGroupMessage( } // Attachments val attachments = message.attachmentIDs.mapNotNull { - val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(it) ?: return@mapNotNull null + val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null return@mapNotNull Attachment( Attachment.Kind.Attachment, server, attachment.id, attachment.contentType!!, - attachment.size.get(), - attachment.fileName.get(), + attachment.size.orNull(), + attachment.fileName.orNull() ?: "", 0, attachment.width, attachment.height, - attachment.caption.get(), + attachment.caption.orNull(), attachment.url, null, null) @@ -121,7 +118,7 @@ data class OpenGroupMessage( val serverID: Long, val contentType: String, val size: Int, - val fileName: String, + val fileName: String?, val flags: Int, val width: Int, val height: Int, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 27e03fbfc9..d56aa6d83b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -11,7 +11,7 @@ object MessageReceiver { private val lastEncryptionKeyPairRequest = mutableMapOf() - internal sealed class Error(val description: String) : Exception() { + internal sealed class Error(val description: String) : Exception(description) { object DuplicateMessage: Error("Duplicate message.") object InvalidMessage: Error("Invalid message.") object UnknownMessage: Error("Unknown message type.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 530badd329..c1bfee205f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -50,7 +50,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content, private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) { val context = MessagingConfiguration.shared.context - SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!.asList(), message.receivedTimestamp!!) + SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!) } private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) { @@ -110,7 +110,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() for (closeGroup in message.closedGroups) { if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue - handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) + handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } for (openGroup in message.openGroups) { @@ -212,8 +212,8 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message) is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message) is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message) - ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message) - ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message) + is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message) + is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message) } } @@ -222,11 +222,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess val groupPublicKey = kind.publicKey.toByteArray().toHexString() val members = kind.members.map { it.toByteArray().toHexString() } val admins = kind.admins.map { it.toByteArray().toHexString() } - handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!) + handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!) } // Parameter @sender:String is just for inserting incoming info message -private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List, formationTimestamp: Long) { +private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List, formationTimestamp: Long) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage // Create the group @@ -239,7 +239,7 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) // Notify the user - storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) } storage.setProfileSharing(Address.fromSerialized(groupID), true) // Add the group to the user's set of public keys to poll for @@ -297,7 +297,7 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl val wasSenderRemoved = !members.contains(senderPublicKey) val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE - storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!) } private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) { @@ -315,7 +315,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr return } if (!group.members.map { it.toString() }.contains(senderPublicKey)) { - android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.") + Log.d("Loki", "Ignoring closed group encryption key pair from non-member.") return } // Find our wrapper and decrypt it if possible @@ -356,12 +356,13 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon val name = kind.name storage.updateTitle(groupID, name) - storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!) } private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage + val userPublicKey = storage.getUserPublicKey()!! val senderPublicKey = message.sender ?: return val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return val groupPublicKey = message.groupPublicKey ?: return @@ -370,9 +371,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo Log.d("Loki", "Ignoring closed group info message for nonexistent group.") return } - if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { - return - } + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } val name = group.title // Check common group update logic val members = group.members.map { it.serialize() } @@ -381,14 +380,25 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo val updateMembers = kind.members.map { it.toByteArray().toHexString() } val newMembers = members + updateMembers storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - // Send the latest encryption key pair to the added members if the current user is the admin of the group - val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!) - if (isCurrentUserAdmin) { - for (member in updateMembers) { - MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey) + if (userPublicKey == senderPublicKey) { + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!) + } else { + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!) + } + if (userPublicKey in admins) { + // send current encryption key to the latest added members + val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + if (encryptionKeyPair == null) { + android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.") + } else { + for (user in updateMembers) { + MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false) + } } } - storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!) } private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) { @@ -437,7 +447,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE - storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!) } private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) { @@ -472,7 +482,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList) } } - storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) + storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!) } private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) { @@ -491,7 +501,13 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C return } if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } - MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey) + val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + if (encryptionKeyPair == null) { + Log.d("Loki", "Couldn't get encryption key pair for closed group.") + } else { + MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false) + } } private fun isValidGroupUpdate(group: GroupRecord, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 044683f155..7344b3f17c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,37 +1,41 @@ package org.session.libsession.messaging.sending_receiving -import android.util.Size import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.NotifyPNServerJob import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage -import org.session.libsession.messaging.messages.visible.Attachment -import org.session.libsession.messaging.messages.visible.Profile -import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.visible.* import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.opengroups.OpenGroupMessage +import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeConfiguration import org.session.libsession.snode.SnodeMessage import org.session.libsession.utilities.SSKEnvironment -import org.session.libsignal.utilities.logging.Log -import org.session.libsignal.service.api.messages.SignalServiceAttachment +import org.session.libsignal.service.internal.push.PushTransportDetails import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.utilities.Base64 import org.session.libsignal.service.loki.api.crypto.ProofOfWork import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey +import org.session.libsignal.utilities.Base64 +import org.session.libsignal.utilities.logging.Log +import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreview +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote object MessageSender { // Error - internal sealed class Error(val description: String) : Exception() { + sealed class Error(val description: String) : Exception(description) { object InvalidMessage : Error("Invalid message.") object ProtoConversionFailed : Error("Couldn't convert message to proto.") object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.") @@ -46,6 +50,9 @@ object MessageSender { object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") object InvalidClosedGroupUpdate : Error("Invalid group update.") + // Precondition + class PreconditionFailure(val reason: String): Error(reason) + internal val isRetryable: Boolean = when (this) { is InvalidMessage -> false is ProtoConversionFailed -> false @@ -55,29 +62,6 @@ object MessageSender { } } - // Preparation - fun prep(signalAttachments: List, message: VisibleMessage) { - // TODO: Deal with SignalServiceAttachmentStream - val attachments = mutableListOf() - for (signalAttachment in signalAttachments) { - val attachment = Attachment() - if (signalAttachment.isPointer) { - val signalAttachmentPointer = signalAttachment.asPointer() - attachment.fileName = signalAttachmentPointer.fileName.orNull() - attachment.caption = signalAttachmentPointer.caption.orNull() - attachment.contentType = signalAttachmentPointer.contentType - attachment.digest = signalAttachmentPointer.digest.orNull() - attachment.key = signalAttachmentPointer.key - attachment.sizeInBytes = signalAttachmentPointer.size.orNull() - attachment.url = signalAttachmentPointer.url - attachment.size = Size(signalAttachmentPointer.width, signalAttachmentPointer.height) - attachments.add(attachment) - } - } - val attachmentIDs = MessagingConfiguration.shared.storage.persistAttachments(message.id ?: 0, attachments) - message.attachmentIDs.addAll(attachmentIDs) - } - // Convenience fun send(message: Message, destination: Destination): Promise { if (destination is Destination.OpenGroup) { @@ -87,30 +71,28 @@ object MessageSender { } // One-on-One Chats & Closed Groups - fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise { + private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise { val deferred = deferred() val promise = deferred.promise val storage = MessagingConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() - val preconditionFailure = Exception("Destination should not be open groups!") - var snodeMessage: SnodeMessage? = null // Set the timestamp, sender and recipient message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ message.sender = userPublicKey + val isSelfSend = (message.recipient == userPublicKey) + // Set the failure handler (need it here already for precondition failure handling) + fun handleFailure(error: Exception) { + handleFailedMessageSend(message, error) + if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { + SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!) + } + deferred.reject(error) + } try { when (destination) { is Destination.Contact -> message.recipient = destination.publicKey is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey - is Destination.OpenGroup -> throw preconditionFailure - } - val isSelfSend = (message.recipient == userPublicKey) - // Set the failure handler (need it here already for precondition failure handling) - fun handleFailure(error: Exception) { - handleFailedMessageSend(message, error) - if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { - //TODO Notify user for send failure - } - deferred.reject(error) + is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!") } // Validate the message if (!message.isValid()) { throw Error.InvalidMessage } @@ -139,11 +121,8 @@ object MessageSender { // Convert it to protobuf val proto = message.toProto() ?: throw Error.ProtoConversionFailed // Serialize the protobuf - val plaintext = proto.toByteArray() + val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray()) // Encrypt the serialized protobuf - if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { - //TODO Notify user for encrypting message - } val ciphertext: ByteArray when (destination) { is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey) @@ -151,7 +130,7 @@ object MessageSender { val encryptionKeyPair = MessagingConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!! ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey) } - is Destination.OpenGroup -> throw preconditionFailure + is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!") } // Wrap the result val kind: SignalServiceProtos.Envelope.Type @@ -165,19 +144,22 @@ object MessageSender { kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT senderPublicKey = destination.groupPublicKey } - is Destination.OpenGroup -> throw preconditionFailure + is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!") } val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) // Calculate proof of work if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { - //TODO Notify user for proof of work calculating + SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!) } val recipient = message.recipient!! val base64EncodedData = Base64.encodeBytes(wrappedMessage) val timestamp = System.currentTimeMillis() val nonce = ProofOfWork.calculate(base64EncodedData, recipient, timestamp, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed // Send the result - snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce) + val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce) + if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { + SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!) + } SnodeAPI.sendMessage(snodeMessage).success { promises: Set -> var isSuccess = false val promiseCount = promises.size @@ -187,7 +169,7 @@ object MessageSender { if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds isSuccess = true if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { - //TODO Notify user for message sent + SnodeConfiguration.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!) } handleSuccessfulMessageSend(message, destination, isSyncMessage) var shouldNotify = (message is VisibleMessage && !isSyncMessage) @@ -199,57 +181,52 @@ object MessageSender { JobQueue.shared.add(notifyPNServerJob) deferred.resolve(Unit) } - } promise.fail { errorCount += 1 if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed handleFailure(it) - deferred.reject(it) } } }.fail { Log.d("Loki", "Couldn't send message due to error: $it.") - deferred.reject(it) + handleFailure(it) } } catch (exception: Exception) { - deferred.reject(exception) + handleFailure(exception) } return promise } // Open Groups - fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { + private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { val deferred = deferred() val storage = MessagingConfiguration.shared.storage - val preconditionFailure = Exception("Destination should not be contacts or closed groups!") message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } message.sender = storage.getUserPublicKey() + // Set the failure handler (need it here already for precondition failure handling) + fun handleFailure(error: Exception) { + handleFailedMessageSend(message, error) + deferred.reject(error) + } try { val server: String val channel: Long when (destination) { - is Destination.Contact -> throw preconditionFailure - is Destination.ClosedGroup -> throw preconditionFailure + is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!") + is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!") is Destination.OpenGroup -> { message.recipient = "${destination.server}.${destination.channel}" server = destination.server channel = destination.channel } } - // Set the failure handler (need it here already for precondition failure handling) - fun handleFailure(error: Exception) { - handleFailedMessageSend(message, error) - deferred.reject(error) - } // Validate the message if (message !is VisibleMessage || !message.isValid()) { - handleFailure(Error.InvalidMessage) throw Error.InvalidMessage } // Convert the message to an open group message val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run { - handleFailure(Error.InvalidMessage) throw Error.InvalidMessage } // Send the result @@ -261,7 +238,7 @@ object MessageSender { handleFailure(it) } } catch (exception: Exception) { - deferred.reject(exception) + handleFailure(exception) } return deferred.promise } @@ -269,7 +246,8 @@ object MessageSender { // Result Handling fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) { val storage = MessagingConfiguration.shared.storage - val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return + val userPublicKey = storage.getUserPublicKey()!! + val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return // Ignore future self-sends storage.addReceivedMessageTimestamp(message.sentTimestamp!!) // Track the open group server message ID @@ -277,23 +255,89 @@ object MessageSender { storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!) } // Mark the message as sent - storage.markAsSent(messageId) - storage.markUnidentified(messageId) + storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey) + storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey) // Start the disappearing messages timer if needed - SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(messageId) + if (message is VisibleMessage && !isSyncMessage) { + SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey) + } // Sync the message if: // • it's a visible message // • the destination was a contact // • we didn't sync it already - val userPublicKey = storage.getUserPublicKey()!! - if (destination is Destination.Contact && !isSyncMessage && message is VisibleMessage) { - sendToSnodeDestination(Destination.Contact(userPublicKey), message, true).get() + if (destination is Destination.Contact && !isSyncMessage) { + if (message is VisibleMessage) { message.syncTarget = destination.publicKey } + if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey } + sendToSnodeDestination(Destination.Contact(userPublicKey), message, true) } } fun handleFailedMessageSend(message: Message, error: Exception) { val storage = MessagingConfiguration.shared.storage - val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return - storage.setErrorMessage(messageId, error) + val userPublicKey = storage.getUserPublicKey()!! + storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error) + } + + // Convenience + @JvmStatic + fun send(message: VisibleMessage, address: Address, attachments: List, quote: SignalQuote?, linkPreview: SignalLinkPreview?) { + val dataProvider = MessagingConfiguration.shared.messageDataProvider + val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!) + message.attachmentIDs.addAll(attachmentIDs) + message.quote = Quote.from(quote) + message.linkPreview = LinkPreview.from(linkPreview) + message.linkPreview?.let { + if (it.attachmentID == null) { + dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { + message.linkPreview!!.attachmentID = it + message.attachmentIDs.remove(it) + } + } + } + send(message, address) + } + + @JvmStatic + fun send(message: Message, address: Address) { + val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) + message.threadID = threadID + val destination = Destination.from(address) + val job = MessageSendJob(message, destination) + JobQueue.shared.add(job) + } + + fun sendNonDurably(message: VisibleMessage, attachments: List, address: Address): Promise { + val attachmentIDs = MessagingConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!) + message.attachmentIDs.addAll(attachmentIDs) + return sendNonDurably(message, address) + } + + fun sendNonDurably(message: Message, address: Address): Promise { + val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) + message.threadID = threadID + val destination = Destination.from(address) + return send(message, destination) + } + + // Closed groups + fun createClosedGroup(name: String, members: Collection): Promise { + return create(name, members) + } + + fun explicitNameChange(groupPublicKey: String, newName: String) { + return setName(groupPublicKey, newName) + } + + fun explicitAddMembers(groupPublicKey: String, membersToAdd: List) { + return addMembers(groupPublicKey, membersToAdd) + } + + fun explicitRemoveMembers(groupPublicKey: String, membersToRemove: List) { + return removeMembers(groupPublicKey, membersToRemove) + } + + @JvmStatic + fun explicitLeave(groupPublicKey: String): Promise { + return leave(groupPublicKey) } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 8299eb73c7..5fbfc9a891 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.threads.Address import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Hex import org.session.libsignal.libsignal.ecc.Curve @@ -25,9 +26,10 @@ import org.session.libsignal.utilities.logging.Log import java.util.* import java.util.concurrent.ConcurrentHashMap -private val pendingKeyPair = ConcurrentHashMap>() +const val groupSizeLimit = 100 +val pendingKeyPair = ConcurrentHashMap>() -fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { +fun MessageSender.create(name: String, members: Collection): Promise { val deferred = deferred() ThreadUtils.queue { // Prepare @@ -48,9 +50,11 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): storage.setProfileSharing(Address.fromSerialized(groupID), true) // Send a closed group update message to all members individually val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) + val sentTime = System.currentTimeMillis() for (member in members) { val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind) - sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get() + closedGroupControlMessage.sentTimestamp = sentTime + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get() } // Add the group to the user's set of public keys to poll for storage.addClosedGroupPublicKey(groupPublicKey) @@ -58,7 +62,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) // Notify the user val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID) + storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime) // Notify the PN server PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) // Fulfill the promise @@ -68,7 +72,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): return deferred.promise } -fun MessageSender.v2_update(groupPublicKey: String, members: List, name: String) { +fun MessageSender.update(groupPublicKey: String, members: List, name: String) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) @@ -98,14 +102,16 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) { val admins = group.admins.map { it.serialize() } // Send the update to the group val kind = ClosedGroupControlMessage.Kind.NameChange(newName) + val sentTime = System.currentTimeMillis() val closedGroupControlMessage = ClosedGroupControlMessage(kind) + closedGroupControlMessage.sentTimestamp = sentTime send(closedGroupControlMessage, Address.fromSerialized(groupID)) // Update the group storage.updateTitle(groupID, newName) // Notify the user val infoType = SignalServiceProtos.GroupContext.Type.UPDATE val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID) + storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) } fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List) { @@ -134,18 +140,21 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List) val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData) + val sentTime = System.currentTimeMillis() val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind) + closedGroupControlMessage.sentTimestamp = sentTime send(closedGroupControlMessage, Address.fromSerialized(groupID)) // Send closed group update messages to any new members individually for (member in membersToAdd) { val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind) + closedGroupControlMessage.sentTimestamp = sentTime send(closedGroupControlMessage, Address.fromSerialized(member)) } // Notify the user val infoType = SignalServiceProtos.GroupContext.Type.UPDATE val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List) { @@ -173,7 +182,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List { + val deferred = deferred() + ThreadUtils.queue { + val context = MessagingConfiguration.shared.context + val storage = MessagingConfiguration.shared.storage + val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: return@queue deferred.reject(Error.NoThread) + val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey + val admins = group.admins.map { it.serialize() } + val name = group.title + // Send the update to the group + val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft()) + val sentTime = System.currentTimeMillis() + closedGroupControlMessage.sentTimestamp = sentTime + sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { + // Notify the user + val infoType = SignalServiceProtos.GroupContext.Type.QUIT + val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) + if (notifyUser) { + storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) + } + // Remove the group private key and unsubscribe from PNs + MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) + deferred.resolve(Unit) + } } - val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey - val admins = group.admins.map { it.serialize() } - val name = group.title - // Send the update to the group - val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft) - sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { - // Remove the group private key and unsubscribe from PNs - MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) - } - // Notify the user - val infoType = SignalServiceProtos.GroupContext.Type.QUIT - val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + return deferred.promise } fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection) { @@ -230,6 +247,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta // make sure we set the pendingKeyPair or wait until it is not null } while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair))) // Distribute it + sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success { + // Store it * after * having sent out the message to the group + storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + pendingKeyPair[groupPublicKey] = Optional.absent() + } +} + +fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection, targetUser: String? = null, force: Boolean = true): Promise? { + val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey) val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) @@ -238,12 +264,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) } - val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers) + val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers) + val sentTime = System.currentTimeMillis() val closedGroupControlMessage = ClosedGroupControlMessage(kind) - sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { - // Store it * after * having sent out the message to the group - storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) - pendingKeyPair[groupPublicKey] = Optional.absent() + closedGroupControlMessage.sentTimestamp = sentTime + return if (force) { + MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination)) + } else { + MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination)) + null } } @@ -258,34 +287,8 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) { val members = group.members.map { it.serialize() }.toSet() if (!members.contains(storage.getUserPublicKey()!!)) return // Send the request to the group - val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest) + val sentTime = System.currentTimeMillis() + val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest()) + closedGroupControlMessage.sentTimestamp = sentTime send(closedGroupControlMessage, Address.fromSerialized(groupID)) -} - -fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) { - val storage = MessagingConfiguration.shared.storage - val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Can't send encryption key pair for nonexistent closed group.") - throw Error.NoThread - } - val members = group.members.map { it.serialize() } - if (!members.contains(publicKey)) { - Log.d("Loki", "Refusing to send latest encryption key pair to non-member.") - return - } - // Get the latest encryption key pair - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() - ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return - // Send it - val proto = SignalServiceProtos.KeyPair.newBuilder() - proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize()) - proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize()) - val plaintext = proto.build().toByteArray() - val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) - Log.d("Loki", "Sending latest encryption key pair to: $publicKey.") - val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) - val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper)) - val closedGroupControlMessage = ClosedGroupControlMessage(kind) - MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey)) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt deleted file mode 100644 index 99d346aeff..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderConvenience.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.session.libsession.messaging.sending_receiving - -import nl.komponents.kovenant.Promise -import org.session.libsession.messaging.MessagingConfiguration - -import org.session.libsession.messaging.jobs.JobQueue -import org.session.libsession.messaging.jobs.MessageSendJob -import org.session.libsession.messaging.messages.Destination -import org.session.libsession.messaging.messages.Message -import org.session.libsession.messaging.messages.visible.VisibleMessage -import org.session.libsession.messaging.threads.Address - -import org.session.libsignal.service.api.messages.SignalServiceAttachment - -fun MessageSender.send(message: VisibleMessage, attachments: List, address: Address) { - prep(attachments, message) - send(message, address) -} - -fun MessageSender.send(message: Message, address: Address) { - val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) - message.threadID = threadID - val destination = Destination.from(address) - val job = MessageSendJob(message, destination) - JobQueue.shared.add(job) -} - -fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List, address: Address): Promise { - prep(attachments, message) - return sendNonDurably(message, address) -} - -fun MessageSender.sendNonDurably(message: Message, address: Address): Promise { - val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address) - message.threadID = threadID - val destination = Destination.from(address) - return MessageSender.send(message, destination) -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt index 43a61a0d25..a108724e9c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt @@ -1,11 +1,53 @@ package org.session.libsession.messaging.sending_receiving +import com.goterl.lazycode.lazysodium.LazySodiumAndroid +import com.goterl.lazycode.lazysodium.SodiumAndroid +import com.goterl.lazycode.lazysodium.interfaces.Box +import com.goterl.lazycode.lazysodium.interfaces.Sign + import org.session.libsession.messaging.MessagingConfiguration +import org.session.libsession.messaging.sending_receiving.MessageSender.Error +import org.session.libsession.utilities.KeyPairUtilities + +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded +import org.session.libsignal.utilities.Hex +import org.session.libsignal.utilities.logging.Log object MessageSenderEncryption { - internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientPublicKey: String): ByteArray{ - return MessagingConfiguration.shared.sessionProtocol.encrypt(plaintext, recipientPublicKey) + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } + + /** + * Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`. + * + * @param plaintext the plaintext to encrypt. Must already be padded. + * @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group. + * + * @return the encrypted message. + */ + internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{ + val context = MessagingConfiguration.shared.context + val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair + val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) + + val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey + val signature = ByteArray(Sign.BYTES) + try { + sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't sign message due to error: $exception.") + throw Error.SigningFailed + } + val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature + val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES) + try { + sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't encrypt message due to error: $exception.") + throw Error.EncryptionFailed + } + + return ciphertext } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentId.java b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentId.java index 7e60f2e77d..1f0f51eece 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentId.java +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/attachments/AttachmentId.java @@ -12,10 +12,10 @@ import org.session.libsession.utilities.Util; public class AttachmentId implements Parcelable { @JsonProperty - private final long rowId; + private final long rowId; // This is the field id in the database @JsonProperty - private final long uniqueId; + private final long uniqueId; // This is the timestamp when the attachment is written into the database public AttachmentId(@JsonProperty("rowId") long rowId, @JsonProperty("uniqueId") long uniqueId) { this.rowId = rowId; diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 007dbbfd60..cdc42be968 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -7,10 +7,10 @@ import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.utilities.MessageWrapper -import org.session.libsession.snode.Snode import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeConfiguration +import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.Base64 diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/DistributionTypes.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/DistributionTypes.kt new file mode 100644 index 0000000000..e71b83b0dd --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/DistributionTypes.kt @@ -0,0 +1,9 @@ +package org.session.libsession.messaging.threads + +object DistributionTypes { + const val DEFAULT = 2 + const val BROADCAST = 1 + const val CONVERSATION = 2 + const val ARCHIVE = 3 + const val INBOX_ZERO = 4 +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientFormattingException.java b/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientFormattingException.java index 7a97811d9d..2bf8910f3c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientFormattingException.java +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientFormattingException.java @@ -17,19 +17,7 @@ package org.session.libsession.messaging.threads.recipients; public class RecipientFormattingException extends Exception { - public RecipientFormattingException() { - super(); - } - public RecipientFormattingException(String message) { super(message); } - - public RecipientFormattingException(String message, Throwable nested) { - super(message, nested); - } - - public RecipientFormattingException(Throwable nested) { - super(nested); - } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientsFormatter.java b/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientsFormatter.java deleted file mode 100644 index bdce30b69c..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/recipients/RecipientsFormatter.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.session.libsession.messaging.threads.recipients; - -import android.telephony.PhoneNumberUtils; -import android.text.TextUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.StringTokenizer; - -public class RecipientsFormatter { - - private static String parseBracketedNumber(String recipient) throws RecipientFormattingException { - int begin = recipient.indexOf('<'); - int end = recipient.indexOf('>'); - String value = recipient.substring(begin + 1, end); - - if (PhoneNumberUtils.isWellFormedSmsAddress(value)) - return value; - else - throw new RecipientFormattingException("Bracketed value: " + value + " is not valid."); - } - - private static String parseRecipient(String recipient) throws RecipientFormattingException { - recipient = recipient.trim(); - - if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1)) - return parseBracketedNumber(recipient); - - if (PhoneNumberUtils.isWellFormedSmsAddress(recipient)) - return recipient; - - throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted."); - } - - public static List getRecipients(String rawText) throws RecipientFormattingException { - ArrayList results = new ArrayList(); - StringTokenizer tokenizer = new StringTokenizer(rawText, ","); - - while (tokenizer.hasMoreTokens()) { - results.add(parseRecipient(tokenizer.nextToken())); - } - - return results; - } - - public static String formatNameAndNumber(String name, String number) { - // Format like this: Mike Cleron <(650) 555-1234> - // Erick Tseng <(650) 555-1212> - // Tutankhamun - // (408) 555-1289 - String formattedNumber = PhoneNumberUtils.formatNumber(number); - if (!TextUtils.isEmpty(name) && !name.equals(number)) { - return name + " <" + formattedNumber + ">"; - } else { - return formattedNumber; - } - } - - -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt index 49853b33e2..0856e43862 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt @@ -42,7 +42,7 @@ open class DotNetAPI { internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH } // Error - internal sealed class Error(val description: String) : Exception() { + internal sealed class Error(val description: String) : Exception(description) { object Generic : Error("An error occurred.") object InvalidURL : Error("Invalid URL.") object ParsingFailed : Error("Invalid file server response.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt index 9305852258..b11cbe1774 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt @@ -10,7 +10,7 @@ import java.security.SecureRandom object MessageWrapper { // region Types - sealed class Error(val description: String) : Exception() { + sealed class Error(val description: String) : Exception(description) { object FailedToWrapData : Error("Failed to wrap data.") object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.") object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt deleted file mode 100644 index 31a9a0bc01..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.session.libsession.messaging.utilities - -import android.content.Context -import com.goterl.lazycode.lazysodium.LazySodiumAndroid -import com.goterl.lazycode.lazysodium.SodiumAndroid -import org.session.libsession.messaging.MessagingConfiguration -import org.session.libsession.utilities.TextSecurePreferences.isUniversalUnidentifiedAccess -import org.session.libsession.utilities.Util.getSecretBytes -import org.session.libsignal.metadata.SignalProtos -import org.session.libsignal.service.api.crypto.UnidentifiedAccess -import org.session.libsignal.utilities.logging.Log - -object UnidentifiedAccessUtil { - private val TAG = UnidentifiedAccessUtil::class.simpleName - private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } - - fun getAccessFor(recipientPublicKey: String): UnidentifiedAccess? { - val theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientPublicKey) - val ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey() - val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate() - - Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + - " | Our access key present? " + (ourUnidentifiedAccessKey != null) + - " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null)) - - return if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { - UnidentifiedAccess(theirUnidentifiedAccessKey) - } else null - } - - fun getAccessForSync(context: Context): UnidentifiedAccess? { - var ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey() - val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate() - if (isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = getSecretBytes(16) - } - return if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { - UnidentifiedAccess(ourUnidentifiedAccessKey) - } else null - } - - private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? { - val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16) - return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey) - } - - private fun getSelfUnidentifiedAccessKey(): ByteArray? { - val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() - if (userPublicKey != null) { - return sodium.randomBytesBuf(16) - } - return null - } - - private fun getUnidentifiedAccessCertificate(): ByteArray? { - val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() - if (userPublicKey != null) { - val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build() - return certificate.toByteArray() - } - return null - } -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 8ab1c33dc5..20f1fc8d04 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -10,7 +10,7 @@ import org.session.libsession.utilities.AESGCM import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.* -import org.session.libsignal.service.loki.api.* +import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.service.loki.api.fileserver.FileServerAPI import org.session.libsignal.service.loki.api.utilities.* import org.session.libsession.utilities.AESGCM.EncryptionResult @@ -74,7 +74,7 @@ object OnionRequestAPI { ) internal sealed class Destination { - class Snode(val snode: org.session.libsession.snode.Snode) : Destination() + class Snode(val snode: org.session.libsignal.service.loki.api.Snode) : Destination() class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination() } diff --git a/libsession/src/main/java/org/session/libsession/snode/Snode.kt b/libsession/src/main/java/org/session/libsession/snode/Snode.kt index 8fd05c5f01..b02582846c 100644 --- a/libsession/src/main/java/org/session/libsession/snode/Snode.kt +++ b/libsession/src/main/java/org/session/libsession/snode/Snode.kt @@ -1,6 +1,6 @@ package org.session.libsession.snode -public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { +class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { val ip: String get() = address.removePrefix("https://") diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index c16e3a57a6..98b711af60 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -10,6 +10,9 @@ import org.session.libsession.snode.utilities.getRandomElement import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.loki.api.utilities.HTTP +import org.session.libsignal.service.loki.api.Snode +import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol +import org.session.libsignal.service.loki.utilities.Broadcaster import org.session.libsignal.service.loki.utilities.prettifiedDescription import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.utilities.* @@ -17,10 +20,11 @@ import org.session.libsignal.utilities.* import java.security.SecureRandom object SnodeAPI { - val database = SnodeConfiguration.shared.storage - val broadcaster = SnodeConfiguration.shared.broadcaster + val database: LokiAPIDatabaseProtocol + get() = SnodeConfiguration.shared.storage + val broadcaster: Broadcaster + get() = SnodeConfiguration.shared.broadcaster val sharedContext = Kovenant.createContext() - val messageSendingContext = Kovenant.createContext() val messagePollingContext = Kovenant.createContext() internal var snodeFailureCount: MutableMap = mutableMapOf() @@ -41,7 +45,7 @@ object SnodeAPI { internal var powDifficulty = 1 // Error - internal sealed class Error(val description: String) : Exception() { + internal sealed class Error(val description: String) : Exception(description) { object Generic : Error("An error occurred.") object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.") object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.") @@ -158,7 +162,7 @@ object SnodeAPI { val parameters = mapOf( "pubKey" to publicKey ) return getRandomSnode().bind { invoke(Snode.Method.GetSwarm, it, publicKey, parameters) - }.map(SnodeAPI.sharedContext) { + }.map(sharedContext) { parseSnodes(it).toSet() }.success { database.setSwarm(publicKey, it) @@ -182,19 +186,12 @@ object SnodeAPI { fun sendMessage(message: SnodeMessage): Promise, Exception> { val destination = message.recipient - fun broadcast(event: String) { - val dayInMs: Long = 86400000 - if (message.ttl != dayInMs && message.ttl != 4 * dayInMs) { return } - broadcaster.broadcast(event, message.timestamp) - } - broadcast("calculatingPoW") return retryIfNeeded(maxRetryCount) { - getTargetSnodes(destination).map(messageSendingContext) { swarm -> + getTargetSnodes(destination).map { swarm -> swarm.map { snode -> - broadcast("sendingMessage") val parameters = message.toJSON() retryIfNeeded(maxRetryCount) { - invoke(Snode.Method.SendMessage, snode, destination, parameters).map(messageSendingContext) { rawResponse -> + invoke(Snode.Method.SendMessage, snode, destination, parameters).map { rawResponse -> val json = rawResponse as? Map<*, *> val powDifficulty = json?.get("difficulty") as? Int if (powDifficulty != null) { diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt index ba2923cf4f..a5ff44e7ff 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeConfiguration.kt @@ -1,12 +1,13 @@ package org.session.libsession.snode +import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.utilities.Broadcaster -class SnodeConfiguration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) { +class SnodeConfiguration(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) { companion object { lateinit var shared: SnodeConfiguration - fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) { + fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) { if (Companion::shared.isInitialized) { return } shared = SnodeConfiguration(storage, broadcaster) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java b/libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java similarity index 72% rename from app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java rename to libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java index 275e7ac1bd..06de335b4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/IdentityKeyUtil.java +++ b/libsession/src/main/java/org/session/libsession/utilities/IdentityKeyUtil.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.thoughtcrime.securesms.crypto; +package org.session.libsession.utilities; import android.content.Context; import android.content.SharedPreferences; @@ -23,7 +23,6 @@ import android.content.SharedPreferences.Editor; import androidx.annotation.NonNull; import org.session.libsignal.libsignal.ecc.ECPublicKey; -import org.thoughtcrime.securesms.backup.BackupProtos; import org.session.libsignal.libsignal.IdentityKey; import org.session.libsignal.libsignal.IdentityKeyPair; import org.session.libsignal.libsignal.InvalidKeyException; @@ -34,8 +33,6 @@ import org.session.libsignal.libsignal.ecc.ECPrivateKey; import org.session.libsignal.utilities.Base64; import java.io.IOException; -import java.util.LinkedList; -import java.util.List; /** * Utility class for working with identity keys. @@ -95,45 +92,6 @@ public class IdentityKeyUtil { save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize())); } - public static List getBackupRecords(@NonNull Context context) { - final String prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME; - SharedPreferences preferences = context.getSharedPreferences(prefName, 0); - - LinkedList prefList = new LinkedList<>(); - - prefList.add(BackupProtos.SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IDENTITY_PUBLIC_KEY_PREF) - .setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null)) - .build()); - prefList.add(BackupProtos.SharedPreference.newBuilder() - .setFile(prefName) - .setKey(IDENTITY_PRIVATE_KEY_PREF) - .setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null)) - .build()); - if (preferences.contains(ED25519_PUBLIC_KEY)) { - prefList.add(BackupProtos.SharedPreference.newBuilder() - .setFile(prefName) - .setKey(ED25519_PUBLIC_KEY) - .setValue(preferences.getString(ED25519_PUBLIC_KEY, null)) - .build()); - } - if (preferences.contains(ED25519_SECRET_KEY)) { - prefList.add(BackupProtos.SharedPreference.newBuilder() - .setFile(prefName) - .setKey(ED25519_SECRET_KEY) - .setValue(preferences.getString(ED25519_SECRET_KEY, null)) - .build()); - } - prefList.add(BackupProtos.SharedPreference.newBuilder() - .setFile(prefName) - .setKey(LOKI_SEED) - .setValue(preferences.getString(LOKI_SEED, null)) - .build()); - - return prefList; - } - public static String retrieve(Context context, String key) { SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0); return preferences.getString(key, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt similarity index 96% rename from app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt rename to libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt index 8acd255597..92326cfc68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/KeyPairUtilities.kt @@ -1,11 +1,10 @@ -package org.thoughtcrime.securesms.loki.utilities +package org.session.libsession.utilities import android.content.Context import com.goterl.lazycode.lazysodium.LazySodiumAndroid import com.goterl.lazycode.lazysodium.SodiumAndroid import com.goterl.lazycode.lazysodium.utils.Key import com.goterl.lazycode.lazysodium.utils.KeyPair -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Hex import org.session.libsignal.libsignal.ecc.DjbECPrivateKey diff --git a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt index 2a6be307b0..3c4612c412 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -38,7 +38,7 @@ class SSKEnvironment( interface MessageExpirationManagerProtocol { fun setExpirationTimer(messageID: Long?, duration: Int, senderPublicKey: String, content: SignalServiceProtos.Content) fun disableExpirationTimer(messageID: Long?, senderPublicKey: String, content: SignalServiceProtos.Content) - fun startAnyExpiration(messageID: Long) + fun startAnyExpiration(timestamp: Long, author: String) } companion object { diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java deleted file mode 100644 index 154ab3c025..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ /dev/null @@ -1,825 +0,0 @@ -/* - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ -package org.session.libsignal.service.api; - -import com.google.protobuf.ByteString; - -import org.jetbrains.annotations.Nullable; -import org.session.libsignal.libsignal.ecc.ECKeyPair; -import org.session.libsignal.libsignal.state.IdentityKeyStore; -import org.session.libsignal.utilities.logging.Log; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream; -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.session.libsignal.service.api.crypto.UntrustedIdentityException; -import org.session.libsignal.service.api.messages.SendMessageResult; -import org.session.libsignal.service.api.messages.SignalServiceAttachment; -import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; -import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream; -import org.session.libsignal.service.api.messages.SignalServiceDataMessage; -import org.session.libsignal.service.api.messages.SignalServiceGroup; -import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; -import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; -import org.session.libsignal.service.api.messages.shared.SharedContact; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.api.push.exceptions.PushNetworkException; -import org.session.libsignal.service.internal.crypto.PaddingInputStream; -import org.session.libsignal.service.internal.push.OutgoingPushMessage; -import org.session.libsignal.service.internal.push.OutgoingPushMessageList; -import org.session.libsignal.service.internal.push.PushAttachmentData; -import org.session.libsignal.service.internal.push.PushTransportDetails; -import org.session.libsignal.service.internal.push.SignalServiceProtos; -import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer; -import org.session.libsignal.service.internal.push.SignalServiceProtos.Content; -import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; -import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; -import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage.LokiProfile; -import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage; -import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage; -import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory; -import org.session.libsignal.service.internal.push.http.OutputStreamFactory; -import org.session.libsignal.utilities.Base64; -import org.session.libsignal.service.internal.util.Util; -import org.session.libsignal.utilities.concurrent.SettableFuture; -import org.session.libsignal.service.loki.api.LokiDotNetAPI; -import org.session.libsignal.service.loki.api.PushNotificationAPI; -import org.session.libsignal.service.loki.api.SignalMessageInfo; -import org.session.libsignal.service.loki.api.SnodeAPI; -import org.session.libsignal.service.loki.api.crypto.SessionProtocol; -import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; -import org.session.libsignal.service.loki.api.opengroups.PublicChat; -import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI; -import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage; -import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol; -import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol; -import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol; -import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol; -import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol; -import org.session.libsignal.service.loki.utilities.TTLUtilities; -import org.session.libsignal.service.loki.utilities.Broadcaster; -import org.session.libsignal.service.loki.utilities.HexEncodingKt; -import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import kotlin.Unit; -import kotlin.jvm.functions.Function1; -import nl.komponents.kovenant.Promise; - -/** - * The main interface for sending Signal Service messages. - * - * @author Moxie Marlinspike - */ -public class SignalServiceMessageSender { - - private static final String TAG = SignalServiceMessageSender.class.getSimpleName(); - - private final IdentityKeyStore store; - // Loki - private final String userPublicKey; - private final LokiAPIDatabaseProtocol apiDatabase; - private final LokiThreadDatabaseProtocol threadDatabase; - private final LokiMessageDatabaseProtocol messageDatabase; - private final SessionProtocol sessionProtocolImpl; - private final LokiUserDatabaseProtocol userDatabase; - private final LokiOpenGroupDatabaseProtocol openGroupDatabase; - private final Broadcaster broadcaster; - - public SignalServiceMessageSender(IdentityKeyStore store, - String userPublicKey, - LokiAPIDatabaseProtocol apiDatabase, - LokiThreadDatabaseProtocol threadDatabase, - LokiMessageDatabaseProtocol messageDatabase, - SessionProtocol sessionProtocolImpl, - LokiUserDatabaseProtocol userDatabase, - LokiOpenGroupDatabaseProtocol openGroupDatabase, - Broadcaster broadcaster) - { - this.store = store; - this.userPublicKey = userPublicKey; - this.apiDatabase = apiDatabase; - this.threadDatabase = threadDatabase; - this.messageDatabase = messageDatabase; - this.sessionProtocolImpl = sessionProtocolImpl; - this.userDatabase = userDatabase; - this.openGroupDatabase = openGroupDatabase; - this.broadcaster = broadcaster; - } - - /** - * Send a read receipt for a received message. - * - * @param recipient The sender of the received message you're acknowledging. - * @param message The read receipt to deliver. - * @throws IOException - */ - public void sendReceipt(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceReceiptMessage message) - throws IOException { - byte[] content = createReceiptContent(message); - boolean useFallbackEncryption = true; - sendMessage(recipient, unidentifiedAccess, message.getWhen(), content, false, message.getTTL(), useFallbackEncryption); - } - - public void sendTyping(List recipients, - List> unidentifiedAccess, - SignalServiceTypingMessage message) - throws IOException - { - byte[] content = createTypingContent(message); - sendMessage(0, recipients, unidentifiedAccess, message.getTimestamp(), content, true, message.getTTL(), false, false); - } - - /** - * Send a message to a single recipient. - * - * @param recipient The message's destination. - * @param message The message. - * @throws IOException - */ - public SendMessageResult sendMessage(long messageID, - SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceDataMessage message, - boolean isSelfSend) - throws IOException - { - byte[] content = createMessageContent(message, recipient); - long timestamp = message.getTimestamp(); - boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL; - SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccess, timestamp, content, false, message.getTTL(), true, isClosedGroup, message.hasVisibleContent() && !isSelfSend, message.getSyncTarget()); - - return result; - } - - /** - * Send a message to a group. - * - * @param recipients The group members. - * @param message The group message. - * @throws IOException - */ - public List sendMessage(long messageID, - List recipients, - List> unidentifiedAccess, - SignalServiceDataMessage message) - throws IOException { - // Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine - // whether an attachment is being sent to an open group or not. - byte[] content = createMessageContent(message, recipients.get(0)); - long timestamp = message.getTimestamp(); - boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL; - - return sendMessage(messageID, recipients, unidentifiedAccess, timestamp, content, false, message.getTTL(), isClosedGroup, message.hasVisibleContent()); - } - - public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment, boolean usePadding, @Nullable SignalServiceAddress recipient) - throws IOException - { - boolean shouldEncrypt = true; - String server = FileServerAPI.shared.getServer(); - - // Loki - Check if we are sending to an open group - if (recipient != null) { - long threadID = threadDatabase.getThreadID(recipient.getNumber()); - PublicChat publicChat = threadDatabase.getPublicChat(threadID); - if (publicChat != null) { - shouldEncrypt = false; - server = publicChat.getServer(); - } - } - - byte[] attachmentKey = Util.getSecretBytes(64); - long paddedLength = usePadding ? PaddingInputStream.getPaddedSize(attachment.getLength()) : attachment.getLength(); - InputStream dataStream = usePadding ? new PaddingInputStream(attachment.getInputStream(), attachment.getLength()) : attachment.getInputStream(); - long ciphertextLength = shouldEncrypt ? AttachmentCipherOutputStream.getCiphertextLength(paddedLength) : attachment.getLength(); - - OutputStreamFactory outputStreamFactory = shouldEncrypt ? new AttachmentCipherOutputStreamFactory(attachmentKey) : new PlaintextOutputStreamFactory(); - PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), dataStream, ciphertextLength, outputStreamFactory, attachment.getListener()); - - // Loki - Upload attachment - LokiDotNetAPI.UploadResult result = FileServerAPI.shared.uploadAttachment(server, attachmentData); - return new SignalServiceAttachmentPointer(result.getId(), - attachment.getContentType(), - attachmentKey, - Optional.of(Util.toIntExact(attachment.getLength())), - attachment.getPreview(), - attachment.getWidth(), attachment.getHeight(), - Optional.fromNullable(result.getDigest()), - attachment.getFileName(), - attachment.getVoiceNote(), - attachment.getCaption(), - result.getUrl()); - } - - private byte[] createTypingContent(SignalServiceTypingMessage message) { - Content.Builder container = Content.newBuilder(); - TypingMessage.Builder builder = TypingMessage.newBuilder(); - - builder.setTimestamp(message.getTimestamp()); - - if (message.isTypingStarted()) builder.setAction(TypingMessage.Action.STARTED); - else if (message.isTypingStopped()) builder.setAction(TypingMessage.Action.STOPPED); - else throw new IllegalArgumentException("Unknown typing indicator"); - - return container.setTypingMessage(builder).build().toByteArray(); - } - - private byte[] createReceiptContent(SignalServiceReceiptMessage message) { - Content.Builder container = Content.newBuilder(); - ReceiptMessage.Builder builder = ReceiptMessage.newBuilder(); - - for (long timestamp : message.getTimestamps()) { - builder.addTimestamp(timestamp); - } - - if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY); - else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ); - - return container.setReceiptMessage(builder).build().toByteArray(); - } - - private byte[] createMessageContent(SignalServiceDataMessage message, SignalServiceAddress recipient) - throws IOException - { - Content.Builder container = Content.newBuilder(); - - DataMessage.Builder builder = DataMessage.newBuilder(); - List pointers = createAttachmentPointers(message.getAttachments(), recipient); - - if (!pointers.isEmpty()) { - builder.addAllAttachments(pointers); - } - - if (message.getBody().isPresent()) { - builder.setBody(message.getBody().get()); - } - - if (message.getGroupInfo().isPresent()) { - builder.setGroup(createGroupContent(message.getGroupInfo().get(), recipient)); - } - - if (message.isExpirationUpdate()) { - builder.setFlags(DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE); - } - - if (message.getExpiresInSeconds() > 0) { - builder.setExpireTimer(message.getExpiresInSeconds()); - } - - if (message.getProfileKey().isPresent()) { - builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get())); - } - - if (message.getSyncTarget().isPresent()) { - builder.setSyncTarget(message.getSyncTarget().get()); - } - - if (message.getQuote().isPresent()) { - DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder() - .setId(message.getQuote().get().getId()) - .setAuthor(message.getQuote().get().getAuthor().getNumber()) - .setText(message.getQuote().get().getText()); - - for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) { - DataMessage.Quote.QuotedAttachment.Builder quotedAttachment = DataMessage.Quote.QuotedAttachment.newBuilder(); - - quotedAttachment.setContentType(attachment.getContentType()); - - if (attachment.getFileName() != null) { - quotedAttachment.setFileName(attachment.getFileName()); - } - - if (attachment.getThumbnail() != null) { - quotedAttachment.setThumbnail(createAttachmentPointer(attachment.getThumbnail().asStream(), recipient)); - } - - quoteBuilder.addAttachments(quotedAttachment); - } - - builder.setQuote(quoteBuilder); - } - - if (message.getSharedContacts().isPresent()) { - builder.addAllContact(createSharedContactContent(message.getSharedContacts().get(), recipient)); - } - - if (message.getPreviews().isPresent()) { - for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) { - DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder(); - previewBuilder.setTitle(preview.getTitle()); - previewBuilder.setUrl(preview.getUrl()); - - if (preview.getImage().isPresent()) { - if (preview.getImage().get().isStream()) { - previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asStream(), recipient)); - } else { - previewBuilder.setImage(createAttachmentPointer(preview.getImage().get().asPointer())); - } - } - - builder.addPreview(previewBuilder.build()); - } - } - - LokiProfile.Builder lokiUserProfileBuilder = LokiProfile.newBuilder(); - String displayName = userDatabase.getDisplayName(userPublicKey); - if (displayName != null) { lokiUserProfileBuilder.setDisplayName(displayName); } - String profilePictureURL = userDatabase.getProfilePictureURL(userPublicKey); - if (profilePictureURL != null) { lokiUserProfileBuilder.setProfilePicture(profilePictureURL); } - builder.setProfile(lokiUserProfileBuilder.build()); - - builder.setTimestamp(message.getTimestamp()); - - container.setDataMessage(builder); - - return container.build().toByteArray(); - } - - private GroupContext createGroupContent(SignalServiceGroup group, SignalServiceAddress recipient) - throws IOException - { - GroupContext.Builder builder = GroupContext.newBuilder(); - builder.setId(ByteString.copyFrom(group.getGroupId())); - - if (group.getType() != SignalServiceGroup.Type.DELIVER) { - if (group.getType() == SignalServiceGroup.Type.UPDATE) builder.setType(GroupContext.Type.UPDATE); - else if (group.getType() == SignalServiceGroup.Type.QUIT) builder.setType(GroupContext.Type.QUIT); - else if (group.getType() == SignalServiceGroup.Type.REQUEST_INFO) builder.setType(GroupContext.Type.REQUEST_INFO); - else throw new AssertionError("Unknown type: " + group.getType()); - - if (group.getName().isPresent()) builder.setName(group.getName().get()); - if (group.getMembers().isPresent()) builder.addAllMembers(group.getMembers().get()); - if (group.getAdmins().isPresent()) builder.addAllAdmins(group.getAdmins().get()); - - if (group.getAvatar().isPresent()) { - if (group.getAvatar().get().isStream()) { - builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asStream(), recipient)); - } else { - builder.setAvatar(createAttachmentPointer(group.getAvatar().get().asPointer())); - } - } - } else { - builder.setType(GroupContext.Type.DELIVER); - } - - return builder.build(); - } - - private List createSharedContactContent(List contacts, SignalServiceAddress recipient) - throws IOException - { - List results = new LinkedList<>(); - - for (SharedContact contact : contacts) { - DataMessage.Contact.Name.Builder nameBuilder = DataMessage.Contact.Name.newBuilder(); - - if (contact.getName().getFamily().isPresent()) nameBuilder.setFamilyName(contact.getName().getFamily().get()); - if (contact.getName().getGiven().isPresent()) nameBuilder.setGivenName(contact.getName().getGiven().get()); - if (contact.getName().getMiddle().isPresent()) nameBuilder.setMiddleName(contact.getName().getMiddle().get()); - if (contact.getName().getPrefix().isPresent()) nameBuilder.setPrefix(contact.getName().getPrefix().get()); - if (contact.getName().getSuffix().isPresent()) nameBuilder.setSuffix(contact.getName().getSuffix().get()); - if (contact.getName().getDisplay().isPresent()) nameBuilder.setDisplayName(contact.getName().getDisplay().get()); - - DataMessage.Contact.Builder contactBuilder = DataMessage.Contact.newBuilder() - .setName(nameBuilder); - - if (contact.getAddress().isPresent()) { - for (SharedContact.PostalAddress address : contact.getAddress().get()) { - DataMessage.Contact.PostalAddress.Builder addressBuilder = DataMessage.Contact.PostalAddress.newBuilder(); - - switch (address.getType()) { - case HOME: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.HOME); break; - case WORK: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.WORK); break; - case CUSTOM: addressBuilder.setType(DataMessage.Contact.PostalAddress.Type.CUSTOM); break; - default: throw new AssertionError("Unknown type: " + address.getType()); - } - - if (address.getCity().isPresent()) addressBuilder.setCity(address.getCity().get()); - if (address.getCountry().isPresent()) addressBuilder.setCountry(address.getCountry().get()); - if (address.getLabel().isPresent()) addressBuilder.setLabel(address.getLabel().get()); - if (address.getNeighborhood().isPresent()) addressBuilder.setNeighborhood(address.getNeighborhood().get()); - if (address.getPobox().isPresent()) addressBuilder.setPobox(address.getPobox().get()); - if (address.getPostcode().isPresent()) addressBuilder.setPostcode(address.getPostcode().get()); - if (address.getRegion().isPresent()) addressBuilder.setRegion(address.getRegion().get()); - if (address.getStreet().isPresent()) addressBuilder.setStreet(address.getStreet().get()); - - contactBuilder.addAddress(addressBuilder); - } - } - - if (contact.getEmail().isPresent()) { - for (SharedContact.Email email : contact.getEmail().get()) { - DataMessage.Contact.Email.Builder emailBuilder = DataMessage.Contact.Email.newBuilder() - .setValue(email.getValue()); - - switch (email.getType()) { - case HOME: emailBuilder.setType(DataMessage.Contact.Email.Type.HOME); break; - case WORK: emailBuilder.setType(DataMessage.Contact.Email.Type.WORK); break; - case MOBILE: emailBuilder.setType(DataMessage.Contact.Email.Type.MOBILE); break; - case CUSTOM: emailBuilder.setType(DataMessage.Contact.Email.Type.CUSTOM); break; - default: throw new AssertionError("Unknown type: " + email.getType()); - } - - if (email.getLabel().isPresent()) emailBuilder.setLabel(email.getLabel().get()); - - contactBuilder.addEmail(emailBuilder); - } - } - - if (contact.getPhone().isPresent()) { - for (SharedContact.Phone phone : contact.getPhone().get()) { - DataMessage.Contact.Phone.Builder phoneBuilder = DataMessage.Contact.Phone.newBuilder() - .setValue(phone.getValue()); - - switch (phone.getType()) { - case HOME: phoneBuilder.setType(DataMessage.Contact.Phone.Type.HOME); break; - case WORK: phoneBuilder.setType(DataMessage.Contact.Phone.Type.WORK); break; - case MOBILE: phoneBuilder.setType(DataMessage.Contact.Phone.Type.MOBILE); break; - case CUSTOM: phoneBuilder.setType(DataMessage.Contact.Phone.Type.CUSTOM); break; - default: throw new AssertionError("Unknown type: " + phone.getType()); - } - - if (phone.getLabel().isPresent()) phoneBuilder.setLabel(phone.getLabel().get()); - - contactBuilder.addNumber(phoneBuilder); - } - } - - if (contact.getAvatar().isPresent()) { - AttachmentPointer pointer = contact.getAvatar().get().getAttachment().isStream() ? createAttachmentPointer(contact.getAvatar().get().getAttachment().asStream(), recipient) - : createAttachmentPointer(contact.getAvatar().get().getAttachment().asPointer()); - contactBuilder.setAvatar(DataMessage.Contact.Avatar.newBuilder() - .setAvatar(pointer) - .setIsProfile(contact.getAvatar().get().isProfile())); - } - - if (contact.getOrganization().isPresent()) { - contactBuilder.setOrganization(contact.getOrganization().get()); - } - - results.add(contactBuilder.build()); - } - - return results; - } - - private List sendMessage(long messageID, - List recipients, - List> unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean isClosedGroup, - boolean notifyPNServer) - { - List results = new LinkedList<>(); - Iterator recipientIterator = recipients.iterator(); - Iterator> unidentifiedAccessIterator = unidentifiedAccess.iterator(); - - while (recipientIterator.hasNext()) { - SignalServiceAddress recipient = recipientIterator.next(); - SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, true, isClosedGroup, notifyPNServer, Optional.absent()); - results.add(result); - } - - return results; - } - - private SendMessageResult sendMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption) - throws IOException - { - // Loki - This method is only invoked for various types of control messages - return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, useFallbackEncryption, false,Optional.absent()); - } - - public SendMessageResult sendMessage(final long messageID, - final SignalServiceAddress recipient, - Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isClosedGroup, - boolean notifyPNServer, - Optional syncTarget) - { - boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty(); - long threadID; - if (isSelfSend) { - threadID = threadDatabase.getThreadID(syncTarget.get()); - } else { - threadID = threadDatabase.getThreadID(recipient.getNumber()); - } - PublicChat publicChat = threadDatabase.getPublicChat(threadID); - if (publicChat != null) { - return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat); - } else { - return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget); - } - } - - private SendMessageResult sendMessageToPublicChat(final long messageID, - final SignalServiceAddress recipient, - long timestamp, - byte[] content, - PublicChat publicChat) { - if (messageID == 0) { - Log.d("Loki", "Missing message ID."); - } - final SettableFuture[] future = { new SettableFuture() }; - try { - DataMessage data = Content.parseFrom(content).getDataMessage(); - String body = (data.getBody() != null && data.getBody().length() > 0) ? data.getBody() : Long.toString(data.getTimestamp()); - PublicChatMessage.Quote quote = null; - if (data.hasQuote()) { - long quoteID = data.getQuote().getId(); - String quoteePublicKey = data.getQuote().getAuthor(); - long serverID = messageDatabase.getQuoteServerID(quoteID, quoteePublicKey); - quote = new PublicChatMessage.Quote(quoteID, quoteePublicKey, data.getQuote().getText(), serverID); - } - DataMessage.Preview linkPreview = (data.getPreviewList().size() > 0) ? data.getPreviewList().get(0) : null; - ArrayList attachments = new ArrayList<>(); - if (linkPreview != null && linkPreview.hasImage()) { - AttachmentPointer attachmentPointer = linkPreview.getImage(); - String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null; - attachments.add(new PublicChatMessage.Attachment( - PublicChatMessage.Attachment.Kind.LinkPreview, - publicChat.getServer(), - attachmentPointer.getId(), - attachmentPointer.getContentType(), - attachmentPointer.getSize(), - attachmentPointer.getFileName(), - attachmentPointer.getFlags(), - attachmentPointer.getWidth(), - attachmentPointer.getHeight(), - caption, - attachmentPointer.getUrl(), - linkPreview.getUrl(), - linkPreview.getTitle() - )); - } - for (AttachmentPointer attachmentPointer : data.getAttachmentsList()) { - String caption = attachmentPointer.hasCaption() ? attachmentPointer.getCaption() : null; - attachments.add(new PublicChatMessage.Attachment( - PublicChatMessage.Attachment.Kind.Attachment, - publicChat.getServer(), - attachmentPointer.getId(), - attachmentPointer.getContentType(), - attachmentPointer.getSize(), - attachmentPointer.getFileName(), - attachmentPointer.getFlags(), - attachmentPointer.getWidth(), - attachmentPointer.getHeight(), - caption, - attachmentPointer.getUrl(), - null, - null - )); - } - PublicChatMessage message = new PublicChatMessage(userPublicKey, "", body, timestamp, PublicChatAPI.getPublicChatMessageType(), quote, attachments); - byte[] privateKey = store.getIdentityKeyPair().getPrivateKey().serialize(); - new PublicChatAPI(userPublicKey, privateKey, apiDatabase, userDatabase, openGroupDatabase).sendMessage(message, publicChat.getChannel(), publicChat.getServer()).success(new Function1() { - - @Override - public Unit invoke(PublicChatMessage message) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - messageDatabase.setServerID(messageID, message.getServerID()); - f.set(Unit.INSTANCE); - return Unit.INSTANCE; - } - }).fail(new Function1() { - - @Override - public Unit invoke(Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } - }); - } catch (Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - } - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - try { - f.get(1, TimeUnit.MINUTES); - return SendMessageResult.success(recipient, false, false); - } catch (Exception exception) { - return SendMessageResult.networkFailure(recipient); - } - } - - private SendMessageResult sendMessageToPrivateChat(final long messageID, - final SignalServiceAddress recipient, - Optional unidentifiedAccess, - final long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isClosedGroup, - final boolean notifyPNServer, - Optional syncTarget) - { - final SettableFuture[] future = { new SettableFuture() }; - OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); - // Loki - Remove this when we have shared sender keys - // ======== - if (messages.getMessages().isEmpty()) { - return SendMessageResult.success(recipient, false, false); - } - // ======== - OutgoingPushMessage message = messages.getMessages().get(0); - final SignalServiceProtos.Envelope.Type type = SignalServiceProtos.Envelope.Type.valueOf(message.type); - final String senderID; - if (type == SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT) { - senderID = recipient.getNumber(); - } else if (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) { - senderID = ""; - } else { - senderID = userPublicKey; - } - final int senderDeviceID = (type == SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER) ? 0 : SignalServiceAddress.DEFAULT_DEVICE_ID; - // Make sure we have a valid ttl; otherwise default to 2 days - if (ttl <= 0) { ttl = TTLUtilities.INSTANCE.getFallbackMessageTTL(); } - final int regularMessageTTL = TTLUtilities.getTTL(TTLUtilities.MessageType.Regular); - final int __ttl = ttl; - final SignalMessageInfo messageInfo = new SignalMessageInfo(type, timestamp, senderID, senderDeviceID, message.content, recipient.getNumber(), ttl, false); - SnodeAPI.shared.sendSignalMessage(messageInfo).success(new Function1, Exception>>, Unit>() { - - @Override - public Unit invoke(Set, Exception>> promises) { - final boolean[] isSuccess = { false }; - final int[] promiseCount = {promises.size()}; - final int[] errorCount = { 0 }; - for (Promise, Exception> promise : promises) { - promise.success(new Function1, Unit>() { - - @Override - public Unit invoke(Map map) { - if (isSuccess[0]) { return Unit.INSTANCE; } // Succeed as soon as the first promise succeeds - if (__ttl == regularMessageTTL) { - broadcaster.broadcast("messageSent", timestamp); - } - isSuccess[0] = true; - if (notifyPNServer) { - PushNotificationAPI.shared.notify(messageInfo); - } - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.set(Unit.INSTANCE); - return Unit.INSTANCE; - } - }).fail(new Function1() { - - @Override - public Unit invoke(Exception exception) { - errorCount[0] += 1; - if (errorCount[0] != promiseCount[0]) { return Unit.INSTANCE; } // Only error out if all promises failed - if (__ttl == regularMessageTTL) { - broadcaster.broadcast("messageFailed", timestamp); - } - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } - }); - } - return Unit.INSTANCE; - } - }).fail(exception -> { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - }); - - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - try { - f.get(1, TimeUnit.MINUTES); - return SendMessageResult.success(recipient, false, true); - } catch (Exception exception) { - Throwable underlyingError = exception.getCause(); - if (underlyingError instanceof SnodeAPI.Error) { - return SendMessageResult.lokiAPIError(recipient, (SnodeAPI.Error)underlyingError); - } else { - return SendMessageResult.networkFailure(recipient); - } - } - } - - private List createAttachmentPointers(Optional> attachments, SignalServiceAddress recipient) - throws IOException - { - List pointers = new LinkedList<>(); - - if (!attachments.isPresent() || attachments.get().isEmpty()) { - Log.w(TAG, "No attachments present..."); - return pointers; - } - - for (SignalServiceAttachment attachment : attachments.get()) { - if (attachment.isStream()) { - Log.w(TAG, "Found attachment, creating pointer..."); - pointers.add(createAttachmentPointer(attachment.asStream(), recipient)); - } else if (attachment.isPointer()) { - Log.w(TAG, "Including existing attachment pointer..."); - pointers.add(createAttachmentPointer(attachment.asPointer())); - } - } - - return pointers; - } - - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) { - AttachmentPointer.Builder builder = AttachmentPointer.newBuilder() - .setContentType(attachment.getContentType()) - .setId(attachment.getId()) - .setKey(ByteString.copyFrom(attachment.getKey())) - .setDigest(ByteString.copyFrom(attachment.getDigest().get())) - .setSize(attachment.getSize().get()) - .setUrl(attachment.getUrl()); - - 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()); - } - - if (attachment.getVoiceNote()) { - builder.setFlags(AttachmentPointer.Flags.VOICE_MESSAGE_VALUE); - } - - if (attachment.getCaption().isPresent()) { - builder.setCaption(attachment.getCaption().get()); - } - - return builder.build(); - } - - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient) - throws IOException - { - return createAttachmentPointer(attachment, false, recipient); - } - - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, boolean usePadding, SignalServiceAddress recipient) - throws IOException - { - SignalServiceAttachmentPointer pointer = uploadAttachment(attachment, usePadding, recipient); - return createAttachmentPointer(pointer); - } - - private OutgoingPushMessageList getSessionProtocolEncryptedMessage(SignalServiceAddress recipient, long timestamp, byte[] plaintext) - { - List messages = new LinkedList<>(); - - String publicKey = recipient.getNumber(); // Could be a contact's public key or the public key of a SSK group - boolean isClosedGroup = apiDatabase.isClosedGroup(publicKey); - String encryptionPublicKey; - if (isClosedGroup) { - ECKeyPair encryptionKeyPair = apiDatabase.getLatestClosedGroupEncryptionKeyPair(publicKey); - encryptionPublicKey = HexEncodingKt.getHexEncodedPublicKey(encryptionKeyPair); - } else { - encryptionPublicKey = publicKey; - } - byte[] ciphertext = sessionProtocolImpl.encrypt(PushTransportDetails.getPaddedMessageBody(plaintext), encryptionPublicKey); - String body = Base64.encodeBytes(ciphertext); - int type = isClosedGroup ? SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT_VALUE : - SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE; - OutgoingPushMessage message = new OutgoingPushMessage(type, body); - messages.add(message); - - return new OutgoingPushMessageList(publicKey, timestamp, messages, false); - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/InvalidCiphertextException.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/InvalidCiphertextException.java deleted file mode 100644 index 717c76cece..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/InvalidCiphertextException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.session.libsignal.service.api.crypto; - -public class InvalidCiphertextException extends Exception { - public InvalidCiphertextException(Exception nested) { - super(nested); - } - - public InvalidCiphertextException(String s) { - super(s); - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UnidentifiedAccess.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UnidentifiedAccess.java deleted file mode 100644 index 176621f657..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UnidentifiedAccess.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.session.libsignal.service.api.crypto; - - -import org.session.libsignal.libsignal.util.ByteUtil; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.GCMParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public class UnidentifiedAccess { - - private final byte[] unidentifiedAccessKey; - - public UnidentifiedAccess(byte[] unidentifiedAccessKey) - { - this.unidentifiedAccessKey = unidentifiedAccessKey; - } - - public byte[] getUnidentifiedAccessKey() { - return unidentifiedAccessKey; - } - - public static byte[] deriveAccessKeyFrom(byte[] profileKey) { - try { - byte[] nonce = new byte[12]; - byte[] input = new byte[16]; - - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(profileKey, "AES"), new GCMParameterSpec(128, nonce)); - - byte[] ciphertext = cipher.doFinal(input); - - return ByteUtil.trim(ciphertext, 16); - } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) { - throw new AssertionError(e); - } - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UntrustedIdentityException.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UntrustedIdentityException.java deleted file mode 100644 index 4e168c63ba..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/UntrustedIdentityException.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.session.libsignal.service.api.crypto; - -import org.session.libsignal.libsignal.IdentityKey; - -public class UntrustedIdentityException extends Exception { - - private final IdentityKey identityKey; - private final String e164number; - - public UntrustedIdentityException(String s, String e164number, IdentityKey identityKey) { - super(s); - this.e164number = e164number; - this.identityKey = identityKey; - } - - public IdentityKey getIdentityKey() { - return identityKey; - } - - public String getE164Number() { - return e164number; - } - -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SendMessageResult.java b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SendMessageResult.java deleted file mode 100644 index 9221e4481f..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SendMessageResult.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.session.libsignal.service.api.messages; - - -import org.session.libsignal.libsignal.IdentityKey; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.session.libsignal.service.loki.api.SnodeAPI; - -public class SendMessageResult { - - private final SignalServiceAddress address; - private final Success success; - private final boolean networkFailure; - private final boolean unregisteredFailure; - private final IdentityFailure identityFailure; - private final SnodeAPI.Error lokiAPIError; - - public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync) { - return new SendMessageResult(address, new Success(unidentified, needsSync), false, false, null, null); - } - - public static SendMessageResult lokiAPIError(SignalServiceAddress address, SnodeAPI.Error lokiAPIError) { - return new SendMessageResult(address, null, false, false, null, lokiAPIError); - } - - public static SendMessageResult networkFailure(SignalServiceAddress address) { - return new SendMessageResult(address, null, true, false, null, null); - } - - public static SendMessageResult unregisteredFailure(SignalServiceAddress address) { - return new SendMessageResult(address, null, false, true, null, null); - } - - public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) { - return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey), null); - } - - public SignalServiceAddress getAddress() { - return address; - } - - public Success getSuccess() { - return success; - } - - public boolean isNetworkFailure() { - return networkFailure; - } - - public IdentityFailure getIdentityFailure() { - return identityFailure; - } - - public SnodeAPI.Error getLokiAPIError() { return lokiAPIError; } - - private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure, SnodeAPI.Error lokiAPIError) { - this.address = address; - this.success = success; - this.networkFailure = networkFailure; - this.unregisteredFailure = unregisteredFailure; - this.identityFailure = identityFailure; - this.lokiAPIError = lokiAPIError; - } - - public static class Success { - private final boolean unidentified; - private final boolean needsSync; - - private Success(boolean unidentified, boolean needsSync) { - this.unidentified = unidentified; - this.needsSync = needsSync; - } - - public boolean isUnidentified() { - return unidentified; - } - - public boolean isNeedsSync() { - return needsSync; - } - } - - public static class IdentityFailure { - private final IdentityKey identityKey; - - private IdentityFailure(IdentityKey identityKey) { - this.identityKey = identityKey; - } - - public IdentityKey getIdentityKey() { - return identityKey; - } - } - - - -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java deleted file mode 100644 index 2744ee97a5..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessage.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.session.libsignal.service.internal.push; - - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class OutgoingPushMessage { - - @JsonProperty - public int type; - @JsonProperty - public String content; - - public OutgoingPushMessage(int type, String content) - { - this.type = type; - this.content = content; - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java deleted file mode 100644 index 9a23aae9d5..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/OutgoingPushMessageList.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2014-2016 Open Whisper Systems - * - * Licensed according to the LICENSE file in this repository. - */ - -package org.session.libsignal.service.internal.push; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public class OutgoingPushMessageList { - - @JsonProperty - private String destination; - - @JsonProperty - private long timestamp; - - @JsonProperty - private List messages; - - @JsonProperty - private boolean online; - - public OutgoingPushMessageList(String destination, - long timestamp, - List messages, - boolean online) - { - this.timestamp = timestamp; - this.destination = destination; - this.messages = messages; - this.online = online; - } - - public String getDestination() { - return destination; - } - - public List getMessages() { - return messages; - } - - public long getTimestamp() { - return timestamp; - } - - public boolean isOnline() { - return online; - } -} diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/Snode.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/Snode.kt index 82b6a29d31..72c0eb1d4d 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/api/Snode.kt @@ -4,7 +4,7 @@ public class Snode(val address: String, val port: Int, val publicKeySet: KeySet? val ip: String get() = address.removePrefix("https://") - internal enum class Method(val rawValue: String) { + enum class Method(val rawValue: String) { /** * Only supported by snode targets. */ diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt index c095a1cd76..3851919052 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/api/crypto/SessionProtocol.kt @@ -17,17 +17,6 @@ interface SessionProtocol { object DecryptionFailed : Exception("Couldn't decrypt message.") object InvalidSignature : Exception("Invalid message signature.") } - - /** - * Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`. - * - * @param plaintext the plaintext to encrypt. Must already be padded. - * @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group. - * - * @return the encrypted message. - */ - fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray - /** * Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`. * diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/MnemonicCodec.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/MnemonicCodec.kt index 192218d399..b3c15dd132 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/MnemonicCodec.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/crypto/MnemonicCodec.kt @@ -50,7 +50,7 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) { } } - sealed class DecodingError(val description: String) : Exception() { + sealed class DecodingError(val description: String) : Exception(description) { object Generic : DecodingError("Something went wrong. Please check your mnemonic and try again.") object InputTooShort : DecodingError("Looks like you didn't enter enough words. Please check your mnemonic and try again.") object MissingLastWord : DecodingError("You seem to be missing the last word of your mnemonic. Please check what you entered and try again.")