Merge branch 'dev' of https://github.com/loki-project/session-android into open-group-invitations

This commit is contained in:
Brice-W 2021-05-14 10:32:12 +10:00
commit f5a99b43c7
60 changed files with 871 additions and 801 deletions

View File

@ -8,7 +8,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.1.3'
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
@ -158,8 +158,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 158 def canonicalVersionCode = 162
def canonicalVersionName = "1.10.1" def canonicalVersionName = "1.10.3"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -327,7 +327,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this))) .setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
.setDependencyInjector(this) .setDependencyInjector(this)
.build()); .build());
JobQueue.getShared().resumePendingJobs();
} }
private void initializeDependencyInjection() { private void initializeDependencyInjection() {
@ -455,7 +454,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
poller.setUserPublicKey(userPublicKey); poller.setUserPublicKey(userPublicKey);
return; return;
} }
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
poller = new Poller(); poller = new Poller();
closedGroupPoller = new ClosedGroupPoller(); closedGroupPoller = new ClosedGroupPoller();
} }

View File

@ -193,8 +193,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getSessionJobDatabase(context).markJobAsSucceeded(jobId) DatabaseFactory.getSessionJobDatabase(context).markJobAsSucceeded(jobId)
} }
override fun markJobAsFailed(jobId: String) { override fun markJobAsFailedPermanently(jobId: String) {
DatabaseFactory.getSessionJobDatabase(context).markJobAsFailed(jobId) DatabaseFactory.getSessionJobDatabase(context).markJobAsFailedPermanently(jobId)
} }
override fun getAllPendingJobs(type: String): Map<String, Job?> { override fun getAllPendingJobs(type: String): Map<String, Job?> {
@ -261,7 +261,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor -> return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroupV2.fromJson(publicChatAsJson) OpenGroupV2.fromJSON(publicChatAsJson)
} }
} }
@ -585,7 +585,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val database = DatabaseFactory.getThreadDatabase(context) val database = DatabaseFactory.getThreadDatabase(context)
if (!openGroupID.isNullOrEmpty()) { if (!openGroupID.isNullOrEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
return database.getOrCreateThreadIdFor(recipient) return database.getThreadIdIfExistsFor(recipient)
} else if (!groupPublicKey.isNullOrEmpty()) { } else if (!groupPublicKey.isNullOrEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
return database.getOrCreateThreadIdFor(recipient) return database.getOrCreateThreadIdFor(recipient)

View File

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import java.util.LinkedList; import java.util.LinkedList;

View File

@ -7,7 +7,7 @@ import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec; import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec; import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec; import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.jobmanager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View File

@ -5,7 +5,7 @@ import android.content.Intent;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory; import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage; import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.jobmanager.impl;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.utilities.JsonUtil; import org.session.libsignal.utilities.JsonUtil;

View File

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsession.utilities.DownloadUtilities; import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream; import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsignal.utilities.externalstorage.NoExternalStorageException; import org.session.libsignal.utilities.externalstorage.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;

View File

@ -7,7 +7,7 @@ import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.avatars.AvatarHelper; import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.DownloadUtilities; import org.session.libsession.utilities.DownloadUtilities;

View File

@ -18,7 +18,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;

View File

@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;

View File

@ -195,7 +195,7 @@ class EnterChatURLFragment : Fragment() {
chip.chipIcon = drawable chip.chipIcon = drawable
chip.text = defaultGroup.name chip.text = defaultGroup.name
chip.setOnClickListener { chip.setOnClickListener {
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl()) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
} }
defaultRoomsGridLayout.addView(chip) defaultRoomsGridLayout.addView(chip)
} }

View File

@ -8,7 +8,6 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
@ -17,7 +16,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
@ -25,37 +23,15 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
companion object { companion object {
const val TAG = "BackgroundPollWorker" const val TAG = "BackgroundPollWorker"
private const val RETRY_ATTEMPTS = 3
@JvmStatic
fun scheduleInstant(context: Context) {
val workRequest = OneTimeWorkRequestBuilder<BackgroundPollWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager
.getInstance(context)
.enqueue(workRequest)
}
@JvmStatic @JvmStatic
fun schedulePeriodic(context: Context) { fun schedulePeriodic(context: Context) {
Log.v(TAG, "Scheduling periodic work.") Log.v(TAG, "Scheduling periodic work.")
val workRequest = PeriodicWorkRequestBuilder<BackgroundPollWorker>(15, TimeUnit.MINUTES) val builder = PeriodicWorkRequestBuilder<BackgroundPollWorker>(5, TimeUnit.MINUTES)
.setConstraints(Constraints.Builder() builder.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.setRequiredNetworkType(NetworkType.CONNECTED) val workRequest = builder.build()
.build() WorkManager.getInstance(context).enqueueUniquePeriodicWork(
)
.build()
WorkManager
.getInstance(context)
.enqueueUniquePeriodicWork(
TAG, TAG,
ExistingPeriodicWorkPolicy.KEEP, ExistingPeriodicWorkPolicy.REPLACE,
workRequest workRequest
) )
} }
@ -63,7 +39,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
override fun doWork(): Result { override fun doWork(): Result {
if (TextSecurePreferences.getLocalNumber(context) == null) { if (TextSecurePreferences.getLocalNumber(context) == null) {
Log.v(TAG, "Background poll is canceled due to the Session user is not set up yet.") Log.v(TAG, "User not registered yet.")
return Result.failure() return Result.failure()
} }
@ -71,43 +47,41 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
Log.v(TAG, "Performing background poll.") Log.v(TAG, "Performing background poll.")
val promises = mutableListOf<Promise<Unit, Exception>>() val promises = mutableListOf<Promise<Unit, Exception>>()
// Private chats // DMs
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes -> val dmsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
envelopes.map { envelope -> envelopes.map { envelope ->
// FIXME: Using a job here seems like a bad idea...
MessageReceiveJob(envelope.toByteArray(), false).executeAsync() MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
} }
} }
promises.addAll(privateChatsPromise.get()) promises.addAll(dmsPromise.get())
// Closed groups // Closed groups
promises.addAll(ClosedGroupPoller().pollOnce()) promises.addAll(ClosedGroupPoller().pollOnce())
// Open Groups // Open Groups
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { (_,chat)-> val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().values
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
}
for (openGroup in openGroups) { for (openGroup in openGroups) {
val poller = OpenGroupPoller(openGroup) val poller = OpenGroupPoller(openGroup)
promises.add(poller.pollForNewMessages()) promises.add(poller.pollForNewMessages())
} }
val openGroupsV2 = DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups().values.groupBy(OpenGroupV2::server) val v2OpenGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups().values.groupBy(OpenGroupV2::server)
openGroupsV2.values.map { groups -> v2OpenGroups.values.map { groups ->
OpenGroupV2Poller(groups) OpenGroupV2Poller(groups)
}.forEach { poller -> }.forEach { poller ->
promises.add(poller.compactPoll(true).map{ /*Unit*/ }) promises.add(poller.compactPoll(true).map { })
} }
// Wait till all the promises get resolved // Wait until all the promises are resolved
all(promises).get() all(promises).get()
return Result.success() return Result.success()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.v(TAG, "Background poll failed due to error: ${exception.message}.", exception) Log.e(TAG, "Background poll failed due to error: ${exception.message}.", exception)
return Result.retry()
return if (runAttemptCount < RETRY_ATTEMPTS) Result.retry() else Result.failure()
} }
} }
@ -116,8 +90,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) { if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Log.v(TAG, "Boot broadcast caught.") Log.v(TAG, "Boot broadcast caught.")
BackgroundPollWorker.scheduleInstant(context) schedulePeriodic(context)
BackgroundPollWorker.schedulePeriodic(context)
} }
} }
} }

View File

@ -5,7 +5,7 @@ import android.os.Build
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.jobs.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.sending_receiving.attachments.Attachment 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.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras

View File

