mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 20:15:21 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into error-handling-group-creation
This commit is contained in:
commit
91c3ec6c7d
15
BUILDING.md
15
BUILDING.md
@ -21,15 +21,6 @@ Ensure that the following packages are installed from the Android SDK manager:
|
|||||||
|
|
||||||
In Android studio, this can be done from the Quickstart panel, choose "Configure" then "SDK Manager". In the SDK Tools tab of the SDK Manager, make sure that the "Android Support Repository" is installed, and that the latest "Android SDK build-tools" are installed. Click "OK" to return to the Quickstart panel. You may also need to install API version 28 in the SDK platforms tab.
|
In Android studio, this can be done from the Quickstart panel, choose "Configure" then "SDK Manager". In the SDK Tools tab of the SDK Manager, make sure that the "Android Support Repository" is installed, and that the latest "Android SDK build-tools" are installed. Click "OK" to return to the Quickstart panel. You may also need to install API version 28 in the SDK platforms tab.
|
||||||
|
|
||||||
You will then need to clone and run `./gradlew install` on each of the following repositories IN ORDER:
|
|
||||||
|
|
||||||
* https://github.com/loki-project/loki-messenger-android-curve-25519
|
|
||||||
* https://github.com/loki-project/loki-messenger-android-protocol
|
|
||||||
* https://github.com/loki-project/loki-messenger-android-meta
|
|
||||||
* https://github.com/loki-project/session-android-service
|
|
||||||
|
|
||||||
This installs these dependencies into a local Maven repository which the main Session Android repository will then draw from.
|
|
||||||
|
|
||||||
Setting up a development environment and building from Android Studio
|
Setting up a development environment and building from Android Studio
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
@ -37,7 +28,7 @@ Setting up a development environment and building from Android Studio
|
|||||||
|
|
||||||
1. Open Android Studio. On a new installation, the Quickstart panel will appear. If you have open projects, close them using "File > Close Project" to see the Quickstart panel.
|
1. Open Android Studio. On a new installation, the Quickstart panel will appear. If you have open projects, close them using "File > Close Project" to see the Quickstart panel.
|
||||||
2. From the Quickstart panel, choose "Checkout from Version Control" then "git".
|
2. From the Quickstart panel, choose "Checkout from Version Control" then "git".
|
||||||
3. Paste the URL for the session-android project when prompted (https://github.com/loki-project/session-android.git).
|
3. Paste the URL for the session-android project when prompted (https://github.com/oxen-io/session-android.git).
|
||||||
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
|
4. Android Studio should detect the presence of a project file and ask you whether to open it. Click "yes".
|
||||||
5. Default config options should be good enough.
|
5. Default config options should be good enough.
|
||||||
6. Project initialization and building should proceed.
|
6. Project initialization and building should proceed.
|
||||||
@ -49,7 +40,7 @@ The following steps should help you (re)build Session from the command line once
|
|||||||
|
|
||||||
1. Checkout the session-android project source with the command:
|
1. Checkout the session-android project source with the command:
|
||||||
|
|
||||||
git clone https://github.com/loki-project/session-android.git
|
git clone https://github.com/oxen-io/session-android.git
|
||||||
|
|
||||||
2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed.
|
2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed.
|
||||||
3. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. For example:
|
3. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. For example:
|
||||||
@ -58,7 +49,7 @@ The following steps should help you (re)build Session from the command line once
|
|||||||
|
|
||||||
4. Execute Gradle:
|
4. Execute Gradle:
|
||||||
|
|
||||||
./gradlew build
|
./gradlew :app:build
|
||||||
|
|
||||||
Contributing code
|
Contributing code
|
||||||
-----------------
|
-----------------
|
||||||
|
@ -2,17 +2,19 @@
|
|||||||
|
|
||||||
[Download on the Google Play Store](https://getsession.org/android)
|
[Download on the Google Play Store](https://getsession.org/android)
|
||||||
|
|
||||||
|
Add the [F-Droid repo](https://fdroid.getsession.org/)
|
||||||
|
|
||||||
[Grab the APK here](https://github.com/loki-project/session-android/releases/latest)
|
[Grab the APK here](https://github.com/loki-project/session-android/releases/latest)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
|
Session integrates directly with [Oxen Service Nodes](https://docs.oxen.io/about-the-oxen-blockchain/oxen-service-nodes), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
|
||||||
|
|
||||||
![AndroidSession](https://i.imgur.com/0YC9TyI.png)
|
![AndroidSession](https://i.imgur.com/0YC9TyI.png)
|
||||||
|
|
||||||
## Want to contribute? Found a bug or have a feature request?
|
## Want to contribute? Found a bug or have a feature request?
|
||||||
|
|
||||||
Please search for any [existing issues](https://github.com/loki-project/session-android/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing, try reading the Github issues page for ideas.
|
Please search for any [existing issues](https://github.com/oxen-io/session-android/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our `dev` branch. If you don't know where to start contributing, try reading the Github issues page for ideas.
|
||||||
|
|
||||||
## Build instructions
|
## Build instructions
|
||||||
|
|
||||||
|
@ -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 = 154
|
def canonicalVersionCode = 162
|
||||||
def canonicalVersionName = "1.10.0"
|
def canonicalVersionName = "1.10.3"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -103,7 +103,6 @@ import dagger.ObjectGraph;
|
|||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlinx.coroutines.Job;
|
import kotlinx.coroutines.Job;
|
||||||
import network.loki.messenger.BuildConfig;
|
import network.loki.messenger.BuildConfig;
|
||||||
import nl.komponents.kovenant.Kovenant;
|
|
||||||
|
|
||||||
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
||||||
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||||
@ -328,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() {
|
||||||
@ -456,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();
|
||||||
}
|
}
|
||||||
|
@ -185,15 +185,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
|
DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markJobAsSucceeded(job: Job) {
|
override fun markJobAsSucceeded(jobId: String) {
|
||||||
DatabaseFactory.getSessionJobDatabase(context).markJobAsSucceeded(job)
|
DatabaseFactory.getSessionJobDatabase(context).markJobAsSucceeded(jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markJobAsFailed(job: Job) {
|
override fun markJobAsFailedPermanently(jobId: String) {
|
||||||
DatabaseFactory.getSessionJobDatabase(context).markJobAsFailed(job)
|
DatabaseFactory.getSessionJobDatabase(context).markJobAsFailedPermanently(jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllPendingJobs(type: String): List<Job> {
|
override fun getAllPendingJobs(type: String): Map<String, Job?> {
|
||||||
return DatabaseFactory.getSessionJobDatabase(context).getAllPendingJobs(type)
|
return DatabaseFactory.getSessionJobDatabase(context).getAllPendingJobs(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +257,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -581,7 +581,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)
|
||||||
|
@ -55,9 +55,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int lokiV21 = 42;
|
private static final int lokiV21 = 42;
|
||||||
private static final int lokiV22 = 43;
|
private static final int lokiV22 = 43;
|
||||||
private static final int lokiV23 = 44;
|
private static final int lokiV23 = 44;
|
||||||
|
private static final int lokiV24 = 45;
|
||||||
|
|
||||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||||
private static final int DATABASE_VERSION = lokiV23;
|
private static final int DATABASE_VERSION = lokiV24;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -281,6 +282,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
|
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV24) {
|
||||||
|
String swarmTable = LokiAPIDatabase.Companion.getSwarmTable();
|
||||||
|
String snodePoolTable = LokiAPIDatabase.Companion.getSnodePoolTable();
|
||||||
|
db.execSQL("DROP TABLE " + swarmTable);
|
||||||
|
db.execSQL("DROP TABLE " + snodePoolTable);
|
||||||
|
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
|
||||||
|
db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand());
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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,45 +23,23 @@ 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(
|
||||||
)
|
TAG,
|
||||||
.build()
|
ExistingPeriodicWorkPolicy.REPLACE,
|
||||||
|
workRequest
|
||||||
WorkManager
|
)
|
||||||
.getInstance(context)
|
|
||||||
.enqueueUniquePeriodicWork(
|
|
||||||
TAG,
|
|
||||||
ExistingPeriodicWorkPolicy.KEEP,
|
|
||||||
workRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
private val timestamp = "timestamp"
|
private val timestamp = "timestamp"
|
||||||
private val snode = "snode"
|
private val snode = "snode"
|
||||||
// Snode pool
|
// Snode pool
|
||||||
private val snodePoolTable = "loki_snode_pool_cache"
|
public val snodePoolTable = "loki_snode_pool_cache"
|
||||||
private val dummyKey = "dummy_key"
|
private val dummyKey = "dummy_key"
|
||||||
private val snodePool = "snode_pool_key"
|
private val snodePool = "snode_pool_key"
|
||||||
@JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);"
|
@JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);"
|
||||||
@ -36,7 +36,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
private val indexPath = "index_path"
|
private val indexPath = "index_path"
|
||||||
@JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
|
@JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
|
||||||
// Swarms
|
// Swarms
|
||||||
private val swarmTable = "loki_api_swarm_cache"
|
public val swarmTable = "loki_api_swarm_cache"
|
||||||
private val swarmPublicKey = "hex_encoded_public_key"
|
private val swarmPublicKey = "hex_encoded_public_key"
|
||||||
private val swarm = "swarm"
|
private val swarm = "swarm"
|
||||||
@JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
|
@JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ 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.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer
|
||||||
@ -17,47 +19,55 @@ 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(job: Job) {
|
fun markJobAsSucceeded(jobID: String) {
|
||||||
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(job.id))
|
databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markJobAsFailed(job: Job) {
|
fun markJobAsFailedPermanently(jobID: String) {
|
||||||
databaseHelper.writableDatabase.delete(sessionJobTable, "$jobID = ?", arrayOf(job.id))
|
databaseHelper.writableDatabase.delete(sessionJobTable, "${Companion.jobID} = ?", arrayOf( jobID ))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllPendingJobs(type: String): List<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 ->
|
||||||
jobFromCursor(cursor)
|
val jobID = cursor.getString(jobID)
|
||||||
}
|
try {
|
||||||
|
jobID to jobFromCursor(cursor)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Error deserializing job of type: $type.", e)
|
||||||
|
jobID to null
|
||||||
|
}
|
||||||
|
}.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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,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 {
|
||||||
@ -75,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
|
||||||
|
@ -17,7 +17,7 @@ object MultiDeviceProtocol {
|
|||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
|
||||||
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
|
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
|
if (now - lastSyncTime < 7 * 24 * 60 * 60 * 1000) return
|
||||||
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
|
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
|
||||||
!recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
|
!recipient.isBlocked && !recipient.name.isNullOrEmpty() && !recipient.isLocalNumber && recipient.address.serialize().isNotEmpty()
|
||||||
}.map { recipient ->
|
}.map { recipient ->
|
||||||
|
@ -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;
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -44,9 +44,9 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Jobs
|
// Jobs
|
||||||
fun persistJob(job: Job)
|
fun persistJob(job: Job)
|
||||||
fun markJobAsSucceeded(job: Job)
|
fun markJobAsSucceeded(jobId: String)
|
||||||
fun markJobAsFailed(job: Job)
|
fun markJobAsFailedPermanently(jobId: String)
|
||||||
fun getAllPendingJobs(type: String): List<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?
|
||||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||||
|
@ -1,106 +1,91 @@
|
|||||||
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(
|
||||||
val verb: HTTP.Verb,
|
val verb: HTTP.Verb,
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
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
|
||||||
val useOnionRouting: Boolean = true
|
* this when running over Lokinet.
|
||||||
|
*/
|
||||||
|
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))
|
||||||
when (request.verb) {
|
when (request.verb) {
|
||||||
HTTP.Verb.GET -> requestBuilder.get()
|
HTTP.Verb.GET -> requestBuilder.get()
|
||||||
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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() {
|
||||||
@ -52,18 +52,19 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
try {
|
try {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||||
?: 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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(messageSendJob)
|
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)!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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?
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,42 +92,75 @@ class JobQueue : JobDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasResumedPendingJobs = true
|
hasResumedPendingJobs = true
|
||||||
val allJobTypes = listOf(AttachmentDownloadJob.KEY, AttachmentDownloadJob.KEY, MessageReceiveJob.KEY, MessageSendJob.KEY, NotifyPNServerJob.KEY)
|
val allJobTypes = listOf(
|
||||||
|
AttachmentUploadJob.KEY,
|
||||||
|
AttachmentDownloadJob.KEY,
|
||||||
|
MessageReceiveJob.KEY,
|
||||||
|
MessageSendJob.KEY,
|
||||||
|
NotifyPNServerJob.KEY
|
||||||
|
)
|
||||||
allJobTypes.forEach { type ->
|
allJobTypes.forEach { type ->
|
||||||
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
|
val allPendingJobs = MessagingModuleConfiguration.shared.storage.getAllPendingJobs(type)
|
||||||
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
val pendingJobs = mutableListOf<Job>()
|
||||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
for ((id, job) in allPendingJobs) {
|
||||||
|
if (job == null) {
|
||||||
|
// Job failed to deserialize, remove it from the DB
|
||||||
|
handleJobFailedPermanently(id)
|
||||||
|
} else {
|
||||||
|
pendingJobs.add(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingJobs.sortedBy { it.id }.forEach { job ->
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobSucceeded(job: Job) {
|
override fun handleJobSucceeded(job: Job) {
|
||||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(job)
|
val jobId = job.id ?: return
|
||||||
|
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
storage.persistJob(job)
|
return Log.i("Loki", "${job::class.simpleName} canceled.")
|
||||||
if (job.failureCount == job.maxFailureCount) {
|
}
|
||||||
storage.markJobAsFailed(job)
|
// Message send jobs waiting for the attachment to upload
|
||||||
} else {
|
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
||||||
val retryInterval = getRetryInterval(job)
|
val retryInterval: Long = 1000 * 4
|
||||||
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
Log.i("Loki", "Message send job waiting for attachment upload to finish.")
|
||||||
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)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Regular job failure
|
||||||
|
job.failureCount += 1
|
||||||
|
if (job.failureCount >= job.maxFailureCount) {
|
||||||
|
handleJobFailedPermanently(job, error)
|
||||||
|
} else {
|
||||||
|
storage.persistJob(job)
|
||||||
|
val retryInterval = getRetryInterval(job)
|
||||||
|
Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||||
|
timer.schedule(delay = retryInterval) {
|
||||||
|
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
||||||
queue.offer(job)
|
queue.offer(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
||||||
job.failureCount += 1
|
val jobId = job.id ?: return
|
||||||
|
handleJobFailedPermanently(jobId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleJobFailedPermanently(jobId: String) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
storage.persistJob(job)
|
storage.markJobAsFailedPermanently(jobId)
|
||||||
storage.markJobAsFailed(job)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRetryInterval(job: Job): Long {
|
private fun getRetryInterval(job: Job): Long {
|
||||||
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,33 +4,38 @@ 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)
|
||||||
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
|
||||||
@ -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,35 +88,55 @@ 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), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
|
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 {
|
||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,18 +67,19 @@ 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 {
|
||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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(),
|
||||||
|
@ -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() {
|
||||||
@ -21,11 +18,12 @@ sealed class Destination {
|
|||||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||||
internal constructor(): this(0, "")
|
internal constructor(): this(0, "")
|
||||||
}
|
}
|
||||||
class OpenGroupV2(var room: String, var server: String): Destination() {
|
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
||||||
internal constructor(): this("", "")
|
internal constructor(): this("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
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 -> {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,31 +27,46 @@ 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())
|
||||||
}
|
}
|
||||||
class NameChange(var name: String) : Kind() {
|
class NameChange(var name: String) : Kind() {
|
||||||
internal constructor(): this("")
|
internal constructor() : this("")
|
||||||
}
|
}
|
||||||
class MembersAdded(var members: List<ByteString>) : Kind() {
|
class MembersAdded(var members: List<ByteString>) : Kind() {
|
||||||
internal constructor(): this(listOf())
|
internal constructor() : this(listOf())
|
||||||
}
|
}
|
||||||
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
class MembersRemoved(var members: List<ByteString>) : Kind() {
|
||||||
internal constructor(): this(listOf())
|
internal constructor() : this(listOf())
|
||||||
}
|
}
|
||||||
class MemberLeft() : Kind()
|
class MemberLeft() : Kind()
|
||||||
|
|
||||||
val description: String =
|
val description: String =
|
||||||
when(this) {
|
when (this) {
|
||||||
is New -> "new"
|
is New -> "new"
|
||||||
is EncryptionKeyPair -> "encryptionKeyPair"
|
is EncryptionKeyPair -> "encryptionKeyPair"
|
||||||
is NameChange -> "nameChange"
|
is NameChange -> "nameChange"
|
||||||
@ -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) {
|
||||||
|
@ -14,12 +14,15 @@ 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()
|
||||||
|
|
||||||
internal constructor(): this("", "", null, listOf(), listOf())
|
internal constructor() : this("", "", null, listOf(), listOf())
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return name
|
return name
|
||||||
@ -56,7 +59,7 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
|
|
||||||
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
|
||||||
|
|
||||||
internal constructor(): this("", "", null, null)
|
internal constructor() : this("", "", null, null)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@ -66,8 +69,7 @@ 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()
|
||||||
|
@ -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()
|
||||||
}
|
|
@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.control
|
|||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class DataExtractionNotification(): ControlMessage() {
|
class DataExtractionNotification() : ControlMessage() {
|
||||||
var kind: Kind? = null
|
var kind: Kind? = null
|
||||||
|
|
||||||
sealed class Kind {
|
sealed class Kind {
|
||||||
@ -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
|
||||||
|
@ -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? {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,11 @@ 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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()
|
||||||
|
@ -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
|
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
||||||
} else {
|
return Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
|
||||||
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
|
|
||||||
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) {
|
||||||
|
@ -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()
|
||||||
@ -21,46 +25,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 }
|
|
||||||
}
|
|
||||||
// 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
|
||||||
@ -68,56 +33,84 @@ 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
|
||||||
|
}
|
||||||
|
// 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//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!!)) {
|
||||||
try {
|
try {
|
||||||
setGroupContext(dataMessage)
|
setGroupContext(dataMessage)
|
||||||
} catch(e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
Log.w(TAG, "Couldn't construct visible message proto from: $this")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -135,4 +128,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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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,108 +27,83 @@ 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() {
|
|
||||||
object GENERIC : Error()
|
|
||||||
object PARSING_FAILED : Error()
|
|
||||||
object DECRYPTION_FAILED : Error()
|
|
||||||
object SIGNING_FAILED : Error()
|
|
||||||
object INVALID_URL : Error()
|
|
||||||
object NO_PUBLIC_KEY : Error()
|
|
||||||
|
|
||||||
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."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
sealed class Error(message: String) : Exception(message) {
|
||||||
|
object Generic : Error("An error occurred.")
|
||||||
|
object ParsingFailed : Error("Invalid response.")
|
||||||
|
object DecryptionFailed : Error("Couldn't decrypt response.")
|
||||||
|
object SigningFailed : Error("Couldn't sign message.")
|
||||||
|
object InvalidURL : Error("Invalid URL.")
|
||||||
|
object NoPublicKey : Error("Couldn't find server public key.")
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DefaultGroup(val id: String,
|
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||||
val name: String,
|
|
||||||
val image: ByteArray?) {
|
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||||
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Info(
|
data class Info(val id: String, val name: String, val imageID: String?)
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Request(
|
data class Request(
|
||||||
val verb: HTTP.Verb,
|
val verb: HTTP.Verb,
|
||||||
val room: String?,
|
val room: String?,
|
||||||
val server: String,
|
val server: String,
|
||||||
val endpoint: String,
|
val endpoint: String,
|
||||||
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(),
|
||||||
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
|
||||||
val useOnionRouting: Boolean = true
|
* this when running over Lokinet.
|
||||||
|
*/
|
||||||
|
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,25 +112,25 @@ 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
|
||||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
|
||||||
if (request.room != null) {
|
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||||
storage.removeAuthToken("${request.server}.${request.room}")
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
} else {
|
if (request.room != null) {
|
||||||
storage.removeAuthToken(request.server)
|
storage.removeAuthToken("${request.server}.${request.room}")
|
||||||
}
|
} else {
|
||||||
}
|
storage.removeAuthToken(request.server)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} 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."))
|
||||||
}
|
}
|
||||||
@ -172,52 +145,51 @@ 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 {
|
||||||
Promise.of(it)
|
Promise.of(it)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
requestNewAuthToken(room, server)
|
requestNewAuthToken(room, server)
|
||||||
.bind { claimAuthToken(it, room, server) }
|
.bind { claimAuthToken(it, room, server) }
|
||||||
.success { authToken ->
|
.success { authToken ->
|
||||||
storage.setAuthToken(room, server, authToken)
|
storage.setAuthToken(room, server, authToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||||
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
val parameters = mapOf( "public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!! )
|
||||||
val headers = mapOf("Authorization" to authToken)
|
val headers = mapOf( "Authorization" to authToken )
|
||||||
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
||||||
parameters = parameters, headers = headers, isAuthRequired = false)
|
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||||
return send(request).map { authToken }
|
return send(request).map { authToken }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,37 +241,42 @@ 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
|
|
||||||
val messages = rawMessages.mapNotNull { json ->
|
|
||||||
try {
|
|
||||||
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
|
||||||
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
|
||||||
val sender = message.sender
|
|
||||||
val data = decode(message.base64EncodedData)
|
|
||||||
val signature = decode(message.base64EncodedSignature)
|
|
||||||
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
|
||||||
val isValid = curve.verifySignature(publicKey, data, signature)
|
|
||||||
if (!isValid) {
|
|
||||||
Log.d("Loki", "Ignoring message with invalid signature")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
if (message.serverID > lastMessageServerId) {
|
|
||||||
currentMax = message.serverID
|
|
||||||
}
|
|
||||||
message
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storage.setLastMessageServerId(room, server, currentMax)
|
|
||||||
messages
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
json as Map<String, Any>
|
||||||
|
try {
|
||||||
|
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||||
|
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||||
|
val sender = message.sender
|
||||||
|
val data = decode(message.base64EncodedData)
|
||||||
|
val signature = decode(message.base64EncodedSignature)
|
||||||
|
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
||||||
|
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||||
|
if (!isValid) {
|
||||||
|
Log.d("Loki", "Ignoring message with invalid signature.")
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
if (message.serverID > lastMessageServerID) {
|
||||||
|
currentLastMessageServerID = message.serverID
|
||||||
|
}
|
||||||
|
message
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
||||||
|
return messages
|
||||||
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Message Deletion
|
// region Message Deletion
|
||||||
@ -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
|
||||||
@ -347,90 +327,77 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||||
moderators["$server.$room"]?.contains(publicKey) ?: false
|
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// 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
|
messages = messages,
|
||||||
var currentMax = lastMessageServerId
|
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||||
val messages = rawMessages.mapNotNull { rawMessage ->
|
moderators = moderators
|
||||||
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
|
||||||
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
|
||||||
}
|
|
||||||
message
|
|
||||||
}
|
|
||||||
storage.setLastMessageServerId(roomId, server, currentMax)
|
|
||||||
roomId to CompactPollResult(
|
|
||||||
messages = messages,
|
|
||||||
deletions = deletedServerIDs.map { it.deletedMessageId },
|
|
||||||
moderators = moderators
|
|
||||||
)
|
)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|
||||||
}
|
}
|
@ -9,14 +9,18 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
import org.whispersystems.curve25519.Curve25519
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
data class OpenGroupMessageV2(
|
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
|
/**
|
||||||
val base64EncodedData: String,
|
* The serialized protobuf in base64 encoding.
|
||||||
// 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 base64EncodedData: String,
|
||||||
val base64EncodedSignature: String? = null
|
/**
|
||||||
|
* 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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -28,11 +32,12 @@ 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(
|
||||||
sender = sender,
|
serverID = serverID?.toLong(),
|
||||||
sentTimestamp = sentTimestamp,
|
sender = sender,
|
||||||
base64EncodedData = base64EncodedData,
|
sentTimestamp = sentTimestamp,
|
||||||
base64EncodedSignature = base64EncodedSignature
|
base64EncodedData = base64EncodedData,
|
||||||
|
base64EncodedSignature = base64EncodedSignature
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,51 +1,50 @@
|
|||||||
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(
|
||||||
val server: String,
|
val server: String,
|
||||||
val room: String,
|
val room: String,
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val publicKey: String
|
val publicKey: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||||
server = server,
|
server = server,
|
||||||
room = room,
|
room = room,
|
||||||
id = "$server.$room",
|
id = "$server.$room",
|
||||||
name = name,
|
name = name,
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
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,
|
||||||
"displayName" to name,
|
"displayName" to name,
|
||||||
"publicKey" to publicKey,
|
"publicKey" to publicKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val joinURL: String get() = "$server/$room?public_key=$publicKey"
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving
|
package org.session.libsession.messaging.sending_receiving
|
||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
@ -126,7 +125,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)
|
||||||
@ -154,7 +153,12 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
|
|
||||||
// Get or create thread
|
// Get or create thread
|
||||||
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())
|
||||||
@ -234,7 +238,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!!)
|
||||||
|
@ -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,20 +27,23 @@ 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();
|
||||||
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
|
||||||
@JsonProperty("integers") @NonNull Map<String, Integer> integers,
|
public Data(
|
||||||
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
|
@JsonProperty("strings") @NonNull Map<String, String> strings,
|
||||||
@JsonProperty("longs") @NonNull Map<String, Long> longs,
|
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
||||||
@JsonProperty("longArrays") @NonNull Map<String, long[]> longArrays,
|
@JsonProperty("integers") @NonNull Map<String, Integer> integers,
|
||||||
@JsonProperty("floats") @NonNull Map<String, Float> floats,
|
@JsonProperty("integerArrays") @NonNull Map<String, int[]> integerArrays,
|
||||||
@JsonProperty("floatArrays") @NonNull Map<String, float[]> floatArrays,
|
@JsonProperty("longs") @NonNull Map<String, Long> longs,
|
||||||
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
@JsonProperty("longArrays") @NonNull Map<String, long[]> longArrays,
|
||||||
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
@JsonProperty("floats") @NonNull Map<String, Float> floats,
|
||||||
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
@JsonProperty("floatArrays") @NonNull Map<String, float[]> floatArrays,
|
||||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
|
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
||||||
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays)
|
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
||||||
{
|
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
||||||
|
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
|
||||||
|
@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,19 +337,21 @@ public class Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Data build() {
|
public Data build() {
|
||||||
return new Data(strings,
|
return new Data(
|
||||||
stringArrays,
|
strings,
|
||||||
integers,
|
stringArrays,
|
||||||
integerArrays,
|
integers,
|
||||||
longs,
|
integerArrays,
|
||||||
longArrays,
|
longs,
|
||||||
floats,
|
longArrays,
|
||||||
floatArrays,
|
floats,
|
||||||
doubles,
|
floatArrays,
|
||||||
doubleArrays,
|
doubles,
|
||||||
booleans,
|
doubleArrays,
|
||||||
booleanArrays,
|
booleans,
|
||||||
byteArrays);
|
booleanArrays,
|
||||||
|
byteArrays
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,4 +360,3 @@ public class Data {
|
|||||||
@NonNull Data deserialize(@NonNull String serialized);
|
@NonNull Data deserialize(@NonNull String serialized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.")
|
||||||
|
@ -71,11 +71,11 @@ object OnionRequestEncryption {
|
|||||||
}
|
}
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
is OnionRequestAPI.Destination.Server -> {
|
||||||
payload = mutableMapOf(
|
payload = mutableMapOf(
|
||||||
"host" to rhs.host,
|
"host" to rhs.host,
|
||||||
"target" to rhs.target,
|
"target" to rhs.target,
|
||||||
"method" to "POST",
|
"method" to "POST",
|
||||||
"protocol" to rhs.scheme,
|
"protocol" to rhs.scheme,
|
||||||
"port" to rhs.port
|
"port" to rhs.port
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,8 @@ object SnodeAPI {
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
private val maxRetryCount = 6
|
private val maxRetryCount = 6
|
||||||
private val minimumSnodePoolCount = 24
|
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
|
||||||
|
|
||||||
@ -92,6 +92,7 @@ object SnodeAPI {
|
|||||||
"method" to "get_n_service_nodes",
|
"method" to "get_n_service_nodes",
|
||||||
"params" to mapOf(
|
"params" to mapOf(
|
||||||
"active_only" to true,
|
"active_only" to true,
|
||||||
|
"limit" to 256,
|
||||||
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
|
"fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true )
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -251,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> {
|
||||||
@ -304,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 -> {
|
||||||
@ -315,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) {
|
||||||
Log.d("Loki", "Invalidating swarm for: $publicKey.")
|
fun invalidateSwarm() {
|
||||||
dropSnodeFromSwarmIfNeeded(snode, publicKey)
|
Log.d("Loki", "Invalidating swarm for: $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.")
|
||||||
}
|
}
|
||||||
|
@ -3,23 +3,33 @@ 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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
internal fun toJSON(): Map<String, String> {
|
internal fun toJSON(): Map<String, String> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
||||||
"data" to data,
|
"data" to data,
|
||||||
"ttl" to ttl.toString(),
|
"ttl" to ttl.toString(),
|
||||||
"timestamp" to timestamp.toString(),
|
"timestamp" to timestamp.toString(),
|
||||||
"nonce" to ""
|
"nonce" to ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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}.")
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user