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 6d232a61cc..468aae89ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -1,52 +1,68 @@ -//package org.thoughtcrime.securesms.attachments -// -//import android.content.Context -//import com.google.protobuf.ByteString -//import org.session.libsession.database.dto.DatabaseAttachmentDTO -//import org.session.libsession.database.MessageDataProvider -//import org.session.libsignal.service.internal.push.SignalServiceProtos -//import org.thoughtcrime.securesms.database.Database -//import org.thoughtcrime.securesms.database.DatabaseFactory -//import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper -//import org.thoughtcrime.securesms.util.MediaUtil -// -//class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider { -// override fun getAttachment(uniqueID: String): DatabaseAttachmentDTO? { -// -// val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) -// val uniqueID = uniqueID.toLongOrNull() ?: return null -// val attachmentID = AttachmentId(0, uniqueID) -// val databaseAttachment = attachmentDatabase.getAttachment(attachmentID) ?: return null -// -// return databaseAttachment.toDTO() -// } -// -//} -// -//// Extension to DatabaseAttachment class -// -//fun DatabaseAttachment.toDTO(): DatabaseAttachmentDTO { -// var databaseAttachmentDTO = DatabaseAttachmentDTO() -// databaseAttachmentDTO.contentType = this.contentType -// databaseAttachmentDTO.fileName = this.fileName -// databaseAttachmentDTO.caption = this.caption -// -// databaseAttachmentDTO.size = this.size.toInt() -// databaseAttachmentDTO.key = ByteString.copyFrom(this.key?.toByteArray()) -// databaseAttachmentDTO.digest = ByteString.copyFrom(this.digest) -// databaseAttachmentDTO.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 -// -// databaseAttachmentDTO.url = this.url -// -// if (this.shouldHaveImageSize()) { -// databaseAttachmentDTO.shouldHaveImageSize = true -// databaseAttachmentDTO.width = this.width -// databaseAttachmentDTO.height = this.height -// } -// -// return databaseAttachmentDTO -//} -// -//fun DatabaseAttachment.shouldHaveImageSize(): Boolean { -// return (MediaUtil.isVideo(this) || MediaUtil.isImage(this) || MediaUtil.isGif(this)); -//} \ No newline at end of file +package org.thoughtcrime.securesms.attachments + +import android.content.Context +import com.google.protobuf.ByteString +import org.session.libsession.database.dto.DatabaseAttachmentDTO +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.database.dto.AttachmentState +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.jobs.AttachmentUploadJob +import org.thoughtcrime.securesms.util.MediaUtil + +class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider { + override fun getAttachment(attachmentId: Long): DatabaseAttachmentDTO? { + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null + return databaseAttachment.toDTO() + } + + override fun setAttachmentState(attachmentState: AttachmentState, attachment: DatabaseAttachmentDTO, messageID: Long) { + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + attachmentDatabase.setTransferState(messageID, AttachmentId(attachment.attachmentId, 0), attachmentState.value) + } + + @Throws(Exception::class) + override fun uploadAttachment(attachmentId: Long) { + val attachmentUploadJob = AttachmentUploadJob(AttachmentId(attachmentId, 0), null) + attachmentUploadJob.onRun() + } + + override fun isOutgoingMessage(timestamp: Long): Boolean { + val smsDatabase = DatabaseFactory.getSmsDatabase(context) + return smsDatabase.isOutgoingMessage(timestamp) + } + +} + +// Extension to DatabaseAttachment class + +fun DatabaseAttachment.toDTO(): DatabaseAttachmentDTO { + var databaseAttachmentDTO = DatabaseAttachmentDTO() + databaseAttachmentDTO.attachmentId = this.attachmentId.rowId + databaseAttachmentDTO.contentType = this.contentType + databaseAttachmentDTO.fileName = this.fileName + databaseAttachmentDTO.caption = this.caption + + databaseAttachmentDTO.size = this.size.toInt() + databaseAttachmentDTO.key = ByteString.copyFrom(this.key?.toByteArray()) + databaseAttachmentDTO.digest = ByteString.copyFrom(this.digest) + databaseAttachmentDTO.flags = if (this.isVoiceNote) SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE.number else 0 + + databaseAttachmentDTO.url = this.url + + if (this.shouldHaveImageSize()) { + databaseAttachmentDTO.shouldHaveImageSize = true + databaseAttachmentDTO.width = this.width + databaseAttachmentDTO.height = this.height + } + + return databaseAttachmentDTO +} + +fun DatabaseAttachment.shouldHaveImageSize(): Boolean { + return (MediaUtil.isVideo(this) || MediaUtil.isImage(this) || MediaUtil.isGif(this)); +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index 2f089ff4ff..fc232cfda0 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -1,9 +1,18 @@ package org.session.libsession.database +import org.session.libsession.database.dto.AttachmentState import org.session.libsession.database.dto.DatabaseAttachmentDTO +import org.session.libsession.messaging.messages.visible.Attachment interface MessageDataProvider { - fun getAttachment(uniqueID: String): DatabaseAttachmentDTO? + fun getAttachment(attachmentId: Long): DatabaseAttachmentDTO? + + fun setAttachmentState(attachmentState: AttachmentState, attachment: DatabaseAttachmentDTO, messageID: Long) + + fun isOutgoingMessage(timestamp: Long): Boolean + + @Throws(Exception::class) + fun uploadAttachment(attachmentId: Long) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt index 1a7690fdb6..bf5f31f822 100644 --- a/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt +++ b/libsession/src/main/java/org/session/libsession/database/dto/DatabaseAttachmentDTO.kt @@ -6,6 +6,8 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos import kotlin.math.round class DatabaseAttachmentDTO { + var attachmentId: Long = 0 + var contentType: String? = null var fileName: String? = null 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 e0d880d94c..25c4437ee8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -35,7 +35,7 @@ interface StorageProtocol { fun markJobAsSucceeded(job: Job) fun markJobAsFailed(job: Job) fun getAllPendingJobs(type: String): List - fun getAttachmentUploadJob(attachmentID: String): AttachmentUploadJob? + fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun resumeMessageSendJobIfNeeded(messageSendJobID: String) fun isJobCanceled(job: Job): Boolean 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 4cf728a106..750a10824f 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 @@ -4,12 +4,12 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import okhttp3.Request +import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.libsignal.util.Hex import org.session.libsignal.service.internal.util.Base64 import org.session.libsignal.service.internal.util.JsonUtil import org.session.libsignal.service.loki.api.SnodeAPI -import org.session.libsignal.service.loki.api.LokiDotNetAPI import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink @@ -18,7 +18,7 @@ import java.net.URL import java.util.concurrent.ConcurrentHashMap import kotlin.collections.set -class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) { +class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() { companion object { // region Settings @@ -49,6 +49,7 @@ 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/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index 5f9ee29c6b..126a2a91ad 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 @@ -1,6 +1,13 @@ package org.session.libsession.messaging.jobs +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import org.session.libsession.messaging.sending_receiving.MessageReceiver +import org.session.libsession.messaging.sending_receiving.handle +import org.session.libsignal.libsignal.logging.Log + class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job { + override var delegate: JobDelegate? = null override var id: String? = null override var failureCount: Int = 0 @@ -8,10 +15,43 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val // Settings override val maxFailureCount: Int = 10 companion object { + val TAG = MessageReceiveJob::class.qualifiedName + val collection: String = "MessageReceiveJobCollection" } override fun execute() { - TODO("Not yet implemented") + exec() + } + + fun exec(): Promise { + val deferred = deferred() + try { + val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID) + MessageReceiver.handle(message, proto, this.openGroupID) + this.handleSuccess() + deferred.resolve(Unit) + } catch (e: Exception) { + Log.d(TAG, "Couldn't receive message due to error: $e.") + val error = e as? MessageReceiver.Error + error?.let { + if (!error.isRetryable) this.handlePermanentFailure(error) + } + this.handleFailure(e) + deferred.resolve(Unit) // The promise is just used to keep track of when we're done + } + return deferred.promise + } + + private fun handleSuccess() { + delegate?.handleJobSucceeded(this) + } + + private fun handlePermanentFailure(e: Exception) { + delegate?.handleJobFailedPermanently(this, e) + } + + private fun handleFailure(e: Exception) { + delegate?.handleJobFailed(this, e) } } \ No newline at end of file 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 71cab56c30..5d707d5e93 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 @@ -1,9 +1,15 @@ package org.session.libsession.messaging.jobs +import org.session.libsession.messaging.MessagingConfiguration 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.sending_receiving.MessageSender +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos class MessageSendJob(val message: Message, val destination: Destination) : Job { + override var delegate: JobDelegate? = null override var id: String? = null override var failureCount: Int = 0 @@ -11,10 +17,54 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { // Settings override val maxFailureCount: Int = 10 companion object { + val TAG = MessageSendJob::class.qualifiedName + val collection: String = "MessageSendJobCollection" } override fun execute() { - TODO("Not yet implemented") + val messageDataProvider = MessagingConfiguration.shared.messageDataProvider + val message = message as? VisibleMessage + message?.let { + if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted + val attachments = message.attachmentIDs.map { messageDataProvider.getAttachment(it) }.filterNotNull() + val attachmentsToUpload = attachments.filter { !it.isUploaded } + attachmentsToUpload.forEach { + if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) { + // Wait for it to finish + } else { + val job = AttachmentUploadJob(it.attachmentId, message.threadID!!, message, id!!) + JobQueue.shared.add(job) + } + } + if (attachmentsToUpload.isNotEmpty()) return // Wait for all attachments to upload before continuing + } + MessageSender.send(this.message, this.destination).success { + this.handleSuccess() + }.fail { exception -> + Log.e(TAG, "Couldn't send message due to error: $exception.") + val e = exception as? MessageSender.Error + e?.let { + if (!e.isRetryable) this.handlePermanentFailure(e) + } + this.handleFailure(exception) + } + } + + private fun handleSuccess() { + delegate?.handleJobSucceeded(this) + } + + private fun handlePermanentFailure(error: Exception) { + delegate?.handleJobFailedPermanently(this, error) + } + + private fun handleFailure(error: Exception) { + Log.w(TAG, "Failed to send $message::class.simpleName.") + val message = message as? VisibleMessage + message?.let { + if(!MessagingConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted + } + delegate?.handleJobFailed(this, error) } } \ No newline at end of file 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 246474b9a3..3999148dff 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 @@ -8,7 +8,7 @@ class LinkPreview() { var title: String? = null var url: String? = null - var attachmentID: String? = null + var attachmentID: Long? = 0 companion object { const val TAG = "LinkPreview" @@ -21,7 +21,7 @@ class LinkPreview() { } //constructor - internal constructor(title: String?, url: String, attachmentID: String?) : this() { + internal constructor(title: String?, url: String, attachmentID: Long?) : this() { this.title = title this.url = url this.attachmentID = attachmentID 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 3914072c36..ed6dbb29ea 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 @@ -11,7 +11,7 @@ class Quote() { var timestamp: Long? = 0 var publicKey: String? = null var text: String? = null - var attachmentID: String? = null + var attachmentID: Long? = null companion object { const val TAG = "Quote" @@ -25,7 +25,7 @@ class Quote() { } //constructor - internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: String?) : this() { + internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: Long?) : this() { this.timestamp = timestamp this.publicKey = publicKey this.text = text 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 a1344754e0..b761eb2846 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 @@ -11,7 +11,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class VisibleMessage : Message() { var text: String? = null - var attachmentIDs = ArrayList() + var attachmentIDs = ArrayList() var quote: Quote? = null var linkPreview: LinkPreview? = null var contact: Contact? = null