@ -31,7 +31,7 @@ class PublicChatManager(private val context: Context) {
refreshChatsAndPollers() refreshChatsAndPollers()
for ((threadID, _) in chats) { for ((threadID, _) in chats) {
val poller = pollers[threadID] val poller = pollers[threadID]
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else areAllCaughtUp
} }
return areAllCaughtUp return areAllCaughtUp
} }
@ -42,6 +42,9 @@ class PublicChatManager(private val context: Context) {
val poller = pollers[threadID] ?: OpenGroupPoller(chat, executorService) val poller = pollers[threadID] ?: OpenGroupPoller(chat, executorService)
poller.isCaughtUp = false poller.isCaughtUp = false
} }
for ((_,poller) in v2Pollers) {
poller.isCaughtUp = false
}
} }
public fun startPollersIfNeeded() { public fun startPollersIfNeeded() {

View File

@ -23,7 +23,6 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> { override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey")
val signatureSize = Sign.BYTES val signatureSize = Sign.BYTES
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES val ed25519PublicKeySize = Sign.PUBLICKEYBYTES

View File

@ -68,7 +68,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID) val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat) val string = cursor.getString(publicChat)
val openGroup = OpenGroupV2.fromJson(string) val openGroup = OpenGroupV2.fromJSON(string)
if (openGroup != null) result[threadID] = openGroup if (openGroup != null) result[threadID] = openGroup
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -100,7 +100,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor -> return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
val json = cursor.getString(publicChat) val json = cursor.getString(publicChat)
OpenGroupV2.fromJson(json) OpenGroupV2.fromJSON(json)
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import net.sqlcipher.Cursor import net.sqlcipher.Cursor
import org.session.libsession.messaging.jobs.* import org.session.libsession.messaging.jobs.*
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
@ -18,45 +19,47 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
const val jobType = "job_type" const val jobType = "job_type"
const val failureCount = "failure_count" const val failureCount = "failure_count"
const val serializedData = "serialized_data" const val serializedData = "serialized_data"
@JvmStatic val createSessionJobTableCommand = "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);" @JvmStatic val createSessionJobTableCommand
= "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
} }
fun persistJob(job: Job) { fun persistJob(job: Job) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(4) val contentValues = ContentValues(4)
contentValues.put(jobID, job.id) contentValues.put(jobID, job.id!!)
contentValues.put(jobType, job.getFactoryKey()) contentValues.put(jobType, job.getFactoryKey())
contentValues.put(failureCount, job.failureCount) contentValues.put(failureCount, job.failureCount)
contentValues.put(serializedData, SessionJobHelper.dataSerializer.serialize(job.serialize())) contentValues.put(serializedData, SessionJobHelper.dataSerializer.serialize(job.serialize()))
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID)) database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf( job.id!! ))
} }
fun markJobAsSucceeded(jobId: String) { fun markJobAsSucceeded(jobID: String) {
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(jobId)) databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
} }
fun markJobAsFailed(jobId: String) { fun markJobAsFailedPermanently(jobID: String) {
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(jobId)) databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
} }
fun getAllPendingJobs(type: String): Map<String, Job?> { fun getAllPendingJobs(type: String): Map<String, Job?> {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor -> return database.getAll(sessionJobTable, "$jobType = ?", arrayOf( type )) { cursor ->
val jobId = cursor.getString(jobID) val jobID = cursor.getString(jobID)
try { try {
jobId to jobFromCursor(cursor) jobID to jobFromCursor(cursor)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Error serializing Job of type $type",e) Log.e("Loki", "Error deserializing job of type: $type.", e)
jobId to null jobID to null
} }
}.toMap() }.toMap()
} }
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? { fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
var result = mutableListOf<AttachmentUploadJob>() val result = mutableListOf<AttachmentUploadJob>()
database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor -> database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor ->
result.add(jobFromCursor(cursor) as AttachmentUploadJob) val job = jobFromCursor(cursor) as AttachmentUploadJob?
if (job != null) { result.add(job) }
} }
return result.firstOrNull { job -> job.attachmentID == attachmentID } return result.firstOrNull { job -> job.attachmentID == attachmentID }
} }
@ -64,7 +67,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? { fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor -> return database.get(sessionJobTable, "$jobID = ? AND $jobType = ?", arrayOf( messageSendJobID, MessageSendJob.KEY )) { cursor ->
jobFromCursor(cursor) as MessageSendJob jobFromCursor(cursor) as MessageSendJob?
} }
} }
@ -72,8 +75,8 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
var cursor: android.database.Cursor? = null var cursor: android.database.Cursor? = null
try { try {
cursor = database.rawQuery("SELECT * FROM $sessionJobTable WHERE $jobID = ?", arrayOf(job.id)) cursor = database.rawQuery("SELECT * FROM $sessionJobTable WHERE $jobID = ?", arrayOf( job.id!! ))
return cursor != null && cursor.moveToFirst() return cursor == null || !cursor.moveToFirst()
} catch (e: Exception) { } catch (e: Exception) {
// Do nothing // Do nothing
} finally { } finally {
@ -82,10 +85,10 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return false return false
} }
private fun jobFromCursor(cursor: Cursor): Job { private fun jobFromCursor(cursor: Cursor): Job? {
val type = cursor.getString(jobType) val type = cursor.getString(jobType)
val data = SessionJobHelper.dataSerializer.deserialize(cursor.getString(serializedData)) val data = SessionJobHelper.dataSerializer.deserialize(cursor.getString(serializedData))
val job = SessionJobHelper.sessionJobInstantiator.instantiate(type, data) val job = SessionJobHelper.sessionJobInstantiator.instantiate(type, data) ?: return null
job.id = cursor.getString(jobID) job.id = cursor.getString(jobID)
job.failureCount = cursor.getInt(failureCount) job.failureCount = cursor.getInt(failureCount)
return job return job

View File

@ -1,7 +1,7 @@
package org.thoughtcrime.securesms.jobmanager.impl; package org.thoughtcrime.securesms.jobmanager.impl;
import org.junit.Test; import org.junit.Test;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.utilities.Data;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import java.io.IOException; import java.io.IOException;

View File

@ -6,7 +6,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.gms:google-services:4.3.4" classpath "com.google.gms:google-services:4.3.4"
classpath files('libs/gradle-witness.jar') classpath files('libs/gradle-witness.jar')

View File

@ -8,8 +8,8 @@ class MessagingModuleConfiguration(
val context: Context, val context: Context,
val storage: StorageProtocol, val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider, val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol) val sessionProtocol: SessionProtocol
{ ) {
companion object { companion object {
lateinit var shared: MessagingModuleConfiguration lateinit var shared: MessagingModuleConfiguration

View File

@ -45,7 +45,7 @@ interface StorageProtocol {
// Jobs // Jobs
fun persistJob(job: Job) fun persistJob(job: Job)
fun markJobAsSucceeded(jobId: String) fun markJobAsSucceeded(jobId: String)
fun markJobAsFailed(jobId: String) fun markJobAsFailedPermanently(jobId: String)
fun getAllPendingJobs(type: String): Map<String,Job?> fun getAllPendingJobs(type: String): Map<String,Job?>
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob? fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? fun getMessageSendJob(messageSendJobID: String): MessageSendJob?

View File

@ -1,35 +1,26 @@
package org.session.libsession.messaging.file_server package org.session.libsession.messaging.file_server
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.service.loki.HTTP import org.session.libsignal.service.loki.HTTP
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
object FileServerAPIV2 { object FileServerAPIV2 {
const val DEFAULT_SERVER = "http://88.99.175.227"
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
const val DEFAULT_SERVER = "http://88.99.175.227"
sealed class Error : Exception() { sealed class Error(message: String) : Exception(message) {
object PARSING_FAILED : Error() object ParsingFailed : Error("Invalid response.")
object INVALID_URL : Error() object InvalidURL : Error("Invalid URL.")
fun errorDescription() = when (this) {
PARSING_FAILED -> "Invalid response."
INVALID_URL -> "Invalid URL."
}
} }
data class Request( data class Request(
@ -38,32 +29,31 @@ object FileServerAPIV2 {
val queryParameters: Map<String, String> = mapOf(), val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null, val parameters: Any? = null,
val headers: Map<String, String> = mapOf(), val headers: Map<String, String> = mapOf(),
// Always `true` under normal circumstances. You might want to disable /**
// this when running over Lokinet. * Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet.
*/
val useOnionRouting: Boolean = true val useOnionRouting: Boolean = true
) )
private fun createBody(parameters: Any?): RequestBody? { private fun createBody(parameters: Any?): RequestBody? {
if (parameters == null) return null if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters) val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
} }
private fun send(request: Request): Promise<Map<*, *>, Exception> { private fun send(request: Request): Promise<Map<*, *>, Exception> {
val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL) val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(parsed.scheme()) .scheme(url.scheme())
.host(parsed.host()) .host(url.host())
.port(parsed.port()) .port(url.port())
.addPathSegments(request.endpoint) .addPathSegments(request.endpoint)
if (request.verb == HTTP.Verb.GET) { if (request.verb == HTTP.Verb.GET) {
for ((key, value) in request.queryParameters) { for ((key, value) in request.queryParameters) {
urlBuilder.addQueryParameter(key, value) urlBuilder.addQueryParameter(key, value)
} }
} }
val requestBuilder = okhttp3.Request.Builder() val requestBuilder = okhttp3.Request.Builder()
.url(urlBuilder.build()) .url(urlBuilder.build())
.headers(Headers.of(request.headers)) .headers(Headers.of(request.headers))
@ -73,34 +63,29 @@ object FileServerAPIV2 {
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!) HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
if (request.useOnionRouting) { if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY) return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e ->
.fail { e -> Log.e("Loki", "File server request failed.", e)
Log.e("Loki", "FileServerV2 failed with error",e)
} }
} else { } else {
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests.")) return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
} }
} }
// region Sending
fun upload(file: ByteArray): Promise<Long, Exception> { fun upload(file: ByteArray): Promise<Long, Exception> {
val base64EncodedFile = Base64.encodeBytes(file) val base64EncodedFile = Base64.encodeBytes(file)
val parameters = mapOf( "file" to base64EncodedFile ) val parameters = mapOf( "file" to base64EncodedFile )
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters) val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
return send(request).map { json -> return send(request).map { json ->
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
} }
} }
fun download(file: Long): Promise<ByteArray, Exception> { fun download(file: Long): Promise<ByteArray, Exception> {
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file") val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
return send(request).map { json -> return send(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
} }
} }
} }

View File

@ -3,9 +3,9 @@ package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.utilities.DownloadUtilities import org.session.libsession.utilities.DownloadUtilities
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
@ -31,8 +31,8 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
val KEY: String = "AttachmentDownloadJob" val KEY: String = "AttachmentDownloadJob"
// Keys used for database storage // Keys used for database storage
private val KEY_ATTACHMENT_ID = "attachment_id" private val ATTACHMENT_ID_KEY = "attachment_id"
private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id" private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
} }
override fun execute() { override fun execute() {
@ -55,15 +55,16 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
val tempFile = createTempFile() val tempFile = createTempFile()
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID) val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString()) val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
val stream = if (openGroupV2 == null) { val stream = if (openGroupV2 == null) {
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null) DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
// Assume we're retrieving an attachment for an open group server if the digest is not set // Assume we're retrieving an attachment for an open group server if the digest is not set
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile) if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest) FileInputStream(tempFile)
} else {
AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
}
} else { } else {
val url = HttpUrl.parse(attachment.url)!! val url = HttpUrl.parse(attachment.url)!!
val fileId = url.pathSegments().last() val fileId = url.pathSegments().last()
@ -100,8 +101,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
override fun serialize(): Data { override fun serialize(): Data {
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID) return Data.Builder()
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID) .putLong(ATTACHMENT_ID_KEY, attachmentID)
.putLong(TS_INCOMING_MESSAGE_ID_KEY, databaseMessageID)
.build(); .build();
} }
@ -110,8 +112,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
class Factory : Job.Factory<AttachmentDownloadJob> { class Factory : Job.Factory<AttachmentDownloadJob> {
override fun create(data: Data): AttachmentDownloadJob { override fun create(data: Data): AttachmentDownloadJob {
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID)) return AttachmentDownloadJob(data.getLong(ATTACHMENT_ID_KEY), data.getLong(TS_INCOMING_MESSAGE_ID_KEY))
} }
} }
} }

View File

@ -8,6 +8,7 @@ import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
@ -30,44 +31,39 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
// Settings // Settings
override val maxFailureCount: Int = 20 override val maxFailureCount: Int = 20
companion object { companion object {
val TAG = AttachmentUploadJob::class.simpleName val TAG = AttachmentUploadJob::class.simpleName
val KEY: String = "AttachmentUploadJob" val KEY: String = "AttachmentUploadJob"
// Keys used for database storage // Keys used for database storage
private val KEY_ATTACHMENT_ID = "attachment_id" private val ATTACHMENT_ID_KEY = "attachment_id"
private val KEY_THREAD_ID = "thread_id" private val THREAD_ID_KEY = "thread_id"
private val KEY_MESSAGE = "message" private val MESSAGE_KEY = "message"
private val KEY_MESSAGE_SEND_JOB_ID = "message_send_job_id" private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
} }
override fun execute() { override fun execute() {
try { try {
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID) val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
val usePadding = false val usePadding = false
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID) val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID) val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
val server = openGroup?.let { val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server
it.server
} ?: openGroupV2?.let {
it.server
} ?: FileServerAPI.shared.server
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
val attachmentKey = Util.getSecretBytes(64) val attachmentKey = Util.getSecretBytes(64)
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory() val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener) val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
val uploadResult = if (openGroupV2 != null) {
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
val dataBytes = attachmentData.data.readBytes() val dataBytes = attachmentData.data.readBytes()
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get() val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf()) DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
} else {
FileServerAPI.shared.uploadAttachment(server, attachmentData)
} }
handleSuccess(attachment, attachmentKey, uploadResult) handleSuccess(attachment, attachmentKey, uploadResult)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
@ -82,7 +78,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
} }
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
Log.w(TAG, "Attachment uploaded successfully.") Log.d(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this) delegate?.handleJobSucceeded(this)
MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
@ -108,7 +104,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val messageSendJob = storage.getMessageSendJob(messageSendJobID) val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message, e) MessageSender.handleFailedMessageSend(this.message, e)
if (messageSendJob != null) { if (messageSendJob != null) {
storage.markJobAsFailed(messageSendJobID) storage.markJobAsFailedPermanently(messageSendJobID)
} }
} }
@ -119,10 +115,11 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val output = Output(serializedMessage) val output = Output(serializedMessage)
kryo.writeObject(output, message) kryo.writeObject(output, message)
output.close() output.close()
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID) return Data.Builder()
.putString(KEY_THREAD_ID, threadID) .putLong(ATTACHMENT_ID_KEY, attachmentID)
.putByteArray(KEY_MESSAGE, serializedMessage) .putString(THREAD_ID_KEY, threadID)
.putString(KEY_MESSAGE_SEND_JOB_ID, messageSendJobID) .putByteArray(MESSAGE_KEY, serializedMessage)
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
.build(); .build();
} }
@ -133,12 +130,18 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
class Factory: Job.Factory<AttachmentUploadJob> { class Factory: Job.Factory<AttachmentUploadJob> {
override fun create(data: Data): AttachmentUploadJob { override fun create(data: Data): AttachmentUploadJob {
val serializedMessage = data.getByteArray(KEY_MESSAGE) val serializedMessage = data.getByteArray(MESSAGE_KEY)
val kryo = Kryo() val kryo = Kryo()
kryo.isRegistrationRequired = false
val input = Input(serializedMessage) val input = Input(serializedMessage)
val message: Message = kryo.readObject(input, Message::class.java) val message: Message = kryo.readObject(input, Message::class.java)
input.close() input.close()
return AttachmentUploadJob(data.getLong(KEY_ATTACHMENT_ID), data.getString(KEY_THREAD_ID)!!, message, data.getString(KEY_MESSAGE_SEND_JOB_ID)!!) return AttachmentUploadJob(
data.getLong(ATTACHMENT_ID_KEY),
data.getString(THREAD_ID_KEY)!!,
message,
data.getString(MESSAGE_SEND_JOB_ID_KEY)!!
)
} }
} }
} }

View File

@ -1,5 +1,7 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.utilities.Data
interface Job { interface Job {
var delegate: JobDelegate? var delegate: JobDelegate?
var id: String? var id: String?
@ -8,21 +10,21 @@ interface Job {
val maxFailureCount: Int val maxFailureCount: Int
companion object { companion object {
// Keys used for database storage // Keys used for database storage
private val KEY_ID = "id" private val ID_KEY = "id"
private val KEY_FAILURE_COUNT = "failure_count" private val FAILURE_COUNT_KEY = "failure_count"
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
} }
fun execute() fun execute()
fun serialize(): Data fun serialize(): Data
/**
* Returns the key that can be used to find the relevant factory needed to create your job.
*/
fun getFactoryKey(): String fun getFactoryKey(): String
interface Factory<T : Job> { interface Factory<T : Job> {
fun create(data: Data): T
fun create(data: Data): T?
} }
} }

View File

@ -5,6 +5,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import java.lang.IllegalStateException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -17,44 +18,58 @@ import kotlin.math.roundToLong
class JobQueue : JobDelegate { class JobQueue : JobDelegate {
private var hasResumedPendingJobs = false // Just for debugging private var hasResumedPendingJobs = false // Just for debugging
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>() private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val multiDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val attachmentDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
private val scope = GlobalScope + SupervisorJob() private val scope = GlobalScope + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED) private val queue = Channel<Job>(UNLIMITED)
val timer = Timer() val timer = Timer()
private fun CoroutineScope.processWithDispatcher(channel: Channel<Job>, dispatcher: CoroutineDispatcher) = launch(dispatcher) {
for (job in channel) {
if (!isActive) break
job.delegate = this@JobQueue
job.execute()
}
}
init { init {
// Process jobs // Process jobs
scope.launch(dispatcher) { scope.launch {
val rxQueue = Channel<Job>(capacity = 1024)
val txQueue = Channel<Job>(capacity = 1024)
val attachmentQueue = Channel<Job>(capacity = 1024)
val receiveJob = processWithDispatcher(rxQueue, rxDispatcher)
val txJob = processWithDispatcher(txQueue, txDispatcher)
val attachmentJob = processWithDispatcher(attachmentQueue, attachmentDispatcher)
while (isActive) { while (isActive) {
queue.receive().let { job -> for (job in queue) {
if (job.canExecuteParallel()) { when (job) {
launch(multiDispatcher) { is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> txQueue.send(job)
job.delegate = this@JobQueue is AttachmentDownloadJob -> attachmentQueue.send(job)
job.execute() is MessageReceiveJob -> rxQueue.send(job)
} else -> throw IllegalStateException("Unexpected job type.")
} else {
job.delegate = this@JobQueue
job.execute()
} }
} }
} }
// The job has been cancelled
receiveJob.cancel()
txJob.cancel()
attachmentJob.cancel()
} }
} }
companion object { companion object {
@JvmStatic @JvmStatic
val shared: JobQueue by lazy { JobQueue() } val shared: JobQueue by lazy { JobQueue() }
} }
private fun Job.canExecuteParallel(): Boolean {
return this.javaClass in arrayOf(
AttachmentUploadJob::class.java,
AttachmentDownloadJob::class.java
)
}
fun add(job: Job) { fun add(job: Job) {
addWithoutExecuting(job) addWithoutExecuting(job)
queue.offer(job) // offer always called on unlimited capacity queue.offer(job) // offer always called on unlimited capacity
@ -68,7 +83,6 @@ class JobQueue : JobDelegate {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger()) jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString() job.id = currentTime.toString() + jobTimestampMap[currentTime]!!.getAndIncrement().toString()
MessagingModuleConfiguration.shared.storage.persistJob(job) MessagingModuleConfiguration.shared.storage.persistJob(job)
} }
@ -78,7 +92,8 @@ class JobQueue : JobDelegate {
return return
} }
hasResumedPendingJobs = true hasResumedPendingJobs = true
val allJobTypes = listOf(AttachmentUploadJob.KEY, val allJobTypes = listOf(
AttachmentUploadJob.KEY,
AttachmentDownloadJob.KEY, AttachmentDownloadJob.KEY,
MessageReceiveJob.KEY, MessageReceiveJob.KEY,
MessageSendJob.KEY, MessageSendJob.KEY,
@ -89,14 +104,14 @@ class JobQueue : JobDelegate {
val pendingJobs = mutableListOf<Job>() val pendingJobs = mutableListOf<Job>()
for ((id, job) in allPendingJobs) { for ((id, job) in allPendingJobs) {
if (job == null) { if (job == null) {
// job failed to serialize, remove it from the DB // Job failed to deserialize, remove it from the DB
handleJobFailedPermanently(id) handleJobFailedPermanently(id)
} else { } else {
pendingJobs.add(job) pendingJobs.add(job)
} }
} }
pendingJobs.sortedBy { it.id }.forEach { job -> pendingJobs.sortedBy { it.id }.forEach { job ->
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.") Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.")
queue.offer(job) // Offer always called on unlimited capacity queue.offer(job) // Offer always called on unlimited capacity
} }
} }
@ -108,17 +123,31 @@ class JobQueue : JobDelegate {
} }
override fun handleJobFailed(job: Job, error: Exception) { override fun handleJobFailed(job: Job, error: Exception) {
job.failureCount += 1 // Canceled
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")} if (storage.isJobCanceled(job)) {
if (job.failureCount == job.maxFailureCount) { return Log.i("Loki", "${job::class.simpleName} canceled.")
}
// Message send jobs waiting for the attachment to upload
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
val retryInterval: Long = 1000 * 4
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
timer.schedule(delay = retryInterval) {
Log.i("Loki", "Retrying ${job::class.simpleName}.")
queue.offer(job)
}
return
}
// Regular job failure
job.failureCount += 1
if (job.failureCount >= job.maxFailureCount) {
handleJobFailedPermanently(job, error) handleJobFailedPermanently(job, error)
} else { } else {
storage.persistJob(job) storage.persistJob(job)
val retryInterval = getRetryInterval(job) val retryInterval = getRetryInterval(job)
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).") Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
timer.schedule(delay = retryInterval) { timer.schedule(delay = retryInterval) {
Log.i("Jobs", "Retrying ${job::class.simpleName}.") Log.i("Loki", "Retrying ${job::class.simpleName}.")
queue.offer(job) queue.offer(job)
} }
} }
@ -131,7 +160,7 @@ class JobQueue : JobDelegate {
private fun handleJobFailedPermanently(jobId: String) { private fun handleJobFailedPermanently(jobId: String) {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
storage.markJobAsFailed(jobId) storage.markJobAsFailedPermanently(jobId)
} }
private fun getRetryInterval(job: Job): Long { private fun getRetryInterval(job: Job): Long {

View File

@ -4,6 +4,7 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.messaging.sending_receiving.handle import org.session.libsession.messaging.sending_receiving.handle
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job { class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
@ -11,7 +12,6 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
override var id: String? = null override var id: String? = null
override var failureCount: Int = 0 override var failureCount: Int = 0
// Settings
override val maxFailureCount: Int = 10 override val maxFailureCount: Int = 10
companion object { companion object {
val TAG = MessageReceiveJob::class.simpleName val TAG = MessageReceiveJob::class.simpleName
@ -20,10 +20,11 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
private val RECEIVE_LOCK = Object() private val RECEIVE_LOCK = Object()
// Keys used for database storage // Keys used for database storage
private val KEY_DATA = "data" private val DATA_KEY = "data"
private val KEY_IS_BACKGROUND_POLL = "is_background_poll" // FIXME: We probably shouldn't be using this job when background polling
private val KEY_OPEN_GROUP_MESSAGE_SERVER_ID = "openGroupMessageServerID" private val IS_BACKGROUND_POLL_KEY = "is_background_poll"
private val KEY_OPEN_GROUP_ID = "open_group_id" private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
private val OPEN_GROUP_ID_KEY = "open_group_id"
} }
override fun execute() { override fun execute() {
@ -35,19 +36,18 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
try { try {
val isRetry: Boolean = failureCount != 0 val isRetry: Boolean = failureCount != 0
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry) val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
synchronized(RECEIVE_LOCK) { synchronized(RECEIVE_LOCK) { // FIXME: Do we need this?
MessageReceiver.handle(message, proto, this.openGroupID) MessageReceiver.handle(message, proto, this.openGroupID)
} }
this.handleSuccess() this.handleSuccess()
deferred.resolve(Unit) deferred.resolve(Unit)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Couldn't receive message due to error", e) Log.e(TAG, "Couldn't receive message.", e)
val error = e as? MessageReceiver.Error if (e is MessageReceiver.Error && !e.isRetryable) {
if (error != null && !error.isRetryable) { Log.e("Loki", "Message receive job permanently failed.", e)
Log.e("Loki", "Message receive job permanently failed due to error", e) this.handlePermanentFailure(e)
this.handlePermanentFailure(error)
} else { } else {
Log.e("Loki", "Couldn't receive message due to error", e) Log.e("Loki", "Couldn't receive message.", e)
this.handleFailure(e) this.handleFailure(e)
} }
deferred.resolve(Unit) // The promise is just used to keep track of when we're done deferred.resolve(Unit) // The promise is just used to keep track of when we're done
@ -68,10 +68,10 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
} }
override fun serialize(): Data { override fun serialize(): Data {
val builder = Data.Builder().putByteArray(KEY_DATA, data) val builder = Data.Builder().putByteArray(DATA_KEY, data)
.putBoolean(KEY_IS_BACKGROUND_POLL, isBackgroundPoll) .putBoolean(IS_BACKGROUND_POLL_KEY, isBackgroundPoll)
openGroupMessageServerID?.let { builder.putLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID, openGroupMessageServerID) } openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
openGroupID?.let { builder.putString(KEY_OPEN_GROUP_ID, openGroupID) } openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
return builder.build(); return builder.build();
} }
@ -82,7 +82,12 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
class Factory: Job.Factory<MessageReceiveJob> { class Factory: Job.Factory<MessageReceiveJob> {
override fun create(data: Data): MessageReceiveJob { override fun create(data: Data): MessageReceiveJob {
return MessageReceiveJob(data.getByteArray(KEY_DATA), data.getBoolean(KEY_IS_BACKGROUND_POLL), data.getLong(KEY_OPEN_GROUP_MESSAGE_SERVER_ID), data.getString(KEY_OPEN_GROUP_ID)) return MessageReceiveJob(
data.getByteArray(DATA_KEY),
data.getBoolean(IS_BACKGROUND_POLL_KEY),
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
data.getString(OPEN_GROUP_ID_KEY)
)
} }
} }
} }

View File

@ -4,32 +4,37 @@ import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.Job.Companion.MAX_BUFFER_SIZE
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class MessageSendJob(val message: Message, val destination: Destination) : Job { class MessageSendJob(val message: Message, val destination: Destination) : Job {
object AwaitingAttachmentUploadException : Exception("Awaiting attachment upload.")
override var delegate: JobDelegate? = null override var delegate: JobDelegate? = null
override var id: String? = null override var id: String? = null
override var failureCount: Int = 0 override var failureCount: Int = 0
// Settings
override val maxFailureCount: Int = 10 override val maxFailureCount: Int = 10
companion object { companion object {
val TAG = MessageSendJob::class.simpleName val TAG = MessageSendJob::class.simpleName
val KEY: String = "MessageSendJob" val KEY: String = "MessageSendJob"
// Keys used for database storage // Keys used for database storage
private val KEY_MESSAGE = "message" private val MESSAGE_KEY = "message"
private val KEY_DESTINATION = "destination" private val DESTINATION_KEY = "destination"
} }
override fun execute() { override fun execute() {
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val message = message as? VisibleMessage val message = message as? VisibleMessage
message?.let { if (message != null) {
if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
val attachmentIDs = mutableListOf<Long>() val attachmentIDs = mutableListOf<Long>()
attachmentIDs.addAll(message.attachmentIDs) attachmentIDs.addAll(message.attachmentIDs)
@ -45,15 +50,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
JobQueue.shared.add(job) JobQueue.shared.add(job)
} }
} }
if (attachmentsToUpload.isNotEmpty()) return // Wait for all attachments to upload before continuing if (attachmentsToUpload.isNotEmpty()) {
this.handleFailure(AwaitingAttachmentUploadException)
return
} // Wait for all attachments to upload before continuing
} }
MessageSender.send(this.message, this.destination).success { MessageSender.send(this.message, this.destination).success {
this.handleSuccess() this.handleSuccess()
}.fail { exception -> }.fail { exception ->
Log.e(TAG, "Couldn't send message due to error: $exception.") Log.e(TAG, "Couldn't send message due to error: $exception.")
val e = exception as? MessageSender.Error if (exception is MessageSender.Error) {
e?.let { if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
if (!e.isRetryable) this.handlePermanentFailure(e)
} }
this.handleFailure(exception) this.handleFailure(exception)
} }
@ -70,8 +77,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
private fun handleFailure(error: Exception) { private fun handleFailure(error: Exception) {
Log.w(TAG, "Failed to send $message::class.simpleName.") Log.w(TAG, "Failed to send $message::class.simpleName.")
val message = message as? VisibleMessage val message = message as? VisibleMessage
message?.let { if (message != null) {
if(!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted if (!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) {
return // The message has been deleted
}
} }
delegate?.handleJobFailed(this, error) delegate?.handleJobFailed(this, error)
} }
@ -79,17 +88,22 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
override fun serialize(): Data { override fun serialize(): Data {
val kryo = Kryo() val kryo = Kryo()
kryo.isRegistrationRequired = false kryo.isRegistrationRequired = false
val output = Output(ByteArray(4096), 10_000_000) val output = Output(ByteArray(4096), MAX_BUFFER_SIZE)
// Message
kryo.writeClassAndObject(output, message) kryo.writeClassAndObject(output, message)
output.close() output.close()
val serializedMessage = output.toBytes() val serializedMessage = output.toBytes()
output.clear() output.clear()
// Destination
kryo.writeClassAndObject(output, destination) kryo.writeClassAndObject(output, destination)
output.close() output.close()
val serializedDestination = output.toBytes() val serializedDestination = output.toBytes()
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage) output.clear()
.putByteArray(KEY_DESTINATION, serializedDestination) // Serialize
.build(); return Data.Builder()
.putByteArray(MESSAGE_KEY, serializedMessage)
.putByteArray(DESTINATION_KEY, serializedDestination)
.build()
} }
override fun getFactoryKey(): String { override fun getFactoryKey(): String {
@ -98,16 +112,31 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
class Factory : Job.Factory<MessageSendJob> { class Factory : Job.Factory<MessageSendJob> {
override fun create(data: Data): MessageSendJob { override fun create(data: Data): MessageSendJob? {
val serializedMessage = data.getByteArray(KEY_MESSAGE) val serializedMessage = data.getByteArray(MESSAGE_KEY)
val serializedDestination = data.getByteArray(KEY_DESTINATION) val serializedDestination = data.getByteArray(DESTINATION_KEY)
val kryo = Kryo() val kryo = Kryo()
var input = Input(serializedMessage) // Message
val message = kryo.readClassAndObject(input) as Message val messageInput = Input(serializedMessage)
input.close() val message: Message
input = Input(serializedDestination) try {
val destination = kryo.readClassAndObject(input) as Destination message = kryo.readClassAndObject(messageInput) as Message
input.close() } catch (e: Exception) {
Log.e("Loki", "Couldn't deserialize message send job.", e)
return null
}
messageInput.close()
// Destination
val destinationInput = Input(serializedDestination)
val destination: Destination
try {
destination = kryo.readClassAndObject(destinationInput) as Destination
} catch (e: Exception) {
Log.e("Loki", "Couldn't deserialize message send job.", e)
return null
}
destinationInput.close()
// Return
return MessageSendJob(message, destination) return MessageSendJob(message, destination)
} }
} }

View File

@ -9,6 +9,7 @@ import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
@ -21,16 +22,14 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
override var id: String? = null override var id: String? = null
override var failureCount: Int = 0 override var failureCount: Int = 0
// Settings
override val maxFailureCount: Int = 20 override val maxFailureCount: Int = 20
companion object { companion object {
val KEY: String = "NotifyPNServerJob" val KEY: String = "NotifyPNServerJob"
// Keys used for database storage // Keys used for database storage
private val KEY_MESSAGE = "message" private val MESSAGE_KEY = "message"
} }
// Running
override fun execute() { override fun execute() {
val server = PushNotificationAPI.server val server = PushNotificationAPI.server
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
@ -41,10 +40,10 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json -> OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
val code = json["code"] as? Int val code = json["code"] as? Int
if (code == null || code == 0) { if (code == null || code == 0) {
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.") Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")
} }
}.fail { exception -> }.fail { exception ->
Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.") Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
} }
}.success { }.success {
handleSuccess() handleSuccess()
@ -68,7 +67,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val output = Output(serializedMessage) val output = Output(serializedMessage)
kryo.writeObject(output, message) kryo.writeObject(output, message)
output.close() output.close()
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage).build(); return Data.Builder().putByteArray(MESSAGE_KEY, serializedMessage).build();
} }
override fun getFactoryKey(): String { override fun getFactoryKey(): String {
@ -78,8 +77,9 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
class Factory : Job.Factory<NotifyPNServerJob> { class Factory : Job.Factory<NotifyPNServerJob> {
override fun create(data: Data): NotifyPNServerJob { override fun create(data: Data): NotifyPNServerJob {
val serializedMessage = data.getByteArray(KEY_MESSAGE) val serializedMessage = data.getByteArray(MESSAGE_KEY)
val kryo = Kryo() val kryo = Kryo()
kryo.isRegistrationRequired = false
val input = Input(serializedMessage) val input = Input(serializedMessage)
val message: SnodeMessage = kryo.readObject(input, SnodeMessage::class.java) val message: SnodeMessage = kryo.readObject(input, SnodeMessage::class.java)
input.close() input.close()

View File

@ -1,12 +1,14 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.utilities.Data
class SessionJobInstantiator(private val jobFactories: Map<String, Job.Factory<out Job>>) { class SessionJobInstantiator(private val jobFactories: Map<String, Job.Factory<out Job>>) {
fun instantiate(jobFactoryKey: String, data: Data): Job { fun instantiate(jobFactoryKey: String, data: Data): Job? {
if (jobFactories.containsKey(jobFactoryKey)) { if (jobFactories.containsKey(jobFactoryKey)) {
return jobFactories[jobFactoryKey]?.create(data) ?: throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.") return jobFactories[jobFactoryKey]?.create(data)
} else { } else {
throw IllegalStateException("Tried to instantiate a job with key '$jobFactoryKey', but no matching factory was found.") return null
} }
} }
} }

View File

@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
class SessionJobManagerFactories { class SessionJobManagerFactories {
companion object { companion object {
fun getSessionJobFactories(): Map<String, Job.Factory<out Job>> { fun getSessionJobFactories(): Map<String, Job.Factory<out Job>> {
return mapOf( return mapOf(
AttachmentDownloadJob.KEY to AttachmentDownloadJob.Factory(), AttachmentDownloadJob.KEY to AttachmentDownloadJob.Factory(),

View File

@ -7,9 +7,6 @@ import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
typealias OpenGroupModel = OpenGroup
typealias OpenGroupV2Model = OpenGroupV2
sealed class Destination { sealed class Destination {
class Contact(var publicKey: String) : Destination() { class Contact(var publicKey: String) : Destination() {
@ -26,6 +23,7 @@ sealed class Destination {
} }
companion object { companion object {
fun from(address: Address): Destination { fun from(address: Address): Destination {
return when { return when {
address.isContact -> { address.isContact -> {
@ -39,10 +37,12 @@ sealed class Destination {
address.isOpenGroup -> { address.isOpenGroup -> {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val threadID = storage.getThreadID(address.contactIdentifier())!! val threadID = storage.getThreadID(address.contactIdentifier())!!
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) { when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server) is org.session.libsession.messaging.open_groups.OpenGroup
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server) -> Destination.OpenGroup(openGroup.channel, openGroup.server)
else -> throw Exception("Invalid OpenGroup $openGroup") is org.session.libsession.messaging.open_groups.OpenGroupV2
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
else -> throw Exception("Missing open group for thread with ID: $threadID.")
} }
} }
else -> { else -> {

View File

@ -18,12 +18,10 @@ abstract class Message {
open val isSelfSendValid: Boolean = false open val isSelfSendValid: Boolean = false
open fun isValid(): Boolean { open fun isValid(): Boolean {
sentTimestamp?.let { val sentTimestamp = sentTimestamp
if (it <= 0) return false if (sentTimestamp != null && sentTimestamp <= 0) { return false }
} val receivedTimestamp = receivedTimestamp
receivedTimestamp?.let { if (receivedTimestamp != null && receivedTimestamp <= 0) { return false }
if (it <= 0) return false
}
return sender != null && recipient != null return sender != null && recipient != null
} }

View File

@ -16,9 +16,10 @@ import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class ClosedGroupControlMessage() : ControlMessage() { class ClosedGroupControlMessage() : ControlMessage() {
var kind: Kind? = null
override val ttl: Long = run { override val ttl: Long get() {
when (kind) { return when (kind) {
is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000 is Kind.EncryptionKeyPair -> 14 * 24 * 60 * 60 * 1000
else -> 14 * 24 * 60 * 60 * 1000 else -> 14 * 24 * 60 * 60 * 1000
} }
@ -26,15 +27,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
override val isSelfSendValid: Boolean = true override val isSelfSendValid: Boolean = true
var kind: Kind? = null override fun isValid(): Boolean {
val kind = kind
if (!super.isValid() || kind == null) 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()
}
is Kind.EncryptionKeyPair -> true
is Kind.NameChange -> kind.name.isNotEmpty()
is Kind.MembersAdded -> kind.members.isNotEmpty()
is Kind.MembersRemoved -> kind.members.isNotEmpty()
is Kind.MemberLeft -> true
}
}
sealed class Kind { sealed class Kind {
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() { class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf()) internal constructor() : this(ByteString.EMPTY, "", null, listOf(), listOf())
} }
/// An encryption key pair encrypted for each member individually. /** 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). * **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(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() { class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
internal constructor() : this(null, listOf()) internal constructor() : this(null, listOf())
} }
@ -65,18 +81,19 @@ class ClosedGroupControlMessage() : ControlMessage() {
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? { fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!! val closedGroupControlMessageProto = proto.dataMessage!!.closedGroupControlMessage!!
val kind: Kind val kind: Kind
when (closedGroupControlMessageProto.type) { when (closedGroupControlMessageProto.type!!) {
DataMessage.ClosedGroupControlMessage.Type.NEW -> { DataMessage.ClosedGroupControlMessage.Type.NEW -> {
val publicKey = closedGroupControlMessageProto.publicKey ?: return null val publicKey = closedGroupControlMessageProto.publicKey ?: return null
val name = closedGroupControlMessageProto.name ?: return null val name = closedGroupControlMessageProto.name ?: return null
val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null
try { try {
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()),
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList) kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't parse key pair") Log.w(TAG, "Couldn't parse key pair from proto: $encryptionKeyPairAsProto.")
return null return null
} }
} }
@ -107,26 +124,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
this.kind = kind this.kind = kind
} }
override fun isValid(): Boolean {
if (!super.isValid()) return false
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()
}
is Kind.EncryptionKeyPair -> true
is Kind.NameChange -> kind.name.isNotEmpty()
is Kind.MembersAdded -> kind.members.isNotEmpty()
is Kind.MembersRemoved -> kind.members.isNotEmpty()
is Kind.MemberLeft -> true
}
}
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {
val kind = kind val kind = kind
if (kind == null) { if (kind == null) {
Log.w(TAG, "Couldn't construct closed group update proto from: $this") Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
return null return null
} }
try { try {
@ -176,7 +177,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
contentProto.dataMessage = dataMessageProto.build() contentProto.dataMessage = dataMessageProto.build()
return contentProto.build() return contentProto.build()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Couldn't construct closed group update proto from: $this") Log.w(TAG, "Couldn't construct closed group control message proto from: $this.")
return null return null
} }
} }
@ -188,6 +189,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
} }
companion object { companion object {
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper { fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair) return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
} }
@ -199,7 +201,6 @@ class ClosedGroupControlMessage() : ControlMessage() {
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder() val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey)) result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.encryptedKeyPair = encryptedKeyPair result.encryptedKeyPair = encryptedKeyPair
return try { return try {
result.build() result.build()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -14,7 +14,10 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() { class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>,
var displayName: String, var profilePicture: String?, var profileKey: ByteArray) : ControlMessage() {
override val isSelfSendValid: Boolean = true
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) { class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty() val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
@ -66,7 +69,6 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
val name = proto.name val name = proto.name
val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null val profilePicture = if (proto.hasProfilePicture()) proto.profilePicture else null
val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null val profileKey = if (proto.hasProfileKey()) proto.profileKey.toByteArray() else null
return Contact(publicKey, name, profilePicture, profileKey) return Contact(publicKey, name, profilePicture, profileKey)
} }
} }
@ -79,18 +81,18 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
} catch (e: Exception) { } catch (e: Exception) {
return null return null
} }
if (!this.profilePicture.isNullOrEmpty()) { val profilePicture = profilePicture
result.profilePicture = this.profilePicture if (!profilePicture.isNullOrEmpty()) {
result.profilePicture = profilePicture
} }
if (this.profileKey != null) { val profileKey = profileKey
result.profileKey = ByteString.copyFrom(this.profileKey) if (profileKey != null) {
result.profileKey = ByteString.copyFrom(profileKey)
} }
return result.build() return result.build()
} }
} }
override val isSelfSendValid: Boolean = true
companion object { companion object {
fun getCurrent(contacts: List<Contact>): ConfigurationMessage? { fun getCurrent(contacts: List<Contact>): ConfigurationMessage? {
@ -103,24 +105,22 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
val profilePicture = TextSecurePreferences.getProfilePictureURL(context) val profilePicture = TextSecurePreferences.getProfilePictureURL(context)
val profileKey = ProfileKeyUtil.getProfileKey(context) val profileKey = ProfileKeyUtil.getProfileKey(context)
val groups = storage.getAllGroups() val groups = storage.getAllGroups()
for (groupRecord in groups) { for (group in groups) {
if (groupRecord.isClosedGroup) { if (group.isClosedGroup) {
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue if (!group.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupRecord.encodedId).toHexString() val groupPublicKey = GroupUtil.doubleDecodeGroupID(group.encodedId).toHexString()
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() }) val closedGroup = ClosedGroup(groupPublicKey, group.title, encryptionKeyPair, group.members.map { it.serialize() }, group.admins.map { it.serialize() })
closedGroups.add(closedGroup) closedGroups.add(closedGroup)
} }
if (groupRecord.isOpenGroup) { if (group.isOpenGroup) {
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue val threadID = storage.getThreadID(group.encodedId) ?: continue
val openGroup = storage.getOpenGroup(threadID) val openGroup = storage.getOpenGroup(threadID)
val openGroupV2 = storage.getV2OpenGroup(threadID) val openGroupV2 = storage.getV2OpenGroup(threadID)
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue
openGroups.add(shareUrl) openGroups.add(shareUrl)
} }
} }
return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey) return ConfigurationMessage(closedGroups, openGroups, contacts, displayName, profilePicture, profileKey)
} }
@ -145,6 +145,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
configurationProto.addAllOpenGroups(openGroups) configurationProto.addAllOpenGroups(openGroups)
configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() }) configurationProto.addAllContacts(this.contacts.mapNotNull { it.toProto() })
configurationProto.displayName = displayName configurationProto.displayName = displayName
val profilePicture = profilePicture
if (!profilePicture.isNullOrEmpty()) { if (!profilePicture.isNullOrEmpty()) {
configurationProto.profilePicture = profilePicture configurationProto.profilePicture = profilePicture
} }
@ -157,10 +158,10 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
override fun toString(): String { override fun toString(): String {
return """ return """
ConfigurationMessage( ConfigurationMessage(
closedGroups: ${(closedGroups)} closedGroups: ${(closedGroups)},
openGroups: ${(openGroups)} openGroups: ${(openGroups)},
displayName: $displayName displayName: $displayName,
profilePicture: $profilePicture profilePicture: $profilePicture,
profileKey: $profileKey profileKey: $profileKey
) )
""".trimIndent() """.trimIndent()

View File

@ -2,5 +2,4 @@ package org.session.libsession.messaging.messages.control
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
abstract class ControlMessage : Message() { abstract class ControlMessage : Message()
}

View File

@ -39,8 +39,8 @@ class DataExtractionNotification(): ControlMessage() {
} }
override fun isValid(): Boolean { override fun isValid(): Boolean {
if (!super.isValid()) return false val kind = kind
val kind = kind ?: return false if (!super.isValid() || kind == null) return false
return when(kind) { return when(kind) {
is Kind.Screenshot -> true is Kind.Screenshot -> true
is Kind.MediaSaved -> kind.timestamp > 0 is Kind.MediaSaved -> kind.timestamp > 0

View File

@ -6,13 +6,20 @@ import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
class ExpirationTimerUpdate() : ControlMessage() { class ExpirationTimerUpdate() : ControlMessage() {
/// In the case of a sync message, the public key of the person the message was targeted at. /** In the case of a sync message, the public key of the person the message was targeted at.
/// - Note: `nil` if this isn't a sync message. *
* **Note:** `nil` if this isn't a sync message.
*/
var syncTarget: String? = null var syncTarget: String? = null
var duration: Int? = 0 var duration: Int? = 0
override val isSelfSendValid: Boolean = true override val isSelfSendValid: Boolean = true
override fun isValid(): Boolean {
if (!super.isValid()) return false
return duration != null
}
companion object { companion object {
const val TAG = "ExpirationTimerUpdate" const val TAG = "ExpirationTimerUpdate"
@ -26,19 +33,14 @@ class ExpirationTimerUpdate() : ControlMessage() {
} }
} }
internal constructor(syncTarget: String?, duration: Int) : this() {
this.syncTarget = syncTarget
this.duration = duration
}
internal constructor(duration: Int) : this() { internal constructor(duration: Int) : this() {
this.syncTarget = null this.syncTarget = null
this.duration = duration this.duration = duration
} }
override fun isValid(): Boolean { internal constructor(syncTarget: String, duration: Int) : this() {
if (!super.isValid()) return false this.syncTarget = syncTarget
return duration != null this.duration = duration
} }
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {

View File

@ -6,6 +6,13 @@ import org.session.libsignal.utilities.logging.Log
class ReadReceipt() : ControlMessage() { class ReadReceipt() : ControlMessage() {
var timestamps: List<Long>? = null var timestamps: List<Long>? = null
override fun isValid(): Boolean {
if (!super.isValid()) return false
val timestamps = timestamps ?: return false
if (timestamps.isNotEmpty()) { return true }
return false
}
companion object { companion object {
const val TAG = "ReadReceipt" const val TAG = "ReadReceipt"
@ -22,13 +29,6 @@ class ReadReceipt() : ControlMessage() {
this.timestamps = timestamps this.timestamps = timestamps
} }
override fun isValid(): Boolean {
if (!super.isValid()) return false
val timestamps = timestamps ?: return false
if (timestamps.isNotEmpty()) { return true }
return false
}
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {
val timestamps = timestamps val timestamps = timestamps
if (timestamps == null) { if (timestamps == null) {

View File

@ -4,9 +4,15 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class TypingIndicator() : ControlMessage() { class TypingIndicator() : ControlMessage() {
override val ttl: Long = 30 * 1000
var kind: Kind? = null var kind: Kind? = null
override val ttl: Long = 20 * 1000
override fun isValid(): Boolean {
if (!super.isValid()) return false
return kind != null
}
companion object { companion object {
const val TAG = "TypingIndicator" const val TAG = "TypingIndicator"
@ -41,11 +47,6 @@ class TypingIndicator() : ControlMessage() {
this.kind = kind this.kind = kind
} }
override fun isValid(): Boolean {
if (!super.isValid()) return false
return kind != null
}
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {
val timestamp = sentTimestamp val timestamp = sentTimestamp
val kind = kind val kind = kind

View File

@ -10,6 +10,10 @@ class LinkPreview() {
var url: String? = null var url: String? = null
var attachmentID: Long? = 0 var attachmentID: Long? = 0
fun isValid(): Boolean {
return (title != null && url != null && attachmentID != null)
}
companion object { companion object {
const val TAG = "LinkPreview" const val TAG = "LinkPreview"
@ -20,11 +24,8 @@ class LinkPreview() {
} }
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? { fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
return if (signalLinkPreview == null) { if (signalLinkPreview == null) { return null }
null return LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
} else {
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
}
} }
} }
@ -34,10 +35,6 @@ class LinkPreview() {
this.attachmentID = attachmentID this.attachmentID = attachmentID
} }
fun isValid(): Boolean {
return (title != null && url != null && attachmentID != null)
}
fun toProto(): SignalServiceProtos.DataMessage.Preview? { fun toProto(): SignalServiceProtos.DataMessage.Preview? {
val url = url val url = url
if (url == null) { if (url == null) {
@ -46,10 +43,10 @@ class LinkPreview() {
} }
val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder()
linkPreviewProto.url = url linkPreviewProto.url = url
title?.let { linkPreviewProto.title = title } title?.let { linkPreviewProto.title = it }
val attachmentID = attachmentID val database = MessagingModuleConfiguration.shared.messageDataProvider
attachmentID?.let { attachmentID?.let {
MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let { database.getSignalAttachmentPointer(it)?.let {
val attachmentProto = Attachment.createAttachmentPointer(it) val attachmentProto = Attachment.createAttachmentPointer(it)
linkPreviewProto.image = attachmentProto linkPreviewProto.image = attachmentProto
} }

View File

@ -17,14 +17,13 @@ class Profile() {
val displayName = profileProto.displayName ?: return null val displayName = profileProto.displayName ?: return null
val profileKey = proto.profileKey val profileKey = proto.profileKey
val profilePictureURL = profileProto.profilePicture val profilePictureURL = profileProto.profilePicture
profileKey?.let { if (profileKey != null && profilePictureURL != null) {
profilePictureURL?.let { return Profile(displayName, profileKey.toByteArray(), profilePictureURL)
return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) } else {
}
}
return Profile(displayName) return Profile(displayName)
} }
} }
}
internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() { internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() {
this.displayName = displayName this.displayName = displayName
@ -35,16 +34,14 @@ class Profile() {
fun toProto(): SignalServiceProtos.DataMessage? { fun toProto(): SignalServiceProtos.DataMessage? {
val displayName = displayName val displayName = displayName
if (displayName == null) { if (displayName == null) {
Log.w(TAG, "Couldn't construct link preview proto from: $this") Log.w(TAG, "Couldn't construct profile proto from: $this")
return null return null
} }
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder() val profileProto = SignalServiceProtos.DataMessage.LokiProfile.newBuilder()
profileProto.displayName = displayName profileProto.displayName = displayName
val profileKey = profileKey profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(it) }
profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) } profilePictureURL?.let { profileProto.profilePicture = it }
val profilePictureURL = profilePictureURL
profilePictureURL?.let { profileProto.profilePicture = profilePictureURL }
// Build // Build
try { try {
dataMessageProto.profile = profileProto.build() dataMessageProto.profile = profileProto.build()

View File

@ -13,6 +13,10 @@ class Quote() {
var text: String? = null var text: String? = null
var attachmentID: Long? = null var attachmentID: Long? = null
fun isValid(): Boolean {
return (timestamp != null && publicKey != null)
}
companion object { companion object {
const val TAG = "Quote" const val TAG = "Quote"
@ -24,12 +28,9 @@ class Quote() {
} }
fun from(signalQuote: SignalQuote?): Quote? { fun from(signalQuote: SignalQuote?): Quote? {
return if (signalQuote == null) { if (signalQuote == null) { return null }
null
} else {
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID) return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
}
} }
} }
@ -40,10 +41,6 @@ class Quote() {
this.attachmentID = attachmentID this.attachmentID = attachmentID
} }
fun isValid(): Boolean {
return (timestamp != null && publicKey != null)
}
fun toProto(): SignalServiceProtos.DataMessage.Quote? { fun toProto(): SignalServiceProtos.DataMessage.Quote? {
val timestamp = timestamp val timestamp = timestamp
val publicKey = publicKey val publicKey = publicKey
@ -54,7 +51,7 @@ class Quote() {
val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder() val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder()
quoteProto.id = timestamp quoteProto.id = timestamp
quoteProto.author = publicKey quoteProto.author = publicKey
text?.let { quoteProto.text = text } text?.let { quoteProto.text = it }
addAttachmentsIfNeeded(quoteProto) addAttachmentsIfNeeded(quoteProto)
// Build // Build
try { try {
@ -66,23 +63,23 @@ class Quote() {
} }
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) { private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
if (attachmentID == null) return val attachmentID = attachmentID ?: return
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!) val database = MessagingModuleConfiguration.shared.messageDataProvider
if (attachment == null) { val pointer = database.getSignalAttachmentPointer(attachmentID)
if (pointer == null) {
Log.w(TAG, "Ignoring invalid attachment for quoted message.") Log.w(TAG, "Ignoring invalid attachment for quoted message.")
return return
} }
if (attachment.url.isNullOrEmpty()) { if (pointer.url.isNullOrEmpty()) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//TODO equivalent to iOS's preconditionFailure Log.w(TAG,"Sending a message before all associated attachments have been uploaded.")
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
return return
} }
} }
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
quotedAttachmentProto.contentType = attachment.contentType quotedAttachmentProto.contentType = pointer.contentType
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get() if (pointer.fileName.isPresent) { quotedAttachmentProto.fileName = pointer.fileName.get() }
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment) quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(pointer)
try { try {
quoteProto.addAttachments(quotedAttachmentProto.build()) quoteProto.addAttachments(quotedAttachmentProto.build())
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -12,6 +12,10 @@ 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.attachments.Attachment as SignalAttachment
class VisibleMessage : Message() { class VisibleMessage : Message() {
/** In the case of a sync message, the public key of the person the message was targeted at.
*
* **Note:** `nil` if this isn't a sync message.
*/
var syncTarget: String? = null var syncTarget: String? = null
var text: String? = null var text: String? = null
val attachmentIDs: MutableList<Long> = mutableListOf() val attachmentIDs: MutableList<Long> = mutableListOf()
@ -22,51 +26,7 @@ class VisibleMessage : Message() {
override val isSelfSendValid: Boolean = true override val isSelfSendValid: Boolean = true
companion object { // region Validation
const val TAG = "VisibleMessage"
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
val dataMessage = if (proto.hasDataMessage()) proto.dataMessage else return null
val result = VisibleMessage()
if (dataMessage.hasSyncTarget()) {
result.syncTarget = dataMessage.syncTarget
}
result.text = dataMessage.body
// Attachments are handled in MessageReceiver
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
quoteProto?.let {
val quote = Quote.fromProto(quoteProto)
quote?.let { result.quote = quote }
}
val linkPreviewProto = dataMessage.previewList.firstOrNull()
linkPreviewProto?.let {
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
linkPreview?.let { result.linkPreview = linkPreview }
}
val openGroupInvitationProto = if (dataMessage.hasOpenGroupInvitation()) dataMessage.openGroupInvitation else null
openGroupInvitationProto?.let {
val openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto)
openGroupInvitation?.let { result.openGroupInvitation = openGroupInvitation}
}
// TODO Contact
val profile = Profile.fromProto(dataMessage)
profile?.let { result.profile = profile }
return result
}
}
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
val attachmentIDs = signalAttachments.map {
val databaseAttachment = it as DatabaseAttachment
databaseAttachment.attachmentId.rowId
}
this.attachmentIDs.addAll(attachmentIDs)
}
fun isMediaMessage(): Boolean {
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
}
override fun isValid(): Boolean { override fun isValid(): Boolean {
if (!super.isValid()) return false if (!super.isValid()) return false
if (attachmentIDs.isNotEmpty()) return true if (attachmentIDs.isNotEmpty()) return true
@ -75,55 +35,88 @@ class VisibleMessage : Message() {
if (text.isNotEmpty()) return true if (text.isNotEmpty()) return true
return false return false
} }
// endregion
// region Proto Conversion
companion object {
const val TAG = "VisibleMessage"
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
val dataMessage = proto.dataMessage ?: return null
val result = VisibleMessage()
if (dataMessage.hasSyncTarget()) { result.syncTarget = dataMessage.syncTarget }
result.text = dataMessage.body
// Attachments are handled in MessageReceiver
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
if (quoteProto != null) {
val quote = Quote.fromProto(quoteProto)
result.quote = quote
}
val linkPreviewProto = dataMessage.previewList.firstOrNull()
if (linkPreviewProto != null) {
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
result.linkPreview = linkPreview
}
val openGroupInvitationProto = if (dataMessage.hasOpenGroupInvitation()) dataMessage.openGroupInvitation else null
openGroupInvitationProto?.let {
val openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto)
openGroupInvitation?.let { result.openGroupInvitation = openGroupInvitation}
}
// TODO Contact
val profile = Profile.fromProto(dataMessage)
if (profile != null) { result.profile = profile }
return result
}
}
override fun toProto(): SignalServiceProtos.Content? { override fun toProto(): SignalServiceProtos.Content? {
val proto = SignalServiceProtos.Content.newBuilder() val proto = SignalServiceProtos.Content.newBuilder()
val dataMessage: SignalServiceProtos.DataMessage.Builder val dataMessage: SignalServiceProtos.DataMessage.Builder
// Profile // Profile
val profile = profile val profileProto = profile?.let { it.toProto() }
val profileProto = profile?.toProto()
if (profileProto != null) { if (profileProto != null) {
dataMessage = profileProto.toBuilder() dataMessage = profileProto.toBuilder()
} else { } else {
dataMessage = SignalServiceProtos.DataMessage.newBuilder() dataMessage = SignalServiceProtos.DataMessage.newBuilder()
} }
// Text // Text
text?.let { dataMessage.body = text } if (text != null) { dataMessage.body = text }
// Quote // Quote
quote?.let { val quoteProto = quote?.let { it.toProto() }
val quoteProto = it.toProto() if (quoteProto != null) {
if (quoteProto != null) dataMessage.quote = quoteProto dataMessage.quote = quoteProto
} }
// Link preview // Link preview
linkPreview?.let { val linkPreviewProto = linkPreview?.let { it.toProto() }
val linkPreviewProto = it.toProto() if (linkPreviewProto != null) {
linkPreviewProto?.let {
dataMessage.addAllPreview(listOf(linkPreviewProto)) dataMessage.addAllPreview(listOf(linkPreviewProto))
} }
}
//Open group invitation //Open group invitation
openGroupInvitation?.let { openGroupInvitation?.let {
val openGroupInvitationProto = it.toProto() val openGroupInvitationProto = it.toProto()
if (openGroupInvitationProto != null) dataMessage.openGroupInvitation = openGroupInvitationProto if (openGroupInvitationProto != null) dataMessage.openGroupInvitation = openGroupInvitationProto
} }
// Attachments // Attachments
val attachments = attachmentIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) } val database = MessagingModuleConfiguration.shared.messageDataProvider
if (!attachments.all { !it.url.isNullOrEmpty() }) { val attachments = attachmentIDs.mapNotNull { database.getSignalAttachmentPointer(it) }
if (attachments.any { it.url.isNullOrEmpty() }) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//TODO equivalent to iOS's preconditionFailure Log.w(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) } val pointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
dataMessage.addAllAttachments(attachmentPointers) dataMessage.addAllAttachments(pointers)
// TODO Contact // TODO: Contact
// Expiration timer // Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation // 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... // if it receives a message without the current expiration timer value attached to it...
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages val expiration = if (storage.isClosedGroup(recipient!!)) {
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
} else {
Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
}
dataMessage.expireTimer = expiration dataMessage.expireTimer = expiration
// Group context // Group context
if (storage.isClosedGroup(recipient!!)) { if (storage.isClosedGroup(recipient!!)) {
@ -147,4 +140,17 @@ class VisibleMessage : Message() {
return null return null
} }
} }
// endregion
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
val attachmentIDs = signalAttachments.map {
val databaseAttachment = it as DatabaseAttachment
databaseAttachment.attachmentId.rowId
}
this.attachmentIDs.addAll(attachmentIDs)
}
fun isMediaMessage(): Boolean {
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null
}
} }

View File

@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming
import com.fasterxml.jackson.databind.type.TypeFactory import com.fasterxml.jackson.databind.type.TypeFactory
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
@ -14,7 +13,6 @@ import okhttp3.HttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsignal.service.loki.HTTP import org.session.libsignal.service.loki.HTTP
@ -29,62 +27,38 @@ import org.whispersystems.curve25519.Curve25519
import java.util.* import java.util.*
object OpenGroupAPIV2 { object OpenGroupAPIV2 {
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
const val DEFAULT_SERVER = "http://116.203.70.33" private val curve = Curve25519.getInstance(Curve25519.BEST)
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1) val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
private val curve = Curve25519.getInstance(Curve25519.BEST) private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
const val DEFAULT_SERVER = "http://116.203.70.33"
sealed class Error : Exception() { sealed class Error(message: String) : Exception(message) {
object GENERIC : Error() object Generic : Error("An error occurred.")
object PARSING_FAILED : Error() object ParsingFailed : Error("Invalid response.")
object DECRYPTION_FAILED : Error() object DecryptionFailed : Error("Couldn't decrypt response.")
object SIGNING_FAILED : Error() object SigningFailed : Error("Couldn't sign message.")
object INVALID_URL : Error() object InvalidURL : Error("Invalid URL.")
object NO_PUBLIC_KEY : Error() object NoPublicKey : Error("Couldn't find server public key.")
fun errorDescription() = when (this) {
Error.GENERIC -> "An error occurred."
Error.PARSING_FAILED -> "Invalid response."
Error.DECRYPTION_FAILED -> "Couldn't decrypt response."
Error.SIGNING_FAILED -> "Couldn't sign message."
Error.INVALID_URL -> "Invalid URL."
Error.NO_PUBLIC_KEY -> "Couldn't find server public key."
} }
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
} }
data class DefaultGroup(val id: String, data class Info(val id: String, val name: String, val imageID: String?)
val name: String,
val image: ByteArray?) {
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
}
data class Info(
val id: String,
val name: String,
val imageID: String?
)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class CompactPollRequest(val roomId: String, data class CompactPollRequest(val roomID: String, val authToken: String, val fromDeletionServerID: Long?, val fromMessageServerID: Long?)
val authToken: String, data class CompactPollResult(val messages: List<OpenGroupMessageV2>, val deletions: List<Long>, val moderators: List<String>)
val fromDeletionServerId: Long?,
val fromMessageServerId: Long?
)
data class CompactPollResult(val messages: List<OpenGroupMessageV2>,
val deletions: List<Long>,
val moderators: List<String>
)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0, data class MessageDeletion
val deletedMessageId: Long = 0 @JvmOverloads constructor(val id: Long = 0, val deletedMessageId: Long = 0
) { ) {
companion object { companion object {
val EMPTY = MessageDeletion() val EMPTY = MessageDeletion()
} }
@ -99,38 +73,37 @@ object OpenGroupAPIV2 {
val parameters: Any? = null, val parameters: Any? = null,
val headers: Map<String, String> = mapOf(), val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true, val isAuthRequired: Boolean = true,
// Always `true` under normal circumstances. You might want to disable /**
// this when running over Lokinet. * Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet.
*/
val useOnionRouting: Boolean = true val useOnionRouting: Boolean = true
) )
private fun createBody(parameters: Any?): RequestBody? { private fun createBody(parameters: Any?): RequestBody? {
if (parameters == null) return null if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters) val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
} }
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> { private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL) val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(parsed.scheme()) .scheme(url.scheme())
.host(parsed.host()) .host(url.host())
.port(parsed.port()) .port(url.port())
.addPathSegments(request.endpoint) .addPathSegments(request.endpoint)
if (request.verb == GET) { if (request.verb == GET) {
for ((key, value) in request.queryParameters) { for ((key, value) in request.queryParameters) {
urlBuilder.addQueryParameter(key, value) urlBuilder.addQueryParameter(key, value)
} }
} }
fun execute(token: String?): Promise<Map<*, *>, Exception> { fun execute(token: String?): Promise<Map<*, *>, Exception> {
val requestBuilder = okhttp3.Request.Builder() val requestBuilder = okhttp3.Request.Builder()
.url(urlBuilder.build()) .url(urlBuilder.build())
.headers(Headers.of(request.headers)) .headers(Headers.of(request.headers))
if (request.isAuthRequired) { if (request.isAuthRequired) {
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request") if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
requestBuilder.header("Authorization", token) requestBuilder.header("Authorization", token)
} }
when (request.verb) { when (request.verb) {
@ -139,16 +112,16 @@ object OpenGroupAPIV2 {
POST -> requestBuilder.post(createBody(request.parameters)!!) POST -> requestBuilder.post(createBody(request.parameters)!!)
DELETE -> requestBuilder.delete(createBody(request.parameters)) DELETE -> requestBuilder.delete(createBody(request.parameters))
} }
if (!request.room.isNullOrEmpty()) { if (!request.room.isNullOrEmpty()) {
requestBuilder.header("Room", request.room) requestBuilder.header("Room", request.room)
} }
if (request.useOnionRouting) { if (request.useOnionRouting) {
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NO_PUBLIC_KEY) ?: return Promise.ofFail(Error.NoPublicKey)
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired) return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired).fail { e ->
.fail { e -> // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) { if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
if (request.room != null) { if (request.room != null) {
@ -172,11 +145,12 @@ object OpenGroupAPIV2 {
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> { fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false) val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
return send(request).map { json -> return send(request).map { json ->
val result = json["result"] as? String ?: throw Error.PARSING_FAILED val result = json["result"] as? String ?: throw Error.ParsingFailed
decode(result) decode(result)
} }
} }
// region Authorization
fun getAuthToken(room: String, server: String): Promise<String, Exception> { fun getAuthToken(room: String, server: String): Promise<String, Exception> {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
return storage.getAuthToken(room, server)?.let { return storage.getAuthToken(room, server)?.let {
@ -192,22 +166,20 @@ object OpenGroupAPIV2 {
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> { fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
?: return Promise.ofFail(Error.GENERIC) ?: return Promise.ofFail(Error.Generic)
val queryParameters = mutableMapOf( "public_key" to publicKey ) val queryParameters = mutableMapOf( "public_key" to publicKey )
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null) val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
return send(request).map { json -> return send(request).map { json ->
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
val base64EncodedCiphertext = challenge["ciphertext"] as? String val base64EncodedCiphertext = challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
?: throw Error.PARSING_FAILED val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
?: throw Error.PARSING_FAILED
val ciphertext = decode(base64EncodedCiphertext) val ciphertext = decode(base64EncodedCiphertext)
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey) val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey) val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
val tokenAsData = try { val tokenAsData = try {
AESGCM.decrypt(ciphertext, symmetricKey) AESGCM.decrypt(ciphertext, symmetricKey)
} catch (e: Exception) { } catch (e: Exception) {
throw Error.DECRYPTION_FAILED throw Error.DecryptionFailed
} }
tokenAsData.toHexString() tokenAsData.toHexString()
} }
@ -227,33 +199,36 @@ object OpenGroupAPIV2 {
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server) MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
} }
} }
// endregion
// region Sending // region Upload/Download
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> { fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
val base64EncodedFile = encodeBytes(file) val base64EncodedFile = encodeBytes(file)
val parameters = mapOf( "file" to base64EncodedFile ) val parameters = mapOf( "file" to base64EncodedFile )
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters) val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
return send(request).map { json -> return send(request).map { json ->
json["result"] as? Long ?: throw Error.PARSING_FAILED json["result"] as? Long ?: throw Error.ParsingFailed
} }
} }
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> { fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file") val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
return send(request).map { json -> return send(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED decode(base64EncodedFile) ?: throw Error.ParsingFailed
} }
} }
// endregion
// region Sending
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> { fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED) val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed)
val jsonMessage = signedMessage.toJSON() val jsonMessage = signedMessage.toJSON()
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage) val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
return send(request).map { json -> return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any> @Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
?: throw Error.PARSING_FAILED ?: throw Error.ParsingFailed
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
} }
} }
// endregion // endregion
@ -266,13 +241,19 @@ object OpenGroupAPIV2 {
queryParameters += "from_server_id" to lastId.toString() queryParameters += "from_server_id" to lastId.toString()
} }
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters) val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
return send(request).map { jsonList -> return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>> @Suppress("UNCHECKED_CAST") val rawMessages = json["messages"] as? List<Map<String, Any>>
?: throw Error.PARSING_FAILED ?: throw Error.ParsingFailed
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0 parseMessages(room, server, rawMessages)
}
}
var currentMax = lastMessageServerId private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
val storage = MessagingModuleConfiguration.shared.storage
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
var currentLastMessageServerID = lastMessageServerID
val messages = rawMessages.mapNotNull { json -> val messages = rawMessages.mapNotNull { json ->
json as Map<String, Any>
try { try {
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
@ -282,20 +263,19 @@ object OpenGroupAPIV2 {
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded()) val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
val isValid = curve.verifySignature(publicKey, data, signature) val isValid = curve.verifySignature(publicKey, data, signature)
if (!isValid) { if (!isValid) {
Log.d("Loki", "Ignoring message with invalid signature") Log.d("Loki", "Ignoring message with invalid signature.")
return@mapNotNull null return@mapNotNull null
} }
if (message.serverID > lastMessageServerId) { if (message.serverID > lastMessageServerID) {
currentMax = message.serverID currentLastMessageServerID = message.serverID
} }
message message
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
storage.setLastMessageServerId(room, server, currentMax) storage.setLastMessageServerId(room, server, currentLastMessageServerID)
messages return messages
}
} }
// endregion // endregion
@ -304,7 +284,7 @@ object OpenGroupAPIV2 {
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> { fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID") val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
return send(request).map { return send(request).map {
Log.d("Loki", "Deleted server message") Log.d("Loki", "Message deletion successful.")
} }
} }
@ -318,7 +298,7 @@ object OpenGroupAPIV2 {
return send(request).map { json -> return send(request).map { json ->
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["ids"]) val idsAsString = JsonUtil.toJson(json["ids"])
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0 val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
if (serverID.id > lastMessageServerId) { if (serverID.id > lastMessageServerId) {
@ -338,7 +318,7 @@ object OpenGroupAPIV2 {
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators") val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
return send(request).map { json -> return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String> @Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
?: throw Error.PARSING_FAILED ?: throw Error.ParsingFailed
val id = "$server.$room" val id = "$server.$room"
handleModerators(id, moderatorsJson) handleModerators(id, moderatorsJson)
moderatorsJson moderatorsJson
@ -350,14 +330,14 @@ object OpenGroupAPIV2 {
val parameters = mapOf( "public_key" to publicKey ) val parameters = mapOf( "public_key" to publicKey )
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters) val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
return send(request).map { return send(request).map {
Log.d("Loki", "Banned user $publicKey from $server.$room") Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
} }
} }
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> { fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey") val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
return send(request).map { return send(request).map {
Log.d("Loki", "Unbanned user $publicKey from $server.$room") Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
} }
} }
@ -369,65 +349,52 @@ object OpenGroupAPIV2 {
// region General // region General
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> { fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) } val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val requests = rooms.mapNotNull { room -> val requests = rooms.mapNotNull { room ->
val authToken = try { val authToken = try {
requestAuths[room]?.get() authTokenRequests[room]?.get()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Failed to get auth token for $room", e) Log.e("Loki", "Failed to get auth token for $room.", e)
null null
} ?: return@mapNotNull null } ?: return@mapNotNull null
CompactPollRequest(
CompactPollRequest(roomId = room, roomID = room,
authToken = authToken, authToken = authToken,
fromDeletionServerId = storage.getLastDeletionServerId(room, server), fromDeletionServerID = storage.getLastDeletionServerId(room, server),
fromMessageServerId = storage.getLastMessageServerId(room, server) fromMessageServerID = storage.getLastMessageServerId(room, server)
) )
} }
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests )) val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
// build a request for all rooms
return send(request = request).map { json -> return send(request = request).map { json ->
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
results.mapNotNull { json ->
results.mapNotNull { roomJson -> if (json !is Map<*,*>) return@mapNotNull null
if (roomJson !is Map<*,*>) return@mapNotNull null val roomID = json["room_id"] as? String ?: return@mapNotNull null
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// check the status was fine // we provided a valid token but it doesn't have a high enough permission level for the route in question.
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
if (statusCode == 401) { if (statusCode == 401) {
// delete auth token and return null // delete auth token and return null
storage.removeAuthToken(roomId, server) storage.removeAuthToken(roomID, server)
} }
// Moderators
// check and store mods val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null handleModerators("$server.$roomID", moderators)
handleModerators("$server.$roomId", moderators) // Deletions
// get deletions
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(roomJson["deletions"]) val idsAsString = JsonUtil.toJson(json["deletions"])
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0 val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY
if (serverID.id > lastDeletionServerId) { if (serverID.id > lastDeletionServerID) {
storage.setLastDeletionServerId(roomId, server, serverID.id) storage.setLastDeletionServerId(roomID, server, serverID.id)
} }
// Messages
// get messages val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed val messages = parseMessages(roomID, server, rawMessages)
roomID to CompactPollResult(
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
var currentMax = lastMessageServerId
val messages = rawMessages.mapNotNull { rawMessage ->
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
currentMax = maxOf(currentMax,this.serverID ?: 0)
}
message
}
storage.setLastMessageServerId(roomId, server, currentMax)
roomId to CompactPollResult(
messages = messages, messages = messages,
deletions = deletedServerIDs.map { it.deletedMessageId }, deletions = deletedServerIDs.map { it.deletedMessageId },
moderators = moderators moderators = moderators
@ -443,7 +410,7 @@ object OpenGroupAPIV2 {
val earlyGroups = groups.map { group -> val earlyGroups = groups.map { group ->
DefaultGroup(group.id, group.name, null) DefaultGroup(group.id, group.name, null)
} }
// see if we have any cached rooms, and if they already have images, don't overwrite with early non-image results // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results
defaultRooms.replayCache.firstOrNull()?.let { replayed -> defaultRooms.replayCache.firstOrNull()?.let { replayed ->
if (replayed.none { it.image?.isNotEmpty() == true}) { if (replayed.none { it.image?.isNotEmpty() == true}) {
defaultRooms.tryEmit(earlyGroups) defaultRooms.tryEmit(earlyGroups)
@ -452,12 +419,11 @@ object OpenGroupAPIV2 {
val images = groups.map { group -> val images = groups.map { group ->
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER) group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
}.toMap() }.toMap()
groups.map { group -> groups.map { group ->
val image = try { val image = try {
images[group.id]!!.get() images[group.id]!!.get()
} catch (e: Exception) { } catch (e: Exception) {
// no image or image failed to download // No image or image failed to download
null null
} }
DefaultGroup(group.id, group.name, image) DefaultGroup(group.id, group.name, image)
@ -470,9 +436,9 @@ object OpenGroupAPIV2 {
fun getInfo(room: String, server: String): Promise<Info, Exception> { fun getInfo(room: String, server: String): Promise<Info, Exception> {
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false) val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
return send(request).map { json -> return send(request).map { json ->
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
val imageID = rawRoom["image_id"] as? String val imageID = rawRoom["image_id"] as? String
Info(id = id, name = name, imageID = imageID) Info(id = id, name = name, imageID = imageID)
} }
@ -481,13 +447,13 @@ object OpenGroupAPIV2 {
fun getAllRooms(server: String): Promise<List<Info>, Exception> { fun getAllRooms(server: String): Promise<List<Info>, Exception> {
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false) val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
return send(request).map { json -> return send(request).map { json ->
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
rawRooms.mapNotNull { rawRooms.mapNotNull {
val roomJson = it as? Map<*, *> ?: return@mapNotNull null val roomJson = it as? Map<*, *> ?: return@mapNotNull null
val id = roomJson["id"] as? String ?: return@mapNotNull null val id = roomJson["id"] as? String ?: return@mapNotNull null
val name = roomJson["name"] as? String ?: return@mapNotNull null val name = roomJson["name"] as? String ?: return@mapNotNull null
val imageId = roomJson["image_id"] as? String val imageID = roomJson["image_id"] as? String
Info(id, name, imageId) Info(id, name, imageID)
} }
} }
} }
@ -495,12 +461,11 @@ object OpenGroupAPIV2 {
fun getMemberCount(room: String, server: String): Promise<Int, Exception> { fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count") val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
return send(request).map { json -> return send(request).map { json ->
val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED val memberCount = json["member_count"] as? Int ?: throw Error.ParsingFailed
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
storage.setUserCount(room, server, memberCount) storage.setUserCount(room, server, memberCount)
memberCount memberCount
} }
} }
// endregion // endregion
} }

View File

@ -12,10 +12,14 @@ data class OpenGroupMessageV2(
val serverID: Long? = null, val serverID: Long? = null,
val sender: String?, val sender: String?,
val sentTimestamp: Long, val sentTimestamp: Long,
// The serialized protobuf in base64 encoding /**
* The serialized protobuf in base64 encoding.
*/
val base64EncodedData: String, val base64EncodedData: String,
// When sending a message, the sender signs the serialized protobuf with their private key so that /**
// a receiving user can verify that the message wasn't tampered with. * When sending a message, the sender signs the serialized protobuf with their private key so that
* a receiving user can verify that the message wasn't tampered with.
*/
val base64EncodedSignature: String? = null val base64EncodedSignature: String? = null
) { ) {
@ -28,7 +32,8 @@ data class OpenGroupMessageV2(
val serverID = json["server_id"] as? Int val serverID = json["server_id"] as? Int
val sender = json["public_key"] as? String val sender = json["public_key"] as? String
val base64EncodedSignature = json["signature"] as? String val base64EncodedSignature = json["signature"] as? String
return OpenGroupMessageV2(serverID = serverID?.toLong(), return OpenGroupMessageV2(
serverID = serverID?.toLong(),
sender = sender, sender = sender,
sentTimestamp = sentTimestamp, sentTimestamp = sentTimestamp,
base64EncodedData = base64EncodedData, base64EncodedData = base64EncodedData,
@ -41,29 +46,26 @@ data class OpenGroupMessageV2(
fun sign(): OpenGroupMessageV2? { fun sign(): OpenGroupMessageV2? {
if (base64EncodedData.isEmpty()) return null if (base64EncodedData.isEmpty()) return null
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
if (sender != publicKey) return null
if (sender != publicKey) return null // only sign our own messages?
val signature = try { val signature = try {
curve.calculateSignature(privateKey, decode(base64EncodedData)) curve.calculateSignature(privateKey, decode(base64EncodedData))
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e) Log.w("Loki", "Couldn't sign open group message.", e)
return null return null
} }
return copy(base64EncodedSignature = Base64.encodeBytes(signature)) return copy(base64EncodedSignature = Base64.encodeBytes(signature))
} }
fun toJSON(): Map<String, Any> { fun toJSON(): Map<String, Any> {
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp) val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp )
serverID?.let { jsonMap["server_id"] = serverID } serverID?.let { json["server_id"] = it }
sender?.let { jsonMap["public_key"] = sender } sender?.let { json["public_key"] = it }
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature } base64EncodedSignature?.let { json["signature"] = it }
return jsonMap return json
} }
fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes -> fun toProto(): SignalServiceProtos.Content {
SignalServiceProtos.Content.parseFrom(bytes) val data = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody)
return SignalServiceProtos.Content.parseFrom(data)
} }
} }

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.open_groups package org.session.libsession.messaging.open_groups
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.logging.Log
import java.util.* import java.util.*
data class OpenGroupV2( data class OpenGroupV2(
@ -21,26 +22,23 @@ data class OpenGroupV2(
companion object { companion object {
fun fromJson(jsonAsString: String): OpenGroupV2? { fun fromJSON(jsonAsString: String): OpenGroupV2? {
return try { return try {
val json = JsonUtil.fromJson(jsonAsString) val json = JsonUtil.fromJson(jsonAsString)
if (!json.has("room")) return null if (!json.has("room")) return null
val room = json.get("room").asText().toLowerCase(Locale.US)
val room = json.get("room").asText().toLowerCase(Locale.getDefault()) val server = json.get("server").asText().toLowerCase(Locale.US)
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
val displayName = json.get("displayName").asText() val displayName = json.get("displayName").asText()
val publicKey = json.get("publicKey").asText() val publicKey = json.get("publicKey").asText()
OpenGroupV2(server, room, displayName, publicKey) OpenGroupV2(server, room, displayName, publicKey)
} catch (e: Exception) { } catch (e: Exception) {
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
null null
} }
} }
} }
fun toJoinUrl(): String = "$server/$room?public_key=$publicKey"
fun toJson(): Map<String,String> = mapOf( fun toJson(): Map<String,String> = mapOf(
"room" to room, "room" to room,
"server" to server, "server" to server,
@ -48,4 +46,5 @@ data class OpenGroupV2(
"publicKey" to publicKey, "publicKey" to publicKey,
) )
val joinURL: String get() = "$server/$room?public_key=$publicKey"
} }

View File

@ -258,11 +258,11 @@ object MessageSender {
} }
val proto = message.toProto()!! val proto = message.toProto()!!
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
val openGroupMessage = OpenGroupMessageV2( val openGroupMessage = OpenGroupMessageV2(
sender = message.sender, sender = message.sender,
sentTimestamp = message.sentTimestamp!!, sentTimestamp = message.sentTimestamp!!,
base64EncodedData = Base64.encodeBytes(proto.toByteArray()), base64EncodedData = Base64.encodeBytes(plaintext),
) )
OpenGroupAPIV2.send(openGroupMessage,room,server).success { OpenGroupAPIV2.send(openGroupMessage,room,server).success {

View File

@ -128,7 +128,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, 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 } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.toJoinUrl() } val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
for (openGroup in message.openGroups) { for (openGroup in message.openGroups) {
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
storage.addOpenGroup(openGroup, 1) storage.addOpenGroup(openGroup, 1)
@ -164,6 +164,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
?: message.sender!!, message.groupPublicKey, openGroupID) ?: message.sender!!, message.groupPublicKey, openGroupID)
if (threadID < 0) {
// thread doesn't exist, should only be reached in a case where we are processing open group messages for no longer existent thread
throw MessageReceiver.Error.NoThread
}
val openGroup = threadID.let { val openGroup = threadID.let {
storage.getOpenGroup(it.toString()) storage.getOpenGroup(it.toString())
} }
@ -242,7 +247,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
} }
val openGroupServerID = message.openGroupServerMessageID val openGroupServerID = message.openGroupServerMessageID
if (openGroupServerID != null) { if (openGroupServerID != null) {
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !(message.isMediaMessage() || attachments.isNotEmpty())) storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !message.isMediaMessage())
} }
// Cancel any typing indicators if needed // Cancel any typing indicators if needed
cancelTypingIndicatorsIfNeeded(message.sender!!) cancelTypingIndicatorsIfNeeded(message.sender!!)

View File

@ -1,4 +1,4 @@
package org.session.libsession.messaging.jobs; package org.session.libsession.messaging.utilities;
import android.os.Parcelable; import android.os.Parcelable;
@ -12,11 +12,7 @@ import org.session.libsession.utilities.ParcelableUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
// Introduce a dedicated Map<String, byte[]> field specifically for parcelable needs.
public class Data { public class Data {
public static final Data EMPTY = new Data.Builder().build();
@JsonProperty private final Map<String, String> strings; @JsonProperty private final Map<String, String> strings;
@JsonProperty private final Map<String, String[]> stringArrays; @JsonProperty private final Map<String, String[]> stringArrays;
@JsonProperty private final Map<String, Integer> integers; @JsonProperty private final Map<String, Integer> integers;
@ -31,7 +27,10 @@ public class Data {
@JsonProperty private final Map<String, boolean[]> booleanArrays; @JsonProperty private final Map<String, boolean[]> booleanArrays;
@JsonProperty private final Map<String, byte[]> byteArrays; @JsonProperty private final Map<String, byte[]> byteArrays;
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings, public static final Data EMPTY = new Data.Builder().build();
public Data(
@JsonProperty("strings") @NonNull Map<String, String> strings,
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays, @JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
@JsonProperty("integers") @NonNull Map<String, Integer> integers, @JsonProperty("integers") @NonNull Map<String, Integer> integers,
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays, @JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
@ -43,8 +42,8 @@ public class Data {
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays, @JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans, @JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays, @JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays) @JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays
{ ) {
this.strings = strings; this.strings = strings;
this.stringArrays = stringArrays; this.stringArrays = stringArrays;
this.integers = integers; this.integers = integers;
@ -75,6 +74,7 @@ public class Data {
} }
public boolean hasStringArray(@NonNull String key) { public boolean hasStringArray(@NonNull String key) {
return stringArrays.containsKey(key); return stringArrays.containsKey(key);
} }
@ -100,6 +100,7 @@ public class Data {
} }
public boolean hasIntegerArray(@NonNull String key) { public boolean hasIntegerArray(@NonNull String key) {
return integerArrays.containsKey(key); return integerArrays.containsKey(key);
} }
@ -110,6 +111,7 @@ public class Data {
} }
public boolean hasLong(@NonNull String key) { public boolean hasLong(@NonNull String key) {
return longs.containsKey(key); return longs.containsKey(key);
} }
@ -125,6 +127,7 @@ public class Data {
} }
public boolean hasLongArray(@NonNull String key) { public boolean hasLongArray(@NonNull String key) {
return longArrays.containsKey(key); return longArrays.containsKey(key);
} }
@ -135,6 +138,7 @@ public class Data {
} }
public boolean hasFloat(@NonNull String key) { public boolean hasFloat(@NonNull String key) {
return floats.containsKey(key); return floats.containsKey(key);
} }
@ -150,6 +154,7 @@ public class Data {
} }
public boolean hasFloatArray(@NonNull String key) { public boolean hasFloatArray(@NonNull String key) {
return floatArrays.containsKey(key); return floatArrays.containsKey(key);
} }
@ -160,6 +165,7 @@ public class Data {
} }
public boolean hasDouble(@NonNull String key) { public boolean hasDouble(@NonNull String key) {
return doubles.containsKey(key); return doubles.containsKey(key);
} }
@ -175,6 +181,7 @@ public class Data {
} }
public boolean hasDoubleArray(@NonNull String key) { public boolean hasDoubleArray(@NonNull String key) {
return floatArrays.containsKey(key); return floatArrays.containsKey(key);
} }
@ -185,6 +192,7 @@ public class Data {
} }
public boolean hasBoolean(@NonNull String key) { public boolean hasBoolean(@NonNull String key) {
return booleans.containsKey(key); return booleans.containsKey(key);
} }
@ -200,6 +208,7 @@ public class Data {
} }
public boolean hasBooleanArray(@NonNull String key) { public boolean hasBooleanArray(@NonNull String key) {
return booleanArrays.containsKey(key); return booleanArrays.containsKey(key);
} }
@ -209,6 +218,8 @@ public class Data {
return booleanArrays.get(key); return booleanArrays.get(key);
} }
public boolean hasByteArray(@NonNull String key) { public boolean hasByteArray(@NonNull String key) {
return byteArrays.containsKey(key); return byteArrays.containsKey(key);
} }
@ -218,6 +229,8 @@ public class Data {
return byteArrays.get(key); return byteArrays.get(key);
} }
public boolean hasParcelable(@NonNull String key) { public boolean hasParcelable(@NonNull String key) {
return byteArrays.containsKey(key); return byteArrays.containsKey(key);
} }
@ -228,6 +241,8 @@ public class Data {
return ParcelableUtil.unmarshall(bytes, creator); return ParcelableUtil.unmarshall(bytes, creator);
} }
private void throwIfAbsent(@NonNull Map map, @NonNull String key) { private void throwIfAbsent(@NonNull Map map, @NonNull String key) {
if (!map.containsKey(key)) { if (!map.containsKey(key)) {
throw new IllegalStateException("Tried to retrieve a value with key '" + key + "', but it wasn't present."); throw new IllegalStateException("Tried to retrieve a value with key '" + key + "', but it wasn't present.");
@ -236,7 +251,6 @@ public class Data {
public static class Builder { public static class Builder {
private final Map<String, String> strings = new HashMap<>(); private final Map<String, String> strings = new HashMap<>();
private final Map<String, String[]> stringArrays = new HashMap<>(); private final Map<String, String[]> stringArrays = new HashMap<>();
private final Map<String, Integer> integers = new HashMap<>(); private final Map<String, Integer> integers = new HashMap<>();
@ -323,7 +337,8 @@ public class Data {
} }
public Data build() { public Data build() {
return new Data(strings, return new Data(
strings,
stringArrays, stringArrays,
integers, integers,
integerArrays, integerArrays,
@ -335,7 +350,8 @@ public class Data {
doubleArrays, doubleArrays,
booleans, booleans,
booleanArrays, booleanArrays,
byteArrays); byteArrays
);
} }
} }
@ -344,4 +360,3 @@ public class Data {
@NonNull Data deserialize(@NonNull String serialized); @NonNull Data deserialize(@NonNull String serialized);
} }
} }

View File

@ -53,11 +53,11 @@ object OnionRequestAPI {
/** /**
* The number of times a path can fail before it's replaced. * The number of times a path can fail before it's replaced.
*/ */
private const val pathFailureThreshold = 1 private const val pathFailureThreshold = 3
/** /**
* The number of times a snode can fail before it's replaced. * The number of times a snode can fail before it's replaced.
*/ */
private const val snodeFailureThreshold = 1 private const val snodeFailureThreshold = 3
/** /**
* The number of guard snodes required to maintain `targetPathCount` paths. * The number of guard snodes required to maintain `targetPathCount` paths.
*/ */
@ -93,7 +93,7 @@ object OnionRequestAPI {
ThreadUtils.queue { // No need to block the shared context for this ThreadUtils.queue { // No need to block the shared context for this
val url = "${snode.address}:${snode.port}/get_stats/v1" val url = "${snode.address}:${snode.port}/get_stats/v1"
try { try {
val json = HTTP.execute(HTTP.Verb.GET, url) val json = HTTP.execute(HTTP.Verb.GET, url, 3)
val version = json["version"] as? String val version = json["version"] as? String
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue } if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
if (version >= "2.0.7") { if (version >= "2.0.7") {
@ -463,7 +463,6 @@ object OnionRequestAPI {
"method" to request.method(), "method" to request.method(),
"headers" to headers "headers" to headers
) )
url.isHttps
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port()) val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port())
return sendOnionRequest(destination, payload, isJSONRequired).recover { exception -> return sendOnionRequest(destination, payload, isJSONRequired).recover { exception ->
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.") Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")

View File

@ -34,7 +34,7 @@ object SnodeAPI {
// Settings // Settings
private val maxRetryCount = 6 private val maxRetryCount = 6
private val minimumSnodePoolCount = 12 private val minimumSnodePoolCount = 12
private val minimumSwarmSnodeCount = 2 private val minimumSwarmSnodeCount = 3
// Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates // Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates
private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433 private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
private val seedNodePool by lazy { private val seedNodePool by lazy {
@ -44,7 +44,7 @@ object SnodeAPI {
setOf( "https://storage.seed1.loki.network:$seedNodePort ", "https://storage.seed3.loki.network:$seedNodePort ", "https://public.loki.foundation:$seedNodePort" ) setOf( "https://storage.seed1.loki.network:$seedNodePort ", "https://storage.seed3.loki.network:$seedNodePort ", "https://public.loki.foundation:$seedNodePort" )
} }
} }
private val snodeFailureThreshold = 4 private val snodeFailureThreshold = 3
private val targetSwarmSnodeCount = 2 private val targetSwarmSnodeCount = 2
private val useOnionRequests = true private val useOnionRequests = true
@ -252,19 +252,20 @@ object SnodeAPI {
private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> { private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> {
val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf() val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf()
return rawMessages.filter { rawMessage -> val result = rawMessages.filter { rawMessage ->
val rawMessageAsJSON = rawMessage as? Map<*, *> val rawMessageAsJSON = rawMessage as? Map<*, *>
val hashValue = rawMessageAsJSON?.get("hash") as? String val hashValue = rawMessageAsJSON?.get("hash") as? String
if (hashValue != null) { if (hashValue != null) {
val isDuplicate = receivedMessageHashValues.contains(hashValue) val isDuplicate = receivedMessageHashValues.contains(hashValue)
receivedMessageHashValues.add(hashValue) receivedMessageHashValues.add(hashValue)
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
!isDuplicate !isDuplicate
} else { } else {
Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.")
false false
} }
} }
database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues)
return result
} }
private fun parseEnvelopes(rawMessages: List<*>): List<SignalServiceProtos.Envelope> { private fun parseEnvelopes(rawMessages: List<*>): List<SignalServiceProtos.Envelope> {
@ -305,7 +306,7 @@ object SnodeAPI {
} }
} }
when (statusCode) { when (statusCode) {
400, 500, 503 -> { // Usually indicates that the snode isn't up to date 400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date
handleBadSnode() handleBadSnode()
} }
406 -> { 406 -> {
@ -316,8 +317,20 @@ object SnodeAPI {
421 -> { 421 -> {
// The snode isn't associated with the given public key anymore // The snode isn't associated with the given public key anymore
if (publicKey != null) { if (publicKey != null) {
fun invalidateSwarm() {
Log.d("Loki", "Invalidating swarm for: $publicKey.") Log.d("Loki", "Invalidating swarm for: $publicKey.")
dropSnodeFromSwarmIfNeeded(snode, publicKey) dropSnodeFromSwarmIfNeeded(snode, publicKey)
}
if (json != null) {
val snodes = parseSnodes(json)
if (snodes.isNotEmpty()) {
database.setSwarm(publicKey, snodes.toSet())
} else {
invalidateSwarm()
}
} else {
invalidateSwarm()
}
} else { } else {
Log.d("Loki", "Got a 421 without an associated public key.") Log.d("Loki", "Got a 421 without an associated public key.")
} }

View File

@ -3,13 +3,23 @@ package org.session.libsession.snode
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
data class SnodeMessage( data class SnodeMessage(
// The hex encoded public key of the recipient. /**
* The hex encoded public key of the recipient.
*/
val recipient: String, val recipient: String,
// The content of the message. /**
* The content of the message.
*/
val data: String, val data: String,
// The time to live for the message in milliseconds. /**
* The time to live for the message in milliseconds.
*/
val ttl: Long, val ttl: Long,
// When the proof of work was calculated. /**
* When the proof of work was calculated.
*
* **Note:** Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
*/
val timestamp: Long val timestamp: Long
) { ) {

View File

@ -6,6 +6,7 @@ import java.security.SecureRandom
* Uses `SecureRandom` to pick an element from this collection. * Uses `SecureRandom` to pick an element from this collection.
*/ */
fun <T> Collection<T>.getRandomElementOrNull(): T? { fun <T> Collection<T>.getRandomElementOrNull(): T? {
if (isEmpty()) return null
val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure
return elementAtOrNull(index) return elementAtOrNull(index)
} }

View File

@ -3,6 +3,7 @@ package org.session.libsignal.service.loki
import okhttp3.* import okhttp3.*
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import java.lang.IllegalStateException
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -25,9 +26,7 @@ object HTTP {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { } override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { } override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> { override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
return arrayOf()
}
} }
val sslContext = SSLContext.getInstance("SSL") val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SecureRandom()) sslContext.init(null, arrayOf( trustManager ), SecureRandom())
@ -40,7 +39,26 @@ object HTTP {
.build() .build()
} }
private const val timeout: Long = 20 private fun getDefaultConnection(timeout: Long): OkHttpClient {
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
val trustManager = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authorizationType: String?) { }
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SecureRandom())
return OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier { _, _ -> true }
.connectTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.build()
}
private const val timeout: Long = 10
class HTTPRequestFailedException(val statusCode: Int, val json: Map<*, *>?) class HTTPRequestFailedException(val statusCode: Int, val json: Map<*, *>?)
: kotlin.Exception("HTTP request failed with status code $statusCode.") : kotlin.Exception("HTTP request failed with status code $statusCode.")
@ -52,26 +70,26 @@ object HTTP {
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */
fun execute(verb: Verb, url: String, useSeedNodeConnection: Boolean = false): Map<*, *> { fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
return execute(verb = verb, url = url, body = null, useSeedNodeConnection = useSeedNodeConnection) return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
} }
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, useSeedNodeConnection: Boolean = false): Map<*, *> { fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
if (parameters != null) { if (parameters != null) {
val body = JsonUtil.toJson(parameters).toByteArray() val body = JsonUtil.toJson(parameters).toByteArray()
return execute(verb = verb, url = url, body = body, useSeedNodeConnection = useSeedNodeConnection) return execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
} else { } else {
return execute(verb = verb, url = url, body = null, useSeedNodeConnection = useSeedNodeConnection) return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
} }
} }
/** /**
* Sync. Don't call from the main thread. * Sync. Don't call from the main thread.
*/ */
fun execute(verb: Verb, url: String, body: ByteArray?, useSeedNodeConnection: Boolean = false): Map<*, *> { fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
val request = Request.Builder().url(url) val request = Request.Builder().url(url)
when (verb) { when (verb) {
Verb.GET -> request.get() Verb.GET -> request.get()
@ -85,7 +103,15 @@ object HTTP {
} }
lateinit var response: Response lateinit var response: Response
try { try {
val connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection val connection: OkHttpClient
if (timeout != HTTP.timeout) { // Custom timeout
if (useSeedNodeConnection) {
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
}
connection = getDefaultConnection(timeout)
} else {
connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection
}
response = connection.newCall(request.build()).execute() response = connection.newCall(request.build()).execute()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.") Log.d("Loki", "${verb.rawValue} request to $url failed due to error: ${exception.localizedMessage}.")

View File

@ -6,6 +6,7 @@ import java.security.SecureRandom
* Uses `SecureRandom` to pick an element from this collection. * Uses `SecureRandom` to pick an element from this collection.
*/ */
fun <T> Collection<T>.getRandomElementOrNull(): T? { fun <T> Collection<T>.getRandomElementOrNull(): T? {
if (isEmpty()) return null
val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure
return elementAtOrNull(index) return elementAtOrNull(index)
} }