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

This commit is contained in:
Brice-W 2021-05-07 09:34:24 +10:00
commit d98cef3c77
60 changed files with 2055 additions and 379 deletions

View File

@ -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
----------------- -----------------

View File

@ -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

View File

@ -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,

View File

@ -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;
} }

View File

@ -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? {

View File

@ -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 {

View File

@ -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");
} }

View File

@ -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);

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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();

View File

@ -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)
} }

View File

@ -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

View File

@ -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()

View File

@ -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()
} }
} }
} }

View File

@ -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()
} }

View File

@ -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 ->

View File

@ -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()))
} }
} }

View File

@ -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()))
} }
} }

View File

@ -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 ->

View File

@ -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)

View File

@ -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 {

View File

@ -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 = "")
} }

View File

@ -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()
}

View File

@ -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>()
}

View File

@ -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);
} }

View File

@ -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);

View File

@ -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"

View 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" />

View File

@ -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"

View 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"/>

View File

@ -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>

View File

@ -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?

View File

@ -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?
} }

View File

@ -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
}
}
}

View File

@ -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))
} }

View File

@ -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) {

View File

@ -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

View File

@ -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.")

View File

@ -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)
} }
} }

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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,
)
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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}.")

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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 )
) )
) )

View File

@ -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 ""
) )
} }
} }

View File

@ -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)
} }

View File

@ -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)
} }
} }
} }

View File

@ -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?

View File

@ -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)
} }

View File

@ -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);
} }