mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into open-group-invitations
This commit is contained in:
commit
d98cef3c77
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
|
||||||
|
|
||||||
|
@ -46,11 +46,12 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.1.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
|
implementation 'androidx.activity:activity-ktx:1.2.2'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
||||||
|
|
||||||
@ -157,8 +158,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 151
|
def canonicalVersionCode = 157
|
||||||
def canonicalVersionName = "1.9.1"
|
def canonicalVersionName = "1.10.0"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -84,6 +84,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
keyCachingService.setMasterSecret(new Object());
|
||||||
keyCachingService = null;
|
keyCachingService = null;
|
||||||
}
|
}
|
||||||
}, Context.BIND_AUTO_CREATE);
|
}, Context.BIND_AUTO_CREATE);
|
||||||
@ -133,7 +134,9 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
private void handleAuthenticated() {
|
private void handleAuthenticated() {
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
//TODO Replace with a proper call.
|
//TODO Replace with a proper call.
|
||||||
keyCachingService.setMasterSecret(new Object());
|
if (keyCachingService != null) {
|
||||||
|
keyCachingService.setMasterSecret(new Object());
|
||||||
|
}
|
||||||
|
|
||||||
// Finish and proceed with the next intent.
|
// Finish and proceed with the next intent.
|
||||||
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
Intent nextIntent = getIntent().getParcelableExtra("next_intent");
|
||||||
@ -188,6 +191,8 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
|
|
||||||
if (!keyguardManager.isKeyguardSecure()) {
|
if (!keyguardManager.isKeyguardSecure()) {
|
||||||
Log.w(TAG ,"Keyguard not secure...");
|
Log.w(TAG ,"Keyguard not secure...");
|
||||||
|
TextSecurePreferences.setScreenLockEnabled(getApplicationContext(), false);
|
||||||
|
TextSecurePreferences.setScreenLockTimeout(getApplicationContext(), 0);
|
||||||
handleAuthenticated();
|
handleAuthenticated();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -137,9 +137,20 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return openGroupMessagingDatabase.getMessageID(serverID)
|
return openGroupMessagingDatabase.getMessageID(serverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessage(messageID: Long) {
|
override fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>? {
|
||||||
val messagingDatabase = DatabaseFactory.getSmsDatabase(context)
|
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
messagingDatabase.deleteMessage(messageID)
|
return messageDB.getMessageID(serverId, threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
|
if (isSms) {
|
||||||
|
val db = DatabaseFactory.getSmsDatabase(context)
|
||||||
|
db.deleteMessage(messageID)
|
||||||
|
} else {
|
||||||
|
val db = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
db.delete(messageID)
|
||||||
|
}
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).deleteMessage(messageID, isSms)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
||||||
|
@ -83,21 +83,42 @@ import com.annimon.stream.Stream;
|
|||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager;
|
import org.session.libsession.messaging.mentions.MentionsManager;
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
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.link_preview.LinkPreview;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||||
|
import org.session.libsession.messaging.threads.Address;
|
||||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||||
|
import org.session.libsession.messaging.threads.GroupRecord;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.RecipientFormattingException;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||||
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
|
||||||
|
import org.session.libsession.utilities.views.Stub;
|
||||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
import org.session.libsignal.service.loki.Mention;
|
import org.session.libsignal.service.loki.Mention;
|
||||||
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
|
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
|
||||||
|
import org.session.libsignal.utilities.concurrent.ListenableFuture;
|
||||||
|
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||||
import org.thoughtcrime.securesms.MediaOverviewActivity;
|
import org.thoughtcrime.securesms.MediaOverviewActivity;
|
||||||
@ -121,7 +142,6 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
|||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||||
@ -135,7 +155,6 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
|||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
|
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
||||||
@ -158,8 +177,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.mms.QuoteId;
|
import org.thoughtcrime.securesms.mms.QuoteId;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
@ -168,31 +185,12 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
|
|||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientFormattingException;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
|
||||||
import org.session.libsession.messaging.threads.GroupRecord;
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
|
||||||
import org.session.libsession.utilities.views.Stub;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
|
|
||||||
import org.session.libsignal.utilities.concurrent.ListenableFuture;
|
|
||||||
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -377,9 +375,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
View rootView = findViewById(R.id.rootView);
|
View rootView = findViewById(R.id.rootView);
|
||||||
@ -1400,11 +1401,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null &&
|
if (publicChat != null &&
|
||||||
publicChat.getChannel() == event.getChannel() &&
|
publicChat.getChannel() == event.getChannel() &&
|
||||||
publicChat.getServer().equals(event.getUrl())) {
|
publicChat.getServer().equals(event.getUrl())) {
|
||||||
this.updateSubtitleTextView();
|
this.updateSubtitleTextView();
|
||||||
}
|
}
|
||||||
|
if (openGroup != null &&
|
||||||
|
openGroup.getRoom().equals(event.getRoom()) &&
|
||||||
|
openGroup.getServer().equals(event.getUrl())) {
|
||||||
|
this.updateSubtitleTextView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//////// Helper Methods
|
//////// Helper Methods
|
||||||
@ -1721,7 +1728,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
boolean initiating = threadId == -1;
|
boolean initiating = threadId == -1;
|
||||||
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
||||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||||
recipient.isGroupRecipient() ||
|
// recipient.isGroupRecipient() ||
|
||||||
inputPanel.getQuote().isPresent() ||
|
inputPanel.getQuote().isPresent() ||
|
||||||
linkPreviewViewModel.hasLinkPreview() ||
|
linkPreviewViewModel.hasLinkPreview() ||
|
||||||
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
||||||
@ -2338,10 +2345,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
||||||
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
|
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
|
||||||
if (userCount == null) { userCount = 0; }
|
if (userCount == null) { userCount = 0; }
|
||||||
subtitleTextView.setText(userCount + " members");
|
subtitleTextView.setText(userCount + " members");
|
||||||
|
} else if (openGroup != null) {
|
||||||
|
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
|
||||||
|
if (userCount == null) { userCount = 0; }
|
||||||
|
subtitleTextView.setText(userCount + " members");
|
||||||
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
|
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
|
||||||
subtitleTextView.setText(recipient.getAddress().toString());
|
subtitleTextView.setText(recipient.getAddress().toString());
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,49 +57,51 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.Quote;
|
import org.session.libsession.messaging.messages.visible.Quote;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
|
import org.session.libsession.messaging.threads.Address;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.session.libsession.utilities.concurrent.SimpleTask;
|
||||||
|
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
||||||
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
import org.thoughtcrime.securesms.ShareActivity;
|
import org.thoughtcrime.securesms.ShareActivity;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -397,7 +399,8 @@ public class ConversationFragment extends Fragment
|
|||||||
|
|
||||||
if (isGroupChat) {
|
if (isGroupChat) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
boolean isPublicChat = (publicChat != null);
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
boolean isPublicChat = (publicChat != null || openGroupChat != null);
|
||||||
int selectedMessageCount = messageRecords.size();
|
int selectedMessageCount = messageRecords.size();
|
||||||
boolean areAllSentByUser = true;
|
boolean areAllSentByUser = true;
|
||||||
Set<String> uniqueUserSet = new HashSet<>();
|
Set<String> uniqueUserSet = new HashSet<>();
|
||||||
@ -407,8 +410,12 @@ public class ConversationFragment extends Fragment
|
|||||||
}
|
}
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
||||||
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
||||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext());
|
||||||
boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
boolean userCanModerate =
|
||||||
|
(isPublicChat &&
|
||||||
|
((publicChat != null && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()))
|
||||||
|
|| (openGroupChat != null && OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())))
|
||||||
|
);
|
||||||
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
||||||
// allow banning if moderating a public chat and only one user's messages are selected
|
// allow banning if moderating a public chat and only one user's messages are selected
|
||||||
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
||||||
@ -509,6 +516,7 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -519,14 +527,14 @@ public class ConversationFragment extends Fragment
|
|||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(MessageRecord... messageRecords) {
|
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||||
if (publicChat != null) {
|
if (publicChat != null || openGroupChat != null) {
|
||||||
ArrayList<Long> serverIDs = new ArrayList<>();
|
ArrayList<Long> serverIDs = new ArrayList<>();
|
||||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||||
ArrayList<Long> failedMessages = new ArrayList<>();
|
ArrayList<Long> failedMessages = new ArrayList<>();
|
||||||
boolean isSentByUser = true;
|
boolean isSentByUser = true;
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
||||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
if (serverID != null) {
|
if (serverID != null) {
|
||||||
serverIDs.add(serverID);
|
serverIDs.add(serverID);
|
||||||
} else {
|
} else {
|
||||||
@ -538,7 +546,7 @@ public class ConversationFragment extends Fragment
|
|||||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||||
.success(l -> {
|
.success(l -> {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
if (l.contains(serverID)) {
|
if (l.contains(serverID)) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
||||||
@ -555,7 +563,25 @@ public class ConversationFragment extends Fragment
|
|||||||
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
|
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
} else if (openGroupChat != null) {
|
||||||
|
for (Long serverId : serverIDs) {
|
||||||
|
OpenGroupAPIV2
|
||||||
|
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
|
.success(l -> {
|
||||||
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
|
if (serverID != null && serverID.equals(serverId)) {
|
||||||
|
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).fail(e->{
|
||||||
|
Log.e("Loki", "Couldn't delete message due to error",e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
@ -591,7 +617,8 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
final OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
|
final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
||||||
ConversationAdapter chatAdapter = getListAdapter();
|
ConversationAdapter chatAdapter = getListAdapter();
|
||||||
@ -610,9 +637,19 @@ public class ConversationFragment extends Fragment
|
|||||||
Log.d("Loki", "User banned");
|
Log.d("Loki", "User banned");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
}).fail(e -> {
|
}).fail(e -> {
|
||||||
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
|
Log.e("Loki", "Couldn't ban user due to error",e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
} else if (openGroupChat != null) {
|
||||||
|
OpenGroupAPIV2
|
||||||
|
.ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
|
.success(l -> {
|
||||||
|
Log.d("Loki", "User banned");
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}).fail(e -> {
|
||||||
|
Log.e("Loki", "Failed to ban user",e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
Log.d("Loki", "Tried to ban user from a non-public chat");
|
Log.d("Loki", "Tried to ban user from a non-public chat");
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,8 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
|
|||||||
import org.session.libsession.messaging.jobs.JobQueue;
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
@ -88,6 +90,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.Quote;
|
import org.thoughtcrime.securesms.database.model.Quote;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
|
||||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
@ -724,9 +727,9 @@ public class ConversationItem extends LinearLayout
|
|||||||
String publicKey = recipient.getAddress().toString();
|
String publicKey = recipient.getAddress().toString();
|
||||||
profilePictureView.setPublicKey(publicKey);
|
profilePictureView.setPublicKey(publicKey);
|
||||||
String displayName = recipient.getName();
|
String displayName = recipient.getName();
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
||||||
if (displayName == null && publicChat != null) {
|
if (displayName == null && openGroup != null) {
|
||||||
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.getId(), publicKey);
|
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(openGroup.getId(), publicKey);
|
||||||
}
|
}
|
||||||
profilePictureView.setDisplayName(displayName);
|
profilePictureView.setDisplayName(displayName);
|
||||||
profilePictureView.setAdditionalPublicKey(null);
|
profilePictureView.setAdditionalPublicKey(null);
|
||||||
@ -867,7 +870,12 @@ public class ConversationItem extends LinearLayout
|
|||||||
try {
|
try {
|
||||||
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
|
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
|
||||||
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
|
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
|
||||||
if (senderDisplayName != null) { displayName = senderDisplayName; }
|
if (senderDisplayName != null) {
|
||||||
|
displayName = senderDisplayName;
|
||||||
|
} else {
|
||||||
|
// opengroupv2 format
|
||||||
|
displayName = OpenGroupUtilities.getDisplayName(recipient);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@ -912,9 +920,13 @@ public class ConversationItem extends LinearLayout
|
|||||||
int visibility = View.GONE;
|
int visibility = View.GONE;
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
||||||
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId());
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
|
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
|
||||||
visibility = isModerator ? View.VISIBLE : View.GONE;
|
visibility = isModerator ? View.VISIBLE : View.GONE;
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer());
|
||||||
|
visibility = isModerator ? View.VISIBLE : View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
moderatorIconImageView.setVisibility(visibility);
|
moderatorIconImageView.setVisibility(visibility);
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.StorageProtocol
|
import org.session.libsession.messaging.StorageProtocol
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||||
import org.session.libsession.messaging.jobs.Job
|
import org.session.libsession.messaging.jobs.Job
|
||||||
@ -13,6 +14,7 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
|||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
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.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
@ -226,6 +228,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAuthToken(room: String, server: String): String? {
|
||||||
|
val id = "$server.$room"
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAuthToken(room: String, server: String, newValue: String) {
|
||||||
|
val id = "$server.$room"
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAuthToken(room: String, server: String) {
|
||||||
|
val id = "$server.$room"
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getOpenGroup(threadID: String): OpenGroup? {
|
override fun getOpenGroup(threadID: String): OpenGroup? {
|
||||||
if (threadID.toInt() < 0) { return null }
|
if (threadID.toInt() < 0) { return null }
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
@ -235,6 +252,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getV2OpenGroup(threadId: String): OpenGroupV2? {
|
||||||
|
if (threadId.toInt() < 0) { return null }
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
||||||
|
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
||||||
|
OpenGroupV2.fromJson(publicChatAsJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getThreadID(openGroupID: String): String {
|
override fun getThreadID(openGroupID: String): String {
|
||||||
val address = Address.fromSerialized(openGroupID)
|
val address = Address.fromSerialized(openGroupID)
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
@ -254,11 +280,33 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) {
|
||||||
|
val groupID = "$server.$room"
|
||||||
|
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
|
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
|
||||||
val groupID = "$server.$channel"
|
val groupID = "$server.$channel"
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? {
|
||||||
|
val groupID = "$server.$room"
|
||||||
|
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLastMessageServerId(room: String, server: String): Long? {
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLastMessageServerId(room: String, server: String, newValue: Long) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeLastMessageServerId(room: String, server: String) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
|
||||||
}
|
}
|
||||||
@ -271,6 +319,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastDeletionServerId(room: String, server: String): Long? {
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeLastDeletionServerId(room: String, server: String) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserCount(room: String, server: String, newValue: Int) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
|
||||||
}
|
}
|
||||||
@ -315,9 +379,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
SessionMetaProtocol.addTimestamp(timestamp)
|
SessionMetaProtocol.addTimestamp(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun removeReceivedMessageTimestamps(timestamps: Set<Long>) {
|
override fun removeReceivedMessageTimestamps(timestamps: Set<Long>) {
|
||||||
// TODO("Not yet implemented")
|
SessionMetaProtocol.removeTimestamps(timestamps)
|
||||||
// }
|
}
|
||||||
|
|
||||||
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
|
override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? {
|
||||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
@ -325,8 +389,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.getMessageFor(timestamp, address)?.getId()
|
return database.getMessageFor(timestamp, address)?.getId()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) {
|
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
|
||||||
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
|
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
||||||
@ -475,8 +540,27 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOpenGroup(server: String, channel: Long) {
|
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
OpenGroupUtilities.addGroup(context, server, channel)
|
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addOpenGroup(serverUrl: String, channel: Long) {
|
||||||
|
val httpUrl = HttpUrl.parse(serverUrl) ?: return
|
||||||
|
if (httpUrl.queryParameterNames().contains("public_key")) {
|
||||||
|
// open group v2
|
||||||
|
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
||||||
|
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
||||||
|
// non-standard port, add to server
|
||||||
|
this.port(httpUrl.port())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
val room = httpUrl.pathSegments().firstOrNull() ?: return
|
||||||
|
val publicKey = httpUrl.queryParameter("public_key") ?: return
|
||||||
|
|
||||||
|
OpenGroupUtilities.addGroup(context, server.toString().removeSuffix("/"), room, publicKey)
|
||||||
|
} else {
|
||||||
|
OpenGroupUtilities.addGroup(context, serverUrl, channel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllGroups(): List<GroupRecord> {
|
override fun getAllGroups(): List<GroupRecord> {
|
||||||
@ -513,6 +597,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return if (threadID < 0) null else threadID
|
return if (threadID < 0) null else threadID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getThreadIdForMms(mmsId: Long): Long {
|
||||||
|
val mmsDb = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
val cursor = mmsDb.getMessage(mmsId)
|
||||||
|
val reader = mmsDb.readerFor(cursor)
|
||||||
|
val threadId = reader.next.threadId
|
||||||
|
cursor.close()
|
||||||
|
return threadId
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
|
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
|
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
|
||||||
@ -410,6 +411,7 @@ public class ThreadDatabase extends Database {
|
|||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
|
SessionMetaProtocol.clearReceivedMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasThread(long threadId) {
|
public boolean hasThread(long threadId) {
|
||||||
|
@ -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;
|
||||||
@ -125,6 +126,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||||
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
|
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
|
||||||
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
|
||||||
|
|
||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
@ -275,6 +278,17 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
if (oldVersion < lokiV23) {
|
if (oldVersion < lokiV23) {
|
||||||
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
|
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
|
||||||
|
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();
|
||||||
|
@ -353,6 +353,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||||
|
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
@ -364,6 +365,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
.removeChat(publicChat.server, publicChat.channel)
|
.removeChat(publicChat.server, publicChat.channel)
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
|
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server)
|
||||||
|
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server)
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
|
.removeChat(openGroupV2.server, openGroupV2.room)
|
||||||
} else {
|
} else {
|
||||||
threadDB.deleteConversation(threadID)
|
threadDB.deleteConversation(threadID)
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,49 @@ package org.thoughtcrime.securesms.loki.activities
|
|||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Patterns
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.activity.viewModels
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.messaging.threads.DistributionTypes
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||||
|
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
|
||||||
|
import org.thoughtcrime.securesms.loki.viewmodel.State
|
||||||
|
|
||||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
|
||||||
|
private val viewModel by viewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
@ -65,16 +83,43 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun joinPublicChatIfPossible(url: String) {
|
fun joinPublicChatIfPossible(url: String) {
|
||||||
if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) {
|
// add http if just an IP style / host style URL is entered but leave it if scheme is included
|
||||||
return Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
val properString = if (!url.startsWith("http")) "http://$url" else url
|
||||||
}
|
val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
val room = httpUrl.pathSegments().firstOrNull()
|
||||||
|
val publicKey = httpUrl.queryParameter("public_key")
|
||||||
|
val isV2OpenGroup = !room.isNullOrEmpty()
|
||||||
showLoader()
|
showLoader()
|
||||||
val channel: Long = 1
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
|
val (threadID, groupID) = if (isV2OpenGroup) {
|
||||||
|
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
||||||
|
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
||||||
|
// non-standard port, add to server
|
||||||
|
this.port(httpUrl.port())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||||
|
threadID to groupID
|
||||||
|
} else {
|
||||||
|
val channel: Long = 1
|
||||||
|
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel)
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||||
|
threadID to groupID
|
||||||
|
}
|
||||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
// go to the new conversation and finish this one
|
||||||
|
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@ -83,10 +128,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) { finish() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region Convenience
|
||||||
|
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||||
|
val intent = Intent(context, ConversationActivity::class.java)
|
||||||
|
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||||
|
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||||
|
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Adapter
|
// region Adapter
|
||||||
@ -109,7 +163,7 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(index: Int): CharSequence? {
|
override fun getPageTitle(index: Int): CharSequence {
|
||||||
return when (index) {
|
return when (index) {
|
||||||
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
|
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
|
||||||
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
||||||
@ -122,24 +176,63 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
|||||||
// region Enter Chat URL Fragment
|
// region Enter Chat URL Fragment
|
||||||
class EnterChatURLFragment : Fragment() {
|
class EnterChatURLFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||||
|
defaultRoomsGridLayout.removeAllViews()
|
||||||
|
groups.forEach { defaultGroup ->
|
||||||
|
val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip
|
||||||
|
val drawable = defaultGroup.image?.let { bytes ->
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size)
|
||||||
|
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||||
|
isCircular = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chip.chipIcon = drawable
|
||||||
|
chip.text = defaultGroup.name
|
||||||
|
chip.setOnClickListener {
|
||||||
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl())
|
||||||
|
}
|
||||||
|
defaultRoomsGridLayout.addView(chip)
|
||||||
|
}
|
||||||
|
if (groups.size and 1 != 0) {
|
||||||
|
// add a filler weight 1 view
|
||||||
|
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||||
|
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||||
|
defaultRoomsParent.isVisible = state is State.Success
|
||||||
|
defaultRoomsLoader.isVisible = state is State.Loading
|
||||||
|
when (state) {
|
||||||
|
State.Loading -> {
|
||||||
|
// show a loader here probs
|
||||||
|
}
|
||||||
|
is State.Error -> {
|
||||||
|
// hide the loader and the
|
||||||
|
}
|
||||||
|
is State.Success -> {
|
||||||
|
populateDefaultGroups(state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Convenience
|
||||||
private fun joinPublicChatIfPossible() {
|
private fun joinPublicChatIfPossible() {
|
||||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||||
var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
|
val chatURL = chatURLEditText.text.trim().toString().toLowerCase()
|
||||||
if (!chatURL.toLowerCase().startsWith("https")) {
|
|
||||||
chatURL = "https://$chatURL"
|
|
||||||
}
|
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
@ -9,8 +9,10 @@ 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.OpenGroup
|
||||||
|
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
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||||
import org.session.libsession.snode.SnodeAPI
|
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
|
||||||
@ -90,6 +92,14 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
promises.add(poller.pollForNewMessages())
|
promises.add(poller.pollForNewMessages())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val openGroupsV2 = DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups().values.groupBy(OpenGroupV2::server)
|
||||||
|
|
||||||
|
openGroupsV2.values.map { groups ->
|
||||||
|
OpenGroupV2Poller(groups)
|
||||||
|
}.forEach { poller ->
|
||||||
|
promises.add(poller.compactPoll(true).map{ /*Unit*/ })
|
||||||
|
}
|
||||||
|
|
||||||
// Wait till all the promises get resolved
|
// Wait till all the promises get resolved
|
||||||
all(promises).get()
|
all(promises).get()
|
||||||
|
|
||||||
|
@ -16,6 +16,25 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
|||||||
|
|
||||||
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
||||||
private const val DATA_KEY_CHANNEL = "channel"
|
private const val DATA_KEY_CHANNEL = "channel"
|
||||||
|
private const val DATA_KEY_ROOM = "room"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun scheduleInstant(context: Context, serverUrl: String, room :String) {
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
|
||||||
|
.setConstraints(Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setInputData(workDataOf(
|
||||||
|
DATA_KEY_SERVER_URL to serverUrl,
|
||||||
|
DATA_KEY_ROOM to room
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueue(workRequest)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
||||||
@ -39,17 +58,35 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
||||||
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
||||||
|
val room = inputData.getString(DATA_KEY_ROOM)
|
||||||
|
|
||||||
val publicChatId = OpenGroup.getId(channel, serverUrl)
|
val isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
|
||||||
|
|
||||||
|
if (!isOpenGroupV2) {
|
||||||
|
val publicChatId = OpenGroup.getId(channel, serverUrl)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.v(TAG, "Updating open group info for $publicChatId.")
|
||||||
|
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
|
||||||
|
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val openGroupId = "$serverUrl.$room"
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.v(TAG, "Updating open group info for $openGroupId.")
|
||||||
|
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
|
||||||
|
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
|
||||||
Log.v(TAG, "Updating open group info for $publicChatId.")
|
|
||||||
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
|
|
||||||
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
|
|
||||||
Result.success()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
|
|
||||||
Result.failure()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,9 @@ import android.graphics.Bitmap
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupInfo
|
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
@ -20,15 +19,17 @@ import java.util.concurrent.Executors
|
|||||||
|
|
||||||
class PublicChatManager(private val context: Context) {
|
class PublicChatManager(private val context: Context) {
|
||||||
private var chats = mutableMapOf<Long, OpenGroup>()
|
private var chats = mutableMapOf<Long, OpenGroup>()
|
||||||
|
private var v2Chats = mutableMapOf<Long, OpenGroupV2>()
|
||||||
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
|
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
|
||||||
|
private val v2Pollers = mutableMapOf<String, OpenGroupV2Poller>()
|
||||||
private val observers = mutableMapOf<Long, ContentObserver>()
|
private val observers = mutableMapOf<Long, ContentObserver>()
|
||||||
private var isPolling = false
|
private var isPolling = false
|
||||||
private val executorService = Executors.newScheduledThreadPool(16)
|
private val executorService = Executors.newScheduledThreadPool(4)
|
||||||
|
|
||||||
public fun areAllCaughtUp(): Boolean {
|
public fun areAllCaughtUp(): Boolean {
|
||||||
var areAllCaughtUp = true
|
var areAllCaughtUp = true
|
||||||
refreshChatsAndPollers()
|
refreshChatsAndPollers()
|
||||||
for ((threadID, chat) 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 true
|
||||||
}
|
}
|
||||||
@ -52,6 +53,17 @@ class PublicChatManager(private val context: Context) {
|
|||||||
listenToThreadDeletion(threadId)
|
listenToThreadDeletion(threadId)
|
||||||
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
||||||
}
|
}
|
||||||
|
v2Pollers.values.forEach { it.stop() }
|
||||||
|
v2Pollers.clear()
|
||||||
|
v2Chats.entries.groupBy { (_, group) -> group.server }.forEach { (server, threadedRooms) ->
|
||||||
|
val poller = OpenGroupV2Poller(threadedRooms.map { it.value }, executorService)
|
||||||
|
poller.startIfNeeded()
|
||||||
|
threadedRooms.forEach { (thread, _) ->
|
||||||
|
listenToThreadDeletion(thread)
|
||||||
|
}
|
||||||
|
v2Pollers[server] = poller
|
||||||
|
}
|
||||||
|
|
||||||
isPolling = true
|
isPolling = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +110,26 @@ class PublicChatManager(private val context: Context) {
|
|||||||
return chat
|
return chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info, publicKey: String): OpenGroupV2 {
|
||||||
|
val chat = OpenGroupV2(server, room, info.name, publicKey)
|
||||||
|
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||||
|
val profilePicture: Bitmap?
|
||||||
|
if (threadID < 0) {
|
||||||
|
val profilePictureAsByteArray = try {
|
||||||
|
OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id,server).get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
||||||
|
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name)
|
||||||
|
threadID = result.threadId
|
||||||
|
}
|
||||||
|
DatabaseFactory.getLokiThreadDatabase(context).setOpenGroupChat(chat, threadID)
|
||||||
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
return chat
|
||||||
|
}
|
||||||
|
|
||||||
public fun removeChat(server: String, channel: Long) {
|
public fun removeChat(server: String, channel: Long) {
|
||||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
val groupId = OpenGroup.getId(channel, server)
|
val groupId = OpenGroup.getId(channel, server)
|
||||||
@ -108,14 +140,26 @@ class PublicChatManager(private val context: Context) {
|
|||||||
Util.runOnMain { startPollersIfNeeded() }
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeChat(server: String, room: String) {
|
||||||
|
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
|
val groupId = "$server.$room"
|
||||||
|
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||||
|
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
||||||
|
GroupManager.deleteGroup(groupAddress, context)
|
||||||
|
|
||||||
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshChatsAndPollers() {
|
private fun refreshChatsAndPollers() {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val chatsInDB = storage.getAllOpenGroups()
|
val chatsInDB = storage.getAllOpenGroups()
|
||||||
|
val v2ChatsInDB = storage.getAllV2OpenGroups()
|
||||||
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
|
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
|
||||||
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
|
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
|
||||||
|
|
||||||
// Only append to chats if we have a thread for the chat
|
// Only append to chats if we have a thread for the chat
|
||||||
chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
||||||
|
v2Chats = v2ChatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenToThreadDeletion(threadID: Long) {
|
private fun listenToThreadDeletion(threadID: Long) {
|
||||||
@ -132,6 +176,8 @@ class PublicChatManager(private val context: Context) {
|
|||||||
|
|
||||||
DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID)
|
DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID)
|
||||||
pollers.remove(threadID)?.stop()
|
pollers.remove(threadID)?.stop()
|
||||||
|
v2Pollers.values.forEach { it.stop() }
|
||||||
|
v2Pollers.clear()
|
||||||
observers.remove(threadID)
|
observers.remove(threadID)
|
||||||
startPollersIfNeeded()
|
startPollersIfNeeded()
|
||||||
}
|
}
|
||||||
|
@ -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);"
|
||||||
@ -286,6 +286,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastMessageServerID(room: String, server: String): Long? {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(lastMessageServerID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -293,12 +301,25 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setLastMessageServerID(room: String, server: String, newValue: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() ))
|
||||||
|
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLastMessageServerID(group: Long, server: String) {
|
fun removeLastMessageServerID(group: Long, server: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
|
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeLastMessageServerID(room: String, server:String) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -307,6 +328,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastDeletionServerID(room: String, server: String): Long? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(lastDeletionServerID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -314,6 +343,19 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setLastDeletionServerID(room: String, server: String, newValue: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString()))
|
||||||
|
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLastDeletionServerID(room: String, server: String) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLastDeletionServerID(group: Long, server: String) {
|
fun removeLastDeletionServerID(group: Long, server: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -328,6 +370,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toInt()
|
}?.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserCount(room: String, server: String): Int? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(userCount)
|
||||||
|
}?.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -335,6 +385,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setUserCount(room: String, server: String, newValue: Int) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() ))
|
||||||
|
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
||||||
|
@ -5,10 +5,7 @@ import android.content.Context
|
|||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.get
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getInt
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
|
|
||||||
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
|
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
|
||||||
|
|
||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
||||||
@ -22,56 +19,111 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
private val friendRequestStatus = "friend_request_status"
|
private val friendRequestStatus = "friend_request_status"
|
||||||
private val threadID = "thread_id"
|
private val threadID = "thread_id"
|
||||||
private val errorMessage = "error_message"
|
private val errorMessage = "error_message"
|
||||||
@JvmStatic val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
private val messageType = "message_type"
|
||||||
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
@JvmStatic
|
||||||
@JvmStatic val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
|
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
||||||
|
@JvmStatic
|
||||||
|
val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
||||||
|
@JvmStatic
|
||||||
|
val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
|
||||||
|
@JvmStatic
|
||||||
|
val updateMessageIDTableForType = "ALTER TABLE $messageIDTable ADD COLUMN $messageType INTEGER DEFAULT 0; ALTER TABLE $messageIDTable ADD CONSTRAINT PK_$messageIDTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
|
@JvmStatic
|
||||||
|
val updateMessageMappingTable = "ALTER TABLE $messageThreadMappingTable ADD COLUMN $serverID INTEGER DEFAULT 0; ALTER TABLE $messageThreadMappingTable ADD CONSTRAINT PK_$messageThreadMappingTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
|
|
||||||
|
const val SMS_TYPE = 0
|
||||||
|
const val MMS_TYPE = 1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
|
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
|
||||||
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
|
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
|
||||||
return if (message != null) getServerID(message.getId()) else null
|
return if (message != null) getServerID(message.getId(), !message.isMms) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getServerID(messageID: Long): Long? {
|
fun getServerID(messageID: Long): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
|
cursor.getInt(serverID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerID(messageID: Long, isSms: Boolean): Long? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(messageIDTable, "${Companion.messageID} = ? AND $messageType = ?", arrayOf(messageID.toString(), if (isSms) SMS_TYPE.toString() else MMS_TYPE.toString())) { cursor ->
|
||||||
cursor.getInt(serverID)
|
cursor.getInt(serverID)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMessageID(serverID: Long): Long? {
|
fun getMessageID(serverID: Long): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
|
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf(serverID.toString())) { cursor ->
|
||||||
cursor.getInt(messageID)
|
cursor.getInt(messageID)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setServerID(messageID: Long, serverID: Long) {
|
fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
|
||||||
|
val serverID = database.get(messageIDTable,
|
||||||
|
"${Companion.messageID} = ? AND ${Companion.messageType} = ?",
|
||||||
|
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
||||||
|
cursor.getInt(Companion.serverID).toLong()
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
database.beginTransaction()
|
||||||
|
|
||||||
|
database.delete(messageIDTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
|
database.delete(messageThreadMappingTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",
|
||||||
|
arrayOf(serverID.toString(), threadID.toString())) { cursor ->
|
||||||
|
cursor.getInt(messageID) to cursor.getInt(Companion.serverID)
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
val (mappedID, mappedServerID) = mappingResult
|
||||||
|
|
||||||
|
return database.get(messageIDTable,
|
||||||
|
"$messageID = ? AND ${Companion.serverID} = ?",
|
||||||
|
arrayOf(mappedID.toString(), mappedServerID.toString())) { cursor ->
|
||||||
|
cursor.getInt(Companion.messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.serverID, serverID)
|
contentValues.put(Companion.serverID, serverID)
|
||||||
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
|
||||||
|
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOriginalThreadID(messageID: Long): Long {
|
fun getOriginalThreadID(messageID: Long): Long {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
cursor.getInt(threadID)
|
cursor.getInt(threadID)
|
||||||
}?.toLong() ?: -1L
|
}?.toLong() ?: -1L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOriginalThreadID(messageID: Long, threadID: Long) {
|
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
|
contentValues.put(Companion.serverID, serverID)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
contentValues.put(Companion.threadID, threadID)
|
||||||
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getErrorMessage(messageID: Long): String? {
|
fun getErrorMessage(messageID: Long): String? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
cursor.getString(errorMessage)
|
cursor.getString(errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,6 +133,6 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.errorMessage, errorMessage)
|
contentValues.put(Companion.errorMessage, errorMessage)
|
||||||
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,12 +10,11 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
|||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
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.TextSecurePreferences
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
|
||||||
|
|
||||||
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
|
|
||||||
@ -26,8 +25,10 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
private val friendRequestStatus = "friend_request_status"
|
private val friendRequestStatus = "friend_request_status"
|
||||||
private val sessionResetStatus = "session_reset_status"
|
private val sessionResetStatus = "session_reset_status"
|
||||||
val publicChat = "public_chat"
|
val publicChat = "public_chat"
|
||||||
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
|
@JvmStatic
|
||||||
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
|
val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
|
||||||
|
@JvmStatic
|
||||||
|
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThreadID(hexEncodedPublicKey: String): Long {
|
fun getThreadID(hexEncodedPublicKey: String): Long {
|
||||||
@ -46,11 +47,33 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
val threadID = cursor.getLong(threadID)
|
val threadID = cursor.getLong(threadID)
|
||||||
val string = cursor.getString(publicChat)
|
val string = cursor.getString(publicChat)
|
||||||
val publicChat = OpenGroup.fromJSON(string)
|
val publicChat = OpenGroup.fromJSON(string)
|
||||||
if (publicChat != null) { result[threadID] = publicChat }
|
if (publicChat != null) {
|
||||||
|
result[threadID] = publicChat
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
} finally {
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
val result = mutableMapOf<Long, OpenGroupV2>()
|
||||||
|
try {
|
||||||
|
cursor = database.rawQuery("select * from $publicChatTable", null)
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
val threadID = cursor.getLong(threadID)
|
||||||
|
val string = cursor.getString(publicChat)
|
||||||
|
val openGroup = OpenGroupV2.fromJson(string)
|
||||||
|
if (openGroup != null) result[threadID] = openGroup
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
cursor?.close()
|
cursor?.close()
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -62,23 +85,48 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
|
|
||||||
fun getPublicChat(threadID: Long): OpenGroup? {
|
fun getPublicChat(threadID: Long): OpenGroup? {
|
||||||
if (threadID < 0) { return null }
|
if (threadID < 0) { return null }
|
||||||
|
|
||||||
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 publicChatAsJSON = cursor.getString(publicChat)
|
val publicChatAsJSON = cursor.getString(publicChat)
|
||||||
OpenGroup.fromJSON(publicChatAsJSON)
|
OpenGroup.fromJSON(publicChatAsJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
||||||
|
if (threadID < 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||||
|
val json = cursor.getString(publicChat)
|
||||||
|
OpenGroupV2.fromJson(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOpenGroupChat(openGroupV2: OpenGroupV2, threadID: Long) {
|
||||||
|
if (threadID < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val contentValues = ContentValues(2)
|
||||||
|
contentValues.put(Companion.threadID, threadID)
|
||||||
|
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
|
||||||
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
|
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
|
||||||
if (threadID < 0) { return }
|
if (threadID < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
contentValues.put(Companion.threadID, threadID)
|
||||||
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
|
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
|
||||||
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePublicChat(threadID: Long) {
|
fun removePublicChat(threadID: Long) {
|
||||||
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
|
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 ->
|
||||||
|
@ -24,6 +24,15 @@ object SessionMetaProtocol {
|
|||||||
timestamps.add(timestamp)
|
timestamps.add(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clearReceivedMessages() {
|
||||||
|
timestamps.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTimestamps(timestamps: Set<Long>) {
|
||||||
|
this.timestamps.removeAll(timestamps)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun shouldIgnoreMessage(timestamp: Long): Boolean {
|
fun shouldIgnoreMessage(timestamp: Long): Boolean {
|
||||||
val shouldIgnoreMessage = timestamps.contains(timestamp)
|
val shouldIgnoreMessage = timestamps.contains(timestamp)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.loki.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
|
|
||||||
object MentionManagerUtilities {
|
object MentionManagerUtilities {
|
||||||
|
|
||||||
|
@ -3,20 +3,47 @@ package org.thoughtcrime.securesms.loki.utilities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
||||||
object OpenGroupUtilities {
|
object OpenGroupUtilities {
|
||||||
|
|
||||||
private const val TAG = "OpenGroupUtilities"
|
private const val TAG = "OpenGroupUtilities"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@WorkerThread
|
||||||
|
fun addGroup(context: Context, server: String, room: String, publicKey: String): OpenGroupV2 {
|
||||||
|
val groupId = "$server.$room"
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||||
|
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
|
if (openGroup != null) return openGroup
|
||||||
|
|
||||||
|
MessagingModuleConfiguration.shared.storage.setOpenGroupPublicKey(server,publicKey)
|
||||||
|
OpenGroupAPIV2.getAuthToken(room, server).get()
|
||||||
|
val groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
|
||||||
|
val application = ApplicationContext.getInstance(context)
|
||||||
|
|
||||||
|
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
||||||
|
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.removeLastDeletionServerId(room, server)
|
||||||
|
storage.removeLastMessageServerId(room, server)
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -67,5 +94,30 @@ object OpenGroupUtilities {
|
|||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GroupInfoUpdatedEvent(val url: String, val channel: Long)
|
@JvmStatic
|
||||||
|
@WorkerThread
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateGroupInfo(context: Context, server: String, room: String) {
|
||||||
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
|
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
||||||
|
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again?
|
||||||
|
OpenGroupAPIV2.getMemberCount(room, server).get()
|
||||||
|
|
||||||
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a generated name for users in the style of `$name (...$hex.takeLast(8))` for public groups
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getDisplayName(recipient: Recipient): String {
|
||||||
|
return String.format(Locale.ROOT, PUBLIC_GROUP_STRING_FORMAT, recipient.name, recipient.address.serialize().takeLast(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
const val PUBLIC_GROUP_STRING_FORMAT = "%s (...%s)"
|
||||||
|
|
||||||
|
data class GroupInfoUpdatedEvent(val url: String, val channel: Long = -1, val room: String = "")
|
||||||
}
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
|
||||||
|
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
|
||||||
|
typealias GroupState = State<DefaultGroups>
|
||||||
|
|
||||||
|
class DefaultGroupsViewModel : ViewModel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultRooms = OpenGroupAPIV2.defaultRooms.map<DefaultGroups, GroupState> {
|
||||||
|
State.Success(it)
|
||||||
|
}.onStart {
|
||||||
|
emit(State.Loading)
|
||||||
|
}.asLiveData()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.viewmodel
|
||||||
|
|
||||||
|
sealed class State<out T> {
|
||||||
|
object Loading : State<Nothing>()
|
||||||
|
data class Success<T>(val value: T): State<T>()
|
||||||
|
data class Error(val error: Exception): State<Nothing>()
|
||||||
|
}
|
@ -102,7 +102,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||||||
if (duration == 0) {
|
if (duration == 0) {
|
||||||
TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
|
TextSecurePreferences.setScreenLockTimeout(getContext(), 0);
|
||||||
} else {
|
} else {
|
||||||
long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
|
long timeoutSeconds = TimeUnit.MILLISECONDS.toSeconds(duration);
|
||||||
|
// long timeoutSeconds = Math.max(TimeUnit.MILLISECONDS.toSeconds(duration), 60);
|
||||||
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
|
TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,6 @@ public class KeyCachingService extends Service {
|
|||||||
KeyCachingService.masterSecret = masterSecret;
|
KeyCachingService.masterSecret = masterSecret;
|
||||||
|
|
||||||
foregroundService();
|
foregroundService();
|
||||||
startTimeoutIfAppropriate(this);
|
|
||||||
|
|
||||||
new AsyncTask<Void, Void, Void>() {
|
new AsyncTask<Void, Void, Void>() {
|
||||||
@Override
|
@Override
|
||||||
@ -210,7 +209,7 @@ public class KeyCachingService extends Service {
|
|||||||
boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context);
|
boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context);
|
||||||
|
|
||||||
long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context);
|
long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context);
|
||||||
boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context);
|
boolean screenLockActive = screenTimeout >= 0 && TextSecurePreferences.isScreenLockEnabled(context);
|
||||||
|
|
||||||
if (!appVisible && secretSet && (passLockActive || screenLockActive)) {
|
if (!appVisible && secretSet && (passLockActive || screenLockActive)) {
|
||||||
long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context);
|
long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/contentView"
|
android:id="@+id/contentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -22,7 +21,36 @@
|
|||||||
android:layout_marginTop="@dimen/large_spacing"
|
android:layout_marginTop="@dimen/large_spacing"
|
||||||
android:layout_marginRight="@dimen/large_spacing"
|
android:layout_marginRight="@dimen/large_spacing"
|
||||||
android:inputType="textWebEmailAddress"
|
android:inputType="textWebEmailAddress"
|
||||||
android:hint="Enter an open group URL" />
|
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||||
|
|
||||||
|
<com.github.ybq.android.spinkit.SpinKitView
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/defaultRoomsLoader"
|
||||||
|
style="@style/SpinKitView.Small.WanderingCubes"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:id="@+id/defaultRoomsParent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
|
android:columnCount="2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
14
app/src/main/res/layout/default_group_chip.xml
Normal file
14
app/src/main/res/layout/default_group_chip.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:theme="@style/Theme.MaterialComponents.DayNight"
|
||||||
|
style="?attr/chipStyle"
|
||||||
|
app:chipStartPadding="6dp"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
android:layout_marginHorizontal="2dp"
|
||||||
|
tools:text="Main Group"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:layout_width="wrap_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp" />
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/contentView"
|
android:id="@+id/contentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -24,6 +23,35 @@
|
|||||||
android:inputType="textWebEmailAddress"
|
android:inputType="textWebEmailAddress"
|
||||||
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||||
|
|
||||||
|
<com.github.ybq.android.spinkit.SpinKitView
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/defaultRoomsLoader"
|
||||||
|
style="@style/SpinKitView.Small.WanderingCubes"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:id="@+id/defaultRoomsParent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
|
android:columnCount="2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
5
app/src/main/res/layout/grid_layout_filler.xml
Normal file
5
app/src/main/res/layout/grid_layout_filler.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
android:layout_height="0dp"/>
|
@ -1898,5 +1898,6 @@
|
|||||||
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
||||||
<!-- LinkDeviceActivity -->
|
<!-- LinkDeviceActivity -->
|
||||||
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
||||||
|
<string name="activity_join_public_chat_join_rooms">Or join one of these...</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,7 +11,8 @@ import java.io.InputStream
|
|||||||
interface MessageDataProvider {
|
interface MessageDataProvider {
|
||||||
|
|
||||||
fun getMessageID(serverID: Long): Long?
|
fun getMessageID(serverID: Long): Long?
|
||||||
fun deleteMessage(messageID: Long)
|
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||||
|
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||||
|
|
||||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
|||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
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.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
@ -52,16 +53,18 @@ interface StorageProtocol {
|
|||||||
fun isJobCanceled(job: Job): Boolean
|
fun isJobCanceled(job: Job): Boolean
|
||||||
|
|
||||||
// Authorization
|
// Authorization
|
||||||
fun getAuthToken(server: String): String?
|
fun getAuthToken(room: String, server: String): String?
|
||||||
fun setAuthToken(server: String, newValue: String?)
|
fun setAuthToken(room: String, server: String, newValue: String)
|
||||||
fun removeAuthToken(server: String)
|
fun removeAuthToken(room: String, server: String)
|
||||||
|
|
||||||
|
// Open Groups
|
||||||
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
||||||
|
fun getV2OpenGroup(threadId: String): OpenGroupV2?
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
fun getOpenGroup(threadID: String): OpenGroup?
|
|
||||||
fun getThreadID(openGroupID: String): String?
|
fun getThreadID(openGroupID: String): String?
|
||||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
fun addOpenGroup(serverUrl: String, channel: Long)
|
||||||
fun addOpenGroup(server: String, channel: Long)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
|
||||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||||
|
|
||||||
// Open Group Public Keys
|
// Open Group Public Keys
|
||||||
@ -69,31 +72,30 @@ interface StorageProtocol {
|
|||||||
fun setOpenGroupPublicKey(server: String, newValue: String)
|
fun setOpenGroupPublicKey(server: String, newValue: String)
|
||||||
|
|
||||||
// Open Group User Info
|
// Open Group User Info
|
||||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
|
||||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
|
||||||
|
|
||||||
// Open Group Metadata
|
// Open Group Metadata
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
|
||||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
|
||||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
|
||||||
fun updateTitle(groupID: String, newValue: String)
|
fun updateTitle(groupID: String, newValue: String)
|
||||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||||
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
|
|
||||||
// Last Message Server ID
|
// Last Message Server ID
|
||||||
fun getLastMessageServerID(group: Long, server: String): Long?
|
fun getLastMessageServerId(room: String, server: String): Long?
|
||||||
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
fun setLastMessageServerId(room: String, server: String, newValue: Long)
|
||||||
fun removeLastMessageServerID(group: Long, server: String)
|
fun removeLastMessageServerId(room: String, server: String)
|
||||||
|
|
||||||
// Last Deletion Server ID
|
// Last Deletion Server ID
|
||||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
fun getLastDeletionServerId(room: String, server: String): Long?
|
||||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
fun setLastDeletionServerId(room: String, server: String, newValue: Long)
|
||||||
fun removeLastDeletionServerID(group: Long, server: String)
|
fun removeLastDeletionServerId(room: String, server: String)
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
||||||
fun getReceivedMessageTimestamps(): Set<Long>
|
fun getReceivedMessageTimestamps(): Set<Long>
|
||||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||||
// fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||||
// Returns the IDs of the saved attachments.
|
// Returns the IDs of the saved attachments.
|
||||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
||||||
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
||||||
@ -137,6 +139,7 @@ interface StorageProtocol {
|
|||||||
fun getOrCreateThreadIdFor(address: Address): Long
|
fun getOrCreateThreadIdFor(address: Address): Long
|
||||||
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
||||||
fun getThreadIdFor(address: Address): Long?
|
fun getThreadIdFor(address: Address): Long?
|
||||||
|
fun getThreadIdForMms(mmsId: Long): Long
|
||||||
|
|
||||||
// Session Request
|
// Session Request
|
||||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||||
@ -164,4 +167,28 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Data Extraction Notification
|
// Data Extraction Notification
|
||||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||||
|
|
||||||
|
// DEPRECATED
|
||||||
|
fun getAuthToken(server: String): String?
|
||||||
|
fun setAuthToken(server: String, newValue: String?)
|
||||||
|
fun removeAuthToken(server: String)
|
||||||
|
|
||||||
|
fun getLastMessageServerID(group: Long, server: String): Long?
|
||||||
|
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
||||||
|
fun removeLastMessageServerID(group: Long, server: String)
|
||||||
|
|
||||||
|
fun getLastDeletionServerID(group: Long, server: String): Long?
|
||||||
|
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||||
|
fun removeLastDeletionServerID(group: Long, server: String)
|
||||||
|
|
||||||
|
fun getOpenGroup(threadID: String): OpenGroup?
|
||||||
|
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||||
|
|
||||||
|
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||||
|
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||||
|
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||||
|
|
||||||
|
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
||||||
|
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
package org.session.libsession.messaging.file_server
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.bind
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
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.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
|
object FileServerAPIV2 {
|
||||||
|
|
||||||
|
const val DEFAULT_SERVER = "http://88.99.175.227"
|
||||||
|
private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||||
|
|
||||||
|
sealed class Error : Exception() {
|
||||||
|
object PARSING_FAILED : Error()
|
||||||
|
object INVALID_URL : Error()
|
||||||
|
|
||||||
|
fun errorDescription() = when (this) {
|
||||||
|
PARSING_FAILED -> "Invalid response."
|
||||||
|
INVALID_URL -> "Invalid URL."
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Request(
|
||||||
|
val verb: HTTP.Verb,
|
||||||
|
val endpoint: String,
|
||||||
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
|
val parameters: Any? = null,
|
||||||
|
val headers: Map<String, String> = mapOf(),
|
||||||
|
// Always `true` under normal circumstances. You might want to disable
|
||||||
|
// this when running over Lokinet.
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
|
if (parameters == null) return null
|
||||||
|
|
||||||
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||||
|
val parsed = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.INVALID_URL)
|
||||||
|
val urlBuilder = HttpUrl.Builder()
|
||||||
|
.scheme(parsed.scheme())
|
||||||
|
.host(parsed.host())
|
||||||
|
.port(parsed.port())
|
||||||
|
.addPathSegments(request.endpoint)
|
||||||
|
|
||||||
|
if (request.verb == HTTP.Verb.GET) {
|
||||||
|
for ((key, value) in request.queryParameters) {
|
||||||
|
urlBuilder.addQueryParameter(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
|
.url(urlBuilder.build())
|
||||||
|
.headers(Headers.of(request.headers))
|
||||||
|
when (request.verb) {
|
||||||
|
HTTP.Verb.GET -> requestBuilder.get()
|
||||||
|
HTTP.Verb.PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
|
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
|
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.useOnionRouting) {
|
||||||
|
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
||||||
|
.fail { e ->
|
||||||
|
Log.e("Loki", "FileServerV2 failed with error",e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Sending
|
||||||
|
fun upload(file: ByteArray): Promise<Long, Exception> {
|
||||||
|
val base64EncodedFile = Base64.encodeBytes(file)
|
||||||
|
val parameters = mapOf("file" to base64EncodedFile)
|
||||||
|
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
||||||
|
return send(request).map { json ->
|
||||||
|
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||||
|
return send(request).map { json ->
|
||||||
|
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
Base64.decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,16 +1,20 @@
|
|||||||
package org.session.libsession.messaging.jobs
|
package org.session.libsession.messaging.jobs
|
||||||
|
|
||||||
|
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.sending_receiving.attachments.AttachmentState
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
|
import org.session.libsession.utilities.DownloadUtilities
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long): Job {
|
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
|
||||||
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
|
||||||
@ -22,6 +26,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
override val maxFailureCount: Int = 20
|
override val maxFailureCount: Int = 20
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val KEY: String = "AttachmentDownloadJob"
|
val KEY: String = "AttachmentDownloadJob"
|
||||||
|
|
||||||
@ -46,18 +51,28 @@ 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) ?: return handleFailure(Error.NoAttachment)
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||||
|
?: return handleFailure(Error.NoAttachment)
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
val tempFile = createTempFile()
|
val tempFile = createTempFile()
|
||||||
|
|
||||||
FileServerAPI.shared.downloadFile(tempFile, attachment.url, null)
|
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
||||||
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
|
||||||
val stream = if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
|
|
||||||
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
|
||||||
|
|
||||||
|
val stream = if (openGroupV2 == 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
|
||||||
|
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
|
||||||
|
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||||
|
} else {
|
||||||
|
val url = HttpUrl.parse(attachment.url)!!
|
||||||
|
val fileId = url.pathSegments().last()
|
||||||
|
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||||
|
tempFile.writeBytes(it)
|
||||||
|
}
|
||||||
|
FileInputStream(tempFile)
|
||||||
|
}
|
||||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
||||||
|
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -94,8 +109,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
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(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import com.esotericsoftware.kryo.io.Output
|
|||||||
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.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
|
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.DotNetAPI
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
||||||
@ -46,9 +47,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
|
|
||||||
val usePadding = false
|
val usePadding = false
|
||||||
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
||||||
val server = if (openGroup != null) openGroup.server else FileServerAPI.shared.server
|
val server = openGroup?.let {
|
||||||
val shouldEncrypt = (openGroup == null) // Encrypt if this isn't an open group
|
it.server
|
||||||
|
} ?: openGroupV2?.let {
|
||||||
|
it.server
|
||||||
|
} ?: FileServerAPI.shared.server
|
||||||
|
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
|
||||||
@ -58,7 +64,11 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
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 = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
|
||||||
|
val dataBytes = attachmentData.data.readBytes()
|
||||||
|
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
||||||
|
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
||||||
|
}
|
||||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
if (e == Error.NoAttachment) {
|
if (e == Error.NoAttachment) {
|
||||||
|
@ -18,6 +18,7 @@ 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 dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
private val multiDispatcher = 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)
|
||||||
|
|
||||||
@ -28,8 +29,15 @@ class JobQueue : JobDelegate {
|
|||||||
scope.launch(dispatcher) {
|
scope.launch(dispatcher) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
queue.receive().let { job ->
|
queue.receive().let { job ->
|
||||||
job.delegate = this@JobQueue
|
if (job.canExecuteParallel()) {
|
||||||
job.execute()
|
launch(multiDispatcher) {
|
||||||
|
job.delegate = this@JobQueue
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job.delegate = this@JobQueue
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,6 +48,13 @@ class JobQueue : JobDelegate {
|
|||||||
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
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
package org.session.libsession.messaging.messages
|
package org.session.libsession.messaging.messages
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.threads.Address
|
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() {
|
||||||
@ -16,6 +21,9 @@ 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() {
|
||||||
|
internal constructor(): this("", "")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(address: Address): Destination {
|
fun from(address: Address): Destination {
|
||||||
@ -29,9 +37,13 @@ sealed class Destination {
|
|||||||
ClosedGroup(groupPublicKey)
|
ClosedGroup(groupPublicKey)
|
||||||
}
|
}
|
||||||
address.isOpenGroup -> {
|
address.isOpenGroup -> {
|
||||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)!!
|
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||||
OpenGroup(openGroup.channel, openGroup.server)
|
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) {
|
||||||
|
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server)
|
||||||
|
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server)
|
||||||
|
else -> throw Exception("Invalid OpenGroup $openGroup")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("TODO: Handle legacy closed groups.")
|
throw Exception("TODO: Handle legacy closed groups.")
|
||||||
|
@ -113,8 +113,11 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
}
|
}
|
||||||
if (groupRecord.isOpenGroup) {
|
if (groupRecord.isOpenGroup) {
|
||||||
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
|
||||||
val openGroup = storage.getOpenGroup(threadID) ?: continue
|
val openGroup = storage.getOpenGroup(threadID)
|
||||||
openGroups.add(openGroup.server)
|
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||||
|
|
||||||
|
val shareUrl = openGroup?.server ?: openGroupV2?.toJoinUrl() ?: continue
|
||||||
|
openGroups.add(shareUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,506 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import nl.komponents.kovenant.Kovenant
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.bind
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
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.utilities.AESGCM
|
||||||
|
import org.session.libsignal.service.loki.HTTP
|
||||||
|
import org.session.libsignal.service.loki.HTTP.Verb.*
|
||||||
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
import org.session.libsignal.utilities.Base64.*
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object OpenGroupAPIV2 {
|
||||||
|
|
||||||
|
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 const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
|
|
||||||
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
|
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DefaultGroup(val id: String,
|
||||||
|
val name: String,
|
||||||
|
val image: ByteArray?) {
|
||||||
|
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Info(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val imageID: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class CompactPollRequest(val roomId: String,
|
||||||
|
val authToken: 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)
|
||||||
|
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0,
|
||||||
|
val deletedMessageId: Long = 0
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = MessageDeletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Request(
|
||||||
|
val verb: HTTP.Verb,
|
||||||
|
val room: String?,
|
||||||
|
val server: String,
|
||||||
|
val endpoint: String,
|
||||||
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
|
val parameters: Any? = null,
|
||||||
|
val headers: Map<String, String> = mapOf(),
|
||||||
|
val isAuthRequired: Boolean = true,
|
||||||
|
// Always `true` under normal circumstances. You might want to disable
|
||||||
|
// this when running over Lokinet.
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
|
if (parameters == null) return null
|
||||||
|
|
||||||
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||||
|
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
|
||||||
|
val urlBuilder = HttpUrl.Builder()
|
||||||
|
.scheme(parsed.scheme())
|
||||||
|
.host(parsed.host())
|
||||||
|
.port(parsed.port())
|
||||||
|
.addPathSegments(request.endpoint)
|
||||||
|
|
||||||
|
if (request.verb == GET) {
|
||||||
|
for ((key, value) in request.queryParameters) {
|
||||||
|
urlBuilder.addQueryParameter(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||||
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
|
.url(urlBuilder.build())
|
||||||
|
.headers(Headers.of(request.headers))
|
||||||
|
if (request.isAuthRequired) {
|
||||||
|
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request")
|
||||||
|
requestBuilder.header("Authorization", token)
|
||||||
|
}
|
||||||
|
when (request.verb) {
|
||||||
|
GET -> requestBuilder.get()
|
||||||
|
PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
|
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
|
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.room.isNullOrEmpty()) {
|
||||||
|
requestBuilder.header("Room", request.room)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.useOnionRouting) {
|
||||||
|
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||||
|
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
|
||||||
|
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired)
|
||||||
|
.fail { e ->
|
||||||
|
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
if (request.room != null) {
|
||||||
|
storage.removeAuthToken("${request.server}.${request.room}")
|
||||||
|
} else {
|
||||||
|
storage.removeAuthToken(request.server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (request.isAuthRequired) {
|
||||||
|
getAuthToken(request.room!!, request.server).bind { execute(it) }
|
||||||
|
} else {
|
||||||
|
execute(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val result = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
decode(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
return storage.getAuthToken(room, server)?.let {
|
||||||
|
Promise.of(it)
|
||||||
|
} ?: run {
|
||||||
|
requestNewAuthToken(room, server)
|
||||||
|
.bind { claimAuthToken(it, room, server) }
|
||||||
|
.success { authToken ->
|
||||||
|
storage.setAuthToken(room, server, authToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
||||||
|
?: return Promise.ofFail(Error.GENERIC)
|
||||||
|
val queryParameters = mutableMapOf("public_key" to publicKey)
|
||||||
|
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||||
|
val base64EncodedCiphertext = challenge["ciphertext"] as? String
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val ciphertext = decode(base64EncodedCiphertext)
|
||||||
|
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||||
|
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||||
|
val tokenAsData = try {
|
||||||
|
AESGCM.decrypt(ciphertext, symmetricKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Error.DECRYPTION_FAILED
|
||||||
|
}
|
||||||
|
tokenAsData.toHexString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||||
|
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
||||||
|
val headers = mapOf("Authorization" to authToken)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
||||||
|
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||||
|
return send(request).map { authToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token")
|
||||||
|
return send(request).map {
|
||||||
|
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Sending
|
||||||
|
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||||
|
val base64EncodedFile = encodeBytes(file)
|
||||||
|
val parameters = mapOf("file" to base64EncodedFile)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
||||||
|
return send(request).map { json ->
|
||||||
|
json["result"] as? Long ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
||||||
|
return send(request).map { json ->
|
||||||
|
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
||||||
|
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
|
||||||
|
val jsonMessage = signedMessage.toJSON()
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
||||||
|
return send(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Messages
|
||||||
|
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
|
storage.getLastMessageServerId(room, server)?.let { lastId ->
|
||||||
|
queryParameters += "from_server_id" to lastId.toString()
|
||||||
|
}
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||||
|
return send(request).map { jsonList ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Message Deletion
|
||||||
|
@JvmStatic
|
||||||
|
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Deleted server message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDeletedMessages(room: String, server: String): Promise<List<MessageDeletion>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
|
storage.getLastDeletionServerId(room, server)?.let { last ->
|
||||||
|
queryParameters["from_server_id"] = last.toString()
|
||||||
|
}
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
|
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||||
|
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||||
|
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
||||||
|
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||||
|
if (serverID.id > lastMessageServerId) {
|
||||||
|
storage.setLastDeletionServerId(room, server, serverID.id)
|
||||||
|
}
|
||||||
|
serverIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Moderation
|
||||||
|
private fun handleModerators(serverRoomId: String, moderatorList: List<String>) {
|
||||||
|
moderators[serverRoomId] = moderatorList.toMutableSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||||
|
return send(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val id = "$server.$room"
|
||||||
|
handleModerators(id, moderatorsJson)
|
||||||
|
moderatorsJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val parameters = mapOf("public_key" to publicKey)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Banned user $publicKey from $server.$room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||||
|
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region General
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||||
|
val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val requests = rooms.mapNotNull { room ->
|
||||||
|
val authToken = try {
|
||||||
|
requestAuths[room]?.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Failed to get auth token for $room", e)
|
||||||
|
null
|
||||||
|
} ?: return@mapNotNull null
|
||||||
|
|
||||||
|
CompactPollRequest(roomId = room,
|
||||||
|
authToken = authToken,
|
||||||
|
fromDeletionServerId = storage.getLastDeletionServerId(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))
|
||||||
|
// build a request for all rooms
|
||||||
|
return send(request = request).map { json ->
|
||||||
|
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
|
||||||
|
|
||||||
|
results.mapNotNull { roomJson ->
|
||||||
|
if (roomJson !is Map<*,*>) return@mapNotNull null
|
||||||
|
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null
|
||||||
|
|
||||||
|
// check the status was fine
|
||||||
|
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null
|
||||||
|
if (statusCode == 401) {
|
||||||
|
// delete auth token and return null
|
||||||
|
storage.removeAuthToken(roomId, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check and store mods
|
||||||
|
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null
|
||||||
|
handleModerators("$server.$roomId", moderators)
|
||||||
|
|
||||||
|
// get deletions
|
||||||
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
|
val idsAsString = JsonUtil.toJson(roomJson["deletions"])
|
||||||
|
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||||
|
val lastDeletionServerId = storage.getLastDeletionServerId(roomId, server) ?: 0
|
||||||
|
val serverID = deletedServerIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
||||||
|
if (serverID.id > lastDeletionServerId) {
|
||||||
|
storage.setLastDeletionServerId(roomId, server, serverID.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get messages
|
||||||
|
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed
|
||||||
|
|
||||||
|
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
|
||||||
|
var currentMax = lastMessageServerId
|
||||||
|
val messages = rawMessages.mapNotNull { rawMessage ->
|
||||||
|
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
||||||
|
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
||||||
|
}
|
||||||
|
message
|
||||||
|
}
|
||||||
|
storage.setLastMessageServerId(roomId, server, currentMax)
|
||||||
|
roomId to CompactPollResult(
|
||||||
|
messages = messages,
|
||||||
|
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||||
|
moderators = moderators
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
||||||
|
return getAllRooms(DEFAULT_SERVER).map { groups ->
|
||||||
|
val earlyGroups = groups.map { group ->
|
||||||
|
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
|
||||||
|
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
||||||
|
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
||||||
|
defaultRooms.tryEmit(earlyGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val images = groups.map { group ->
|
||||||
|
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
groups.map { group ->
|
||||||
|
val image = try {
|
||||||
|
images[group.id]!!.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// no image or image failed to download
|
||||||
|
null
|
||||||
|
}
|
||||||
|
DefaultGroup(group.id, group.name, image)
|
||||||
|
}
|
||||||
|
}.success { new ->
|
||||||
|
defaultRooms.tryEmit(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
||||||
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||||
|
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
val imageID = rawRoom["image_id"] as? String
|
||||||
|
Info(id = id, name = name, imageID = imageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
||||||
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
|
||||||
|
rawRooms.mapNotNull {
|
||||||
|
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||||
|
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||||
|
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
||||||
|
val imageId = roomJson["image_id"] as? String
|
||||||
|
Info(id, name, imageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
||||||
|
return send(request).map { json ->
|
||||||
|
val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setUserCount(room, server, memberCount)
|
||||||
|
memberCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Base64.decode
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
|
data class OpenGroupMessageV2(
|
||||||
|
val serverID: Long? = null,
|
||||||
|
val sender: String?,
|
||||||
|
val sentTimestamp: Long,
|
||||||
|
// The serialized protobuf in base64 encoding
|
||||||
|
val base64EncodedData: String,
|
||||||
|
// When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||||
|
// a receiving user can verify that the message wasn't tampered with.
|
||||||
|
val base64EncodedSignature: String? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
|
||||||
|
fun fromJSON(json: Map<String, Any>): OpenGroupMessageV2? {
|
||||||
|
val base64EncodedData = json["data"] as? String ?: return null
|
||||||
|
val sentTimestamp = json["timestamp"] as? Long ?: return null
|
||||||
|
val serverID = json["server_id"] as? Int
|
||||||
|
val sender = json["public_key"] as? String
|
||||||
|
val base64EncodedSignature = json["signature"] as? String
|
||||||
|
return OpenGroupMessageV2(serverID = serverID?.toLong(),
|
||||||
|
sender = sender,
|
||||||
|
sentTimestamp = sentTimestamp,
|
||||||
|
base64EncodedData = base64EncodedData,
|
||||||
|
base64EncodedSignature = base64EncodedSignature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sign(): OpenGroupMessageV2? {
|
||||||
|
if (base64EncodedData.isEmpty()) return null
|
||||||
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||||
|
|
||||||
|
if (sender != publicKey) return null // only sign our own messages?
|
||||||
|
|
||||||
|
val signature = try {
|
||||||
|
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): Map<String, Any> {
|
||||||
|
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp)
|
||||||
|
serverID?.let { jsonMap["server_id"] = serverID }
|
||||||
|
sender?.let { jsonMap["public_key"] = sender }
|
||||||
|
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
||||||
|
return jsonMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes ->
|
||||||
|
SignalServiceProtos.Content.parseFrom(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class OpenGroupV2(
|
||||||
|
val server: String,
|
||||||
|
val room: String,
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val publicKey: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||||
|
server = server,
|
||||||
|
room = room,
|
||||||
|
id = "$server.$room",
|
||||||
|
name = name,
|
||||||
|
publicKey = publicKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun fromJson(jsonAsString: String): OpenGroupV2? {
|
||||||
|
return try {
|
||||||
|
val json = JsonUtil.fromJson(jsonAsString)
|
||||||
|
if (!json.has("room")) return null
|
||||||
|
|
||||||
|
val room = json.get("room").asText().toLowerCase(Locale.getDefault())
|
||||||
|
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
|
||||||
|
val displayName = json.get("displayName").asText()
|
||||||
|
val publicKey = json.get("publicKey").asText()
|
||||||
|
|
||||||
|
OpenGroupV2(server, room, displayName, publicKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJoinUrl(): String = "$server/$room?public_key=$publicKey"
|
||||||
|
|
||||||
|
fun toJson(): Map<String,String> = mapOf(
|
||||||
|
"room" to room,
|
||||||
|
"server" to server,
|
||||||
|
"displayName" to name,
|
||||||
|
"publicKey" to publicKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -7,7 +7,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
|
|
||||||
object MessageReceiver {
|
object MessageReceiver {
|
||||||
|
|
||||||
@ -37,6 +36,7 @@ object MessageReceiver {
|
|||||||
is UnknownEnvelopeType -> false
|
is UnknownEnvelopeType -> false
|
||||||
is InvalidSignature -> false
|
is InvalidSignature -> false
|
||||||
is NoData -> false
|
is NoData -> false
|
||||||
|
is NoThread -> false
|
||||||
is SenderBlocked -> false
|
is SenderBlocked -> false
|
||||||
is SelfSend -> false
|
is SelfSend -> false
|
||||||
else -> true
|
else -> true
|
||||||
@ -122,7 +122,6 @@ object MessageReceiver {
|
|||||||
message.recipient = userPublicKey
|
message.recipient = userPublicKey
|
||||||
message.sentTimestamp = envelope.timestamp
|
message.sentTimestamp = envelope.timestamp
|
||||||
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
||||||
Log.d("Loki", "time: ${envelope.timestamp}, sent: ${envelope.serverTimestamp}")
|
|
||||||
message.groupPublicKey = groupPublicKey
|
message.groupPublicKey = groupPublicKey
|
||||||
message.openGroupServerMessageID = openGroupServerID
|
message.openGroupServerMessageID = openGroupServerID
|
||||||
// Validate
|
// Validate
|
||||||
|
@ -12,14 +12,15 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
|
|||||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||||
import org.session.libsession.messaging.messages.visible.*
|
import org.session.libsession.messaging.messages.visible.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
|
||||||
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.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.snode.RawResponsePromise
|
import org.session.libsession.snode.RawResponsePromise
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.snode.SnodeModule
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
@ -62,7 +63,7 @@ object MessageSender {
|
|||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||||
if (destination is Destination.OpenGroup) {
|
if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) {
|
||||||
return sendToOpenGroupDestination(destination, message)
|
return sendToOpenGroupDestination(destination, message)
|
||||||
}
|
}
|
||||||
return sendToSnodeDestination(destination, message)
|
return sendToSnodeDestination(destination, message)
|
||||||
@ -90,7 +91,8 @@ object MessageSender {
|
|||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> message.recipient = destination.publicKey
|
is Destination.Contact -> message.recipient = destination.publicKey
|
||||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -109,9 +111,9 @@ object MessageSender {
|
|||||||
if (message is VisibleMessage) {
|
if (message is VisibleMessage) {
|
||||||
val displayName = storage.getUserDisplayName()!!
|
val displayName = storage.getUserDisplayName()!!
|
||||||
val profileKey = storage.getUserProfileKey()
|
val profileKey = storage.getUserProfileKey()
|
||||||
val profilePrictureUrl = storage.getUserProfilePictureURL()
|
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||||
if (profileKey != null && profilePrictureUrl != null) {
|
if (profileKey != null && profilePictureUrl != null) {
|
||||||
message.profile = Profile(displayName, profileKey, profilePrictureUrl)
|
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||||
} else {
|
} else {
|
||||||
message.profile = Profile(displayName)
|
message.profile = Profile(displayName)
|
||||||
}
|
}
|
||||||
@ -128,7 +130,8 @@ object MessageSender {
|
|||||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -142,7 +145,8 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -150,6 +154,7 @@ object MessageSender {
|
|||||||
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||||
|
// Send the result
|
||||||
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
|
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
|
||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeModule.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||||
@ -204,32 +209,71 @@ object MessageSender {
|
|||||||
deferred.reject(error)
|
deferred.reject(error)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val server: String
|
|
||||||
val channel: Long
|
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
||||||
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
||||||
is Destination.OpenGroup -> {
|
is Destination.OpenGroup -> {
|
||||||
message.recipient = "${destination.server}.${destination.channel}"
|
message.recipient = "${destination.server}.${destination.channel}"
|
||||||
server = destination.server
|
val server = destination.server
|
||||||
channel = destination.channel
|
val channel = destination.channel
|
||||||
|
|
||||||
|
// Validate the message
|
||||||
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
|
throw Error.InvalidMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the message to an open group message
|
||||||
|
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
||||||
|
throw Error.InvalidMessage
|
||||||
|
}
|
||||||
|
// Send the result
|
||||||
|
OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success {
|
||||||
|
message.openGroupServerMessageID = it.serverID
|
||||||
|
handleSuccessfulMessageSend(message, destination)
|
||||||
|
deferred.resolve(Unit)
|
||||||
|
}.fail {
|
||||||
|
handleFailure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Destination.OpenGroupV2 -> {
|
||||||
|
message.recipient = "${destination.server}.${destination.room}"
|
||||||
|
val server = destination.server
|
||||||
|
val room = destination.room
|
||||||
|
|
||||||
|
// Attach the user's profile if needed
|
||||||
|
if (message is VisibleMessage) {
|
||||||
|
val displayName = storage.getUserDisplayName()!!
|
||||||
|
val profileKey = storage.getUserProfileKey()
|
||||||
|
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||||
|
if (profileKey != null && profilePictureUrl != null) {
|
||||||
|
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||||
|
} else {
|
||||||
|
message.profile = Profile(displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the message
|
||||||
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
|
throw Error.InvalidMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
val proto = message.toProto()!!
|
||||||
|
|
||||||
|
val openGroupMessage = OpenGroupMessageV2(
|
||||||
|
sender = message.sender,
|
||||||
|
sentTimestamp = message.sentTimestamp!!,
|
||||||
|
base64EncodedData = Base64.encodeBytes(proto.toByteArray()),
|
||||||
|
)
|
||||||
|
|
||||||
|
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||||
|
message.openGroupServerMessageID = it.serverID
|
||||||
|
handleSuccessfulMessageSend(message, destination)
|
||||||
|
deferred.resolve(Unit)
|
||||||
|
}.fail {
|
||||||
|
handleFailure(it)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Validate the message
|
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
|
||||||
throw Error.InvalidMessage
|
|
||||||
}
|
|
||||||
// Convert the message to an open group message
|
|
||||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run {
|
|
||||||
throw Error.InvalidMessage
|
|
||||||
}
|
|
||||||
// Send the result
|
|
||||||
OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success {
|
|
||||||
message.openGroupServerMessageID = it.serverID
|
|
||||||
handleSuccessfulMessageSend(message, destination)
|
|
||||||
deferred.resolve(Unit)
|
|
||||||
}.fail {
|
|
||||||
handleFailure(it)
|
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
handleFailure(exception)
|
handleFailure(exception)
|
||||||
@ -245,8 +289,12 @@ object MessageSender {
|
|||||||
// Ignore future self-sends
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
if (message.openGroupServerMessageID != null) {
|
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
||||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||||
|
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||||
|
if (threadID != null && threadID >= 0) {
|
||||||
|
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Mark the message as sent
|
// Mark the message as sent
|
||||||
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@ -128,8 +129,9 @@ 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() }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups) {
|
||||||
if (allOpenGroups.contains(openGroup)) continue
|
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||||
storage.addOpenGroup(openGroup, 1)
|
storage.addOpenGroup(openGroup, 1)
|
||||||
}
|
}
|
||||||
if (message.displayName.isNotEmpty()) {
|
if (message.displayName.isNotEmpty()) {
|
||||||
@ -164,16 +166,27 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
|
|
||||||
|
// Get or create thread
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||||
|
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||||
|
|
||||||
|
val openGroup = threadID.let {
|
||||||
|
storage.getOpenGroup(it.toString())
|
||||||
|
}
|
||||||
|
|
||||||
// Update profile if needed
|
// Update profile if needed
|
||||||
val newProfile = message.profile
|
val newProfile = message.profile
|
||||||
if (newProfile != null && openGroupID.isNullOrEmpty() && userPublicKey != message.sender) {
|
|
||||||
|
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||||
val displayName = newProfile.displayName!!
|
val displayName = newProfile.displayName!!
|
||||||
if (displayName.isNotEmpty()) {
|
if (displayName.isNotEmpty()) {
|
||||||
profileManager.setDisplayName(context, recipient, displayName)
|
profileManager.setDisplayName(context, recipient, displayName)
|
||||||
}
|
}
|
||||||
if (newProfile.profileKey?.isNotEmpty() == true && !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey)) {
|
if (newProfile.profileKey?.isNotEmpty() == true
|
||||||
|
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) {
|
||||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
||||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||||
val newUrl = newProfile.profilePictureURL
|
val newUrl = newProfile.profilePictureURL
|
||||||
@ -185,9 +198,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get or create thread
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
|
||||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
|
||||||
// Parse quote if needed
|
// Parse quote if needed
|
||||||
var quoteModel: QuoteModel? = null
|
var quoteModel: QuoteModel? = null
|
||||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||||
@ -228,7 +238,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
// Parse stickers if needed
|
// Parse stickers if needed
|
||||||
// Persist the message
|
// Persist the message
|
||||||
message.threadID = threadID
|
message.threadID = threadID
|
||||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.NoThread
|
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
||||||
// Parse & persist attachments
|
// Parse & persist attachments
|
||||||
// Start attachment downloads if needed
|
// Start attachment downloads if needed
|
||||||
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
||||||
@ -237,6 +247,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
JobQueue.shared.add(downloadJob)
|
JobQueue.shared.add(downloadJob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val openGroupServerID = message.openGroupServerMessageID
|
||||||
|
if (openGroupServerID != null) {
|
||||||
|
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !(message.isMediaMessage() || attachments.isNotEmpty()))
|
||||||
|
}
|
||||||
// Cancel any typing indicators if needed
|
// Cancel any typing indicators if needed
|
||||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||||
//Notify the user if needed
|
//Notify the user if needed
|
||||||
|
@ -69,9 +69,6 @@ class ClosedGroupPoller {
|
|||||||
// ignore inactive group's messages
|
// ignore inactive group's messages
|
||||||
return@successBackground
|
return@successBackground
|
||||||
}
|
}
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
|
||||||
}
|
|
||||||
messages.forEach { envelope ->
|
messages.forEach { envelope ->
|
||||||
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
||||||
JobQueue.shared.add(job)
|
JobQueue.shared.add(job)
|
||||||
|
@ -9,6 +9,8 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
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
|
||||||
import org.session.libsignal.utilities.successBackground
|
import org.session.libsignal.utilities.successBackground
|
||||||
@ -72,7 +74,6 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ
|
|||||||
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
||||||
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
||||||
// Process messages in the background
|
// Process messages in the background
|
||||||
Log.d("Loki", "received ${messages.size} messages")
|
|
||||||
messages.forEach { message ->
|
messages.forEach { message ->
|
||||||
try {
|
try {
|
||||||
val senderPublicKey = message.senderPublicKey
|
val senderPublicKey = message.senderPublicKey
|
||||||
@ -211,10 +212,13 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pollForDeletedMessages() {
|
private fun pollForDeletedMessages() {
|
||||||
|
val messagingModule = MessagingModuleConfiguration.shared
|
||||||
|
val address = GroupUtil.getEncodedOpenGroupID(openGroup.id.toByteArray())
|
||||||
|
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||||
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getMessageID(it) }
|
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { messagingModule.messageDataProvider.getMessageID(it, threadId) }
|
||||||
deletedMessageIDs.forEach {
|
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(it)
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||||
}
|
}
|
||||||
}.fail {
|
}.fail {
|
||||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
package org.session.libsession.messaging.sending_receiving.pollers
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val executorService: ScheduledExecutorService? = null) {
|
||||||
|
|
||||||
|
private var hasStarted = false
|
||||||
|
@Volatile private var isPollOngoing = false
|
||||||
|
var isCaughtUp = false
|
||||||
|
|
||||||
|
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||||
|
|
||||||
|
// use this as a receive time-based window to calculate re-poll interval
|
||||||
|
private val receivedQueue = ArrayDeque<Long>(50)
|
||||||
|
|
||||||
|
private fun calculatePollInterval(): Long {
|
||||||
|
// sample last default poll time * 2
|
||||||
|
while (receivedQueue.size > 50) {
|
||||||
|
receivedQueue.removeLast()
|
||||||
|
}
|
||||||
|
val sampleWindow = System.currentTimeMillis() - pollForNewMessagesInterval * 2
|
||||||
|
val numberInSample = receivedQueue.toList().filter { it > sampleWindow }.size.coerceAtLeast(1)
|
||||||
|
return ((2 + (50 / numberInSample / 20)*5) * 1000).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Settings
|
||||||
|
companion object {
|
||||||
|
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
fun startIfNeeded() {
|
||||||
|
if (hasStarted || executorService == null) return
|
||||||
|
cancellableFutures += executorService.schedule(::compactPoll, 0, TimeUnit.MILLISECONDS)
|
||||||
|
hasStarted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
cancellableFutures.forEach { future ->
|
||||||
|
future.cancel(false)
|
||||||
|
}
|
||||||
|
cancellableFutures.clear()
|
||||||
|
hasStarted = false
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Polling
|
||||||
|
|
||||||
|
private fun compactPoll(): Promise<Any, Exception> {
|
||||||
|
return compactPoll(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compactPoll(isBackgroundPoll: Boolean): Promise<Any, Exception> {
|
||||||
|
if (isPollOngoing || !hasStarted) return Promise.of(Unit)
|
||||||
|
isPollOngoing = true
|
||||||
|
val server = openGroups.first().server // assume all the same server
|
||||||
|
val rooms = openGroups.map { it.room }
|
||||||
|
return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results ->
|
||||||
|
results.forEach { (room, results) ->
|
||||||
|
val serverRoomId = "$server.$room"
|
||||||
|
handleDeletedMessages(serverRoomId,results.deletions)
|
||||||
|
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
||||||
|
}
|
||||||
|
}.always {
|
||||||
|
isPollOngoing = false
|
||||||
|
if (!isBackgroundPoll) {
|
||||||
|
val delay = calculatePollInterval()
|
||||||
|
executorService?.schedule(this@OpenGroupV2Poller::compactPoll, delay, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNewMessages(serverRoomId: String, newMessages: List<OpenGroupMessageV2>, isBackgroundPoll: Boolean) {
|
||||||
|
if (!hasStarted) return
|
||||||
|
newMessages.forEach { message ->
|
||||||
|
try {
|
||||||
|
val senderPublicKey = message.sender!!
|
||||||
|
// Main message
|
||||||
|
// Envelope
|
||||||
|
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||||
|
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||||
|
builder.source = senderPublicKey
|
||||||
|
builder.sourceDevice = 1
|
||||||
|
builder.content = message.toProto().toByteString()
|
||||||
|
builder.timestamp = message.sentTimestamp
|
||||||
|
val envelope = builder.build()
|
||||||
|
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, message.serverID, serverRoomId)
|
||||||
|
Log.d("Loki", "Scheduling Job $job")
|
||||||
|
if (isBackgroundPoll) {
|
||||||
|
job.executeAsync()
|
||||||
|
// The promise is just used to keep track of when we're done
|
||||||
|
} else {
|
||||||
|
JobQueue.shared.add(job)
|
||||||
|
}
|
||||||
|
receivedQueue.addFirst(message.sentTimestamp)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Exception parsing message", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeletedMessages(serverRoomId: String, deletedMessageServerIDs: List<Long>) {
|
||||||
|
val messagingModule = MessagingModuleConfiguration.shared
|
||||||
|
val address = GroupUtil.getEncodedOpenGroupID(serverRoomId.toByteArray())
|
||||||
|
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||||
|
|
||||||
|
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
|
||||||
|
messagingModule.messageDataProvider.getMessageID(serverId, threadId)
|
||||||
|
}
|
||||||
|
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||||
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
@ -10,7 +10,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.DiffieHellman
|
import org.session.libsignal.utilities.DiffieHellman
|
||||||
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||||
@ -27,7 +26,7 @@ import org.session.libsignal.service.loki.HTTP
|
|||||||
import org.session.libsignal.service.loki.utilities.*
|
import org.session.libsignal.service.loki.utilities.*
|
||||||
import org.session.libsignal.utilities.*
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@ -179,80 +178,9 @@ open class DotNetAPI {
|
|||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOWNLOAD
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks the calling thread.
|
|
||||||
*/
|
|
||||||
fun downloadFile(destination: File, url: String, listener: SignalServiceAttachment.ProgressListener?) {
|
|
||||||
val outputStream = FileOutputStream(destination) // Throws
|
|
||||||
var remainingAttempts = 4
|
|
||||||
var exception: Exception? = null
|
|
||||||
while (remainingAttempts > 0) {
|
|
||||||
remainingAttempts -= 1
|
|
||||||
try {
|
|
||||||
downloadFile(outputStream, url, listener)
|
|
||||||
exception = null
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
exception = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exception != null) { throw exception }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks the calling thread.
|
|
||||||
*/
|
|
||||||
fun downloadFile(outputStream: OutputStream, url: String, listener: SignalServiceAttachment.ProgressListener?) {
|
|
||||||
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
|
||||||
// because the underlying Signal logic requires these to work correctly
|
|
||||||
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
|
||||||
var newPrefixedHost = oldPrefixedHost
|
|
||||||
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
|
||||||
newPrefixedHost = FileServerAPI.shared.server
|
|
||||||
}
|
|
||||||
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
|
||||||
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
|
||||||
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
|
||||||
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
|
||||||
val request = Request.Builder().url(sanitizedURL).get()
|
|
||||||
try {
|
|
||||||
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
|
||||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
|
||||||
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
|
||||||
val result = json["result"] as? String
|
|
||||||
if (result == null) {
|
|
||||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
|
||||||
throw PushNetworkException("Missing response body.")
|
|
||||||
}
|
|
||||||
val body = Base64.decode(result)
|
|
||||||
if (body.size > FileServerAPI.maxFileSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
val input = body.inputStream()
|
|
||||||
val buffer = ByteArray(32768)
|
|
||||||
var count = 0
|
|
||||||
var bytes = input.read(buffer)
|
|
||||||
while (bytes >= 0) {
|
|
||||||
outputStream.write(buffer, 0, bytes)
|
|
||||||
count += bytes
|
|
||||||
if (count > FileServerAPI.maxFileSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
|
||||||
bytes = input.read(buffer)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't download attachment due to error: $e.")
|
|
||||||
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPLOAD
|
// UPLOAD
|
||||||
|
|
||||||
|
// TODO: migrate to v2 file server
|
||||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||||
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
||||||
// This function mimics what Signal does in PushServiceSocket
|
// This function mimics what Signal does in PushServiceSocket
|
||||||
@ -269,13 +197,13 @@ open class DotNetAPI {
|
|||||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
||||||
val data = json["data"] as? Map<*, *>
|
val data = json["data"] as? Map<*, *>
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
Log.e("Loki", "Couldn't parse attachment from: $json.")
|
||||||
throw Error.ParsingFailed
|
throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||||
val url = data["url"] as? String
|
val url = data["url"] as? String
|
||||||
if (id == null || url == null || url.isEmpty()) {
|
if (id == null || url == null || url.isEmpty()) {
|
||||||
Log.d("Loki", "Couldn't parse upload from: $json.")
|
Log.e("Loki", "Couldn't parse upload from: $json.")
|
||||||
throw Error.ParsingFailed
|
throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
UploadResult(id, url, file.transmittedDigest)
|
UploadResult(id, url, file.transmittedDigest)
|
||||||
|
@ -14,7 +14,6 @@ import org.session.libsignal.utilities.*
|
|||||||
import org.session.libsignal.service.loki.Snode
|
import org.session.libsignal.service.loki.Snode
|
||||||
import org.session.libsignal.service.loki.*
|
import org.session.libsignal.service.loki.*
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
|
||||||
import org.session.libsession.utilities.getBodyForOnionRequest
|
import org.session.libsession.utilities.getBodyForOnionRequest
|
||||||
import org.session.libsession.utilities.getHeadersForOnionRequest
|
import org.session.libsession.utilities.getHeadersForOnionRequest
|
||||||
import org.session.libsignal.service.loki.Broadcaster
|
import org.session.libsignal.service.loki.Broadcaster
|
||||||
@ -82,7 +81,7 @@ object OnionRequestAPI {
|
|||||||
|
|
||||||
internal sealed class Destination {
|
internal sealed class Destination {
|
||||||
class Snode(val snode: org.session.libsignal.service.loki.Snode) : Destination()
|
class Snode(val snode: org.session.libsignal.service.loki.Snode) : Destination()
|
||||||
class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination()
|
class Server(val host: String, val target: String, val x25519PublicKey: String, val scheme: String, val port: Int) : Destination()
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Private API
|
// region Private API
|
||||||
@ -330,7 +329,7 @@ object OnionRequestAPI {
|
|||||||
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
||||||
try {
|
try {
|
||||||
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
||||||
val statusCode = json["status"] as Int
|
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
|
||||||
if (statusCode == 406) {
|
if (statusCode == 406) {
|
||||||
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
||||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
||||||
@ -454,7 +453,7 @@ object OnionRequestAPI {
|
|||||||
val urlAsString = url.toString()
|
val urlAsString = url.toString()
|
||||||
val host = url.host()
|
val host = url.host()
|
||||||
val endpoint = when {
|
val endpoint = when {
|
||||||
server.count() < urlAsString.count() -> urlAsString.substringAfter("$server/")
|
server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/")
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val body = request.getBodyForOnionRequest() ?: "null"
|
val body = request.getBodyForOnionRequest() ?: "null"
|
||||||
@ -464,7 +463,8 @@ object OnionRequestAPI {
|
|||||||
"method" to request.method(),
|
"method" to request.method(),
|
||||||
"headers" to headers
|
"headers" to headers
|
||||||
)
|
)
|
||||||
val destination = Destination.Server(host, target, x25519PublicKey)
|
url.isHttps
|
||||||
|
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.")
|
||||||
throw exception
|
throw exception
|
||||||
|
@ -70,7 +70,13 @@ object OnionRequestEncryption {
|
|||||||
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
||||||
}
|
}
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
is OnionRequestAPI.Destination.Server -> {
|
||||||
payload = mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST" )
|
payload = mutableMapOf(
|
||||||
|
"host" to rhs.host,
|
||||||
|
"target" to rhs.target,
|
||||||
|
"method" to "POST",
|
||||||
|
"protocol" to rhs.scheme,
|
||||||
|
"port" to rhs.port
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
||||||
|
@ -33,7 +33,7 @@ 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 = 2
|
||||||
// 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
|
||||||
@ -41,7 +41,7 @@ object SnodeAPI {
|
|||||||
if (useTestnet) {
|
if (useTestnet) {
|
||||||
setOf( "http://public.loki.foundation:38157" )
|
setOf( "http://public.loki.foundation:38157" )
|
||||||
} else {
|
} else {
|
||||||
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 = 4
|
||||||
@ -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 )
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -16,10 +16,10 @@ data class SnodeMessage(
|
|||||||
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 ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import org.whispersystems.curve25519.Curve25519
|
import androidx.annotation.WorkerThread
|
||||||
import org.session.libsignal.libsignal.util.ByteUtil
|
import org.session.libsignal.libsignal.util.ByteUtil
|
||||||
import org.session.libsignal.service.internal.util.Util
|
import org.session.libsignal.service.internal.util.Util
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
internal object AESGCM {
|
internal object AESGCM {
|
||||||
|
|
||||||
internal data class EncryptionResult(
|
internal data class EncryptionResult(
|
||||||
@ -31,6 +33,16 @@ internal object AESGCM {
|
|||||||
return cipher.doFinal(ciphertext)
|
return cipher.doFinal(ciphertext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync. Don't call from the main thread.
|
||||||
|
*/
|
||||||
|
internal fun generateSymmetricKey(x25519PublicKey: ByteArray, x25519PrivateKey: ByteArray): ByteArray {
|
||||||
|
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, x25519PrivateKey)
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
||||||
|
return mac.doFinal(ephemeralSharedSecret)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync. Don't call from the main thread.
|
* Sync. Don't call from the main thread.
|
||||||
*/
|
*/
|
||||||
@ -47,10 +59,7 @@ internal object AESGCM {
|
|||||||
internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
|
internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
|
||||||
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
|
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
|
||||||
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
|
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
|
||||||
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey)
|
val symmetricKey = generateSymmetricKey(x25519PublicKey, ephemeralKeyPair.privateKey)
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
|
||||||
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
|
||||||
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
|
|
||||||
val ciphertext = encrypt(plaintext, symmetricKey)
|
val ciphertext = encrypt(plaintext, symmetricKey)
|
||||||
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
|
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ package org.session.libsession.utilities
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
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.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||||
@ -39,50 +41,64 @@ object DownloadUtilities {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
||||||
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
|
||||||
// because the underlying Signal logic requires these to work correctly
|
if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) {
|
||||||
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
val httpUrl = HttpUrl.parse(url)!!
|
||||||
var newPrefixedHost = oldPrefixedHost
|
val fileId = httpUrl.pathSegments().last()
|
||||||
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
try {
|
||||||
newPrefixedHost = FileServerAPI.shared.server
|
FileServerAPIV2.download(fileId.toLong()).get().let {
|
||||||
}
|
outputStream.write(it)
|
||||||
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
|
||||||
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
|
||||||
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
|
||||||
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
|
||||||
val request = Request.Builder().url(sanitizedURL).get()
|
|
||||||
try {
|
|
||||||
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
|
||||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
|
||||||
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
|
||||||
val result = json["result"] as? String
|
|
||||||
if (result == null) {
|
|
||||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
|
||||||
throw PushNetworkException("Missing response body.")
|
|
||||||
}
|
|
||||||
val body = Base64.decode(result)
|
|
||||||
if (body.size > maxSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
body.inputStream().use { input ->
|
|
||||||
val buffer = ByteArray(32768)
|
|
||||||
var count = 0
|
|
||||||
var bytes = input.read(buffer)
|
|
||||||
while (bytes >= 0) {
|
|
||||||
outputStream.write(buffer, 0, bytes)
|
|
||||||
count += bytes
|
|
||||||
if (count > maxSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
|
||||||
bytes = input.read(buffer)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couln't download attachment due to error",e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
||||||
|
// because the underlying Signal logic requires these to work correctly
|
||||||
|
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
||||||
|
var newPrefixedHost = oldPrefixedHost
|
||||||
|
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
||||||
|
newPrefixedHost = FileServerAPI.shared.server
|
||||||
|
}
|
||||||
|
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
||||||
|
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
||||||
|
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
||||||
|
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
||||||
|
val request = Request.Builder().url(sanitizedURL).get()
|
||||||
|
try {
|
||||||
|
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
||||||
|
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
||||||
|
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
||||||
|
val result = json["result"] as? String
|
||||||
|
if (result == null) {
|
||||||
|
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
||||||
|
throw PushNetworkException("Missing response body.")
|
||||||
|
}
|
||||||
|
val body = Base64.decode(result)
|
||||||
|
if (body.size > maxSize) {
|
||||||
|
Log.d("Loki", "Attachment size limit exceeded.")
|
||||||
|
throw PushNetworkException("Max response size exceeded.")
|
||||||
|
}
|
||||||
|
body.inputStream().use { input ->
|
||||||
|
val buffer = ByteArray(32768)
|
||||||
|
var count = 0
|
||||||
|
var bytes = input.read(buffer)
|
||||||
|
while (bytes >= 0) {
|
||||||
|
outputStream.write(buffer, 0, bytes)
|
||||||
|
count += bytes
|
||||||
|
if (count > maxSize) {
|
||||||
|
Log.d("Loki", "Attachment size limit exceeded.")
|
||||||
|
throw PushNetworkException("Max response size exceeded.")
|
||||||
|
}
|
||||||
|
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
||||||
|
bytes = input.read(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't download attachment due to error", e)
|
||||||
|
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't download attachment due to error: $e.")
|
|
||||||
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,6 +24,11 @@ interface LokiAPIDatabaseProtocol {
|
|||||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
fun getLastDeletionServerID(group: Long, server: String): Long?
|
||||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||||
|
fun getLastMessageServerID(room: String, server: String): Long?
|
||||||
|
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
||||||
|
fun getLastDeletionServerID(room: String, server: String): Long?
|
||||||
|
fun setLastDeletionServerID(room: String, server: String, newValue: Long)
|
||||||
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
||||||
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
||||||
|
@ -3,5 +3,5 @@ package org.session.libsignal.service.loki
|
|||||||
interface LokiMessageDatabaseProtocol {
|
interface LokiMessageDatabaseProtocol {
|
||||||
|
|
||||||
fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long?
|
fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long?
|
||||||
fun setServerID(messageID: Long, serverID: Long)
|
fun setServerID(messageID: Long, serverID: Long, isSms: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ package org.session.libsignal.utilities;
|
|||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.ResolvedType;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
@ -42,6 +44,10 @@ public class JsonUtil {
|
|||||||
return objectMapper.readValue(serialized, clazz);
|
return objectMapper.readValue(serialized, clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static<T> T fromJson(String serialized, JavaType clazz) throws IOException {
|
||||||
|
return objectMapper.readValue(serialized, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
||||||
return objectMapper.readValue(serialized, clazz);
|
return objectMapper.readValue(serialized, clazz);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user