Merge branch 'dev'

This commit is contained in:
Niels Andriesse 2021-05-06 16:06:04 +10:00
commit 2f23a0e59f
231 changed files with 4009 additions and 17827 deletions

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

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms
import android.util.Log
import nl.komponents.kovenant.Kovenant
import nl.komponents.kovenant.jvm.asDispatcher
import org.session.libsignal.utilities.ThreadUtils
import java.util.concurrent.Executors
object AppContext {
fun configureKovenant() {
Kovenant.context {
callbackContext.dispatcher = Executors.newSingleThreadExecutor().asDispatcher()
workerContext.dispatcher = ThreadUtils.executorPool.asDispatcher()
multipleCompletion = { v1, v2 ->
Log.d("Loki", "Promise resolved more than once (first with $v1, then with $v2); ignoring $v2.")
}
}
}
}

View File

@ -30,15 +30,17 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.session.libsession.messaging.MessagingConfiguration; import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.avatars.AvatarHelper; import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.messaging.file_server.FileServerAPI;
import org.session.libsession.messaging.jobs.JobQueue; import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.session.libsession.messaging.mentions.MentionsManager;
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
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.Poller; import org.session.libsession.messaging.sending_receiving.pollers.Poller;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.snode.SnodeConfiguration; import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.IdentityKeyUtil; import org.session.libsession.utilities.IdentityKeyUtil;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
@ -47,12 +49,7 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWr
import org.session.libsession.utilities.dynamiclanguage.LocaleParser; import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.api.util.StreamDetails;
import org.session.libsignal.service.loki.api.PushNotificationAPI; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol;
import org.session.libsignal.service.loki.api.SnodeAPI;
import org.session.libsignal.service.loki.api.SwarmAPI;
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
@ -106,6 +103,7 @@ import dagger.ObjectGraph;
import kotlin.Unit; import kotlin.Unit;
import kotlinx.coroutines.Job; import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import nl.komponents.kovenant.Kovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant; import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
@ -166,6 +164,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
ProcessLifecycleOwner.get().getLifecycle().addObserver(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
// Loki // Loki
// ======== // ========
AppContext.INSTANCE.configureKovenant();
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier()); messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
broadcaster = new Broadcaster(this); broadcaster = new Broadcaster(this);
threadNotificationHandler = new Handler(Looper.getMainLooper()); threadNotificationHandler = new Handler(Looper.getMainLooper());
@ -173,17 +172,14 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this); LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this); LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
MessagingConfiguration.Companion.configure(this, MessagingModuleConfiguration.Companion.configure(this,
DatabaseFactory.getStorage(this), DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this), DatabaseFactory.getAttachmentProvider(this),
new SessionProtocolImpl(this)); new SessionProtocolImpl(this));
SnodeConfiguration.Companion.configure(apiDB, broadcaster); SnodeModule.Companion.configure(apiDB, broadcaster);
if (userPublicKey != null) { if (userPublicKey != null) {
SwarmAPI.Companion.configureIfNeeded(apiDB); MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
} }
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
setUpStorageAPIIfNeeded(); setUpStorageAPIIfNeeded();
resubmitProfilePictureIfNeeded(); resubmitProfilePictureIfNeeded();
publicChatManager = new PublicChatManager(this); publicChatManager = new PublicChatManager(this);
@ -428,7 +424,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this); LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB); FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
return true; return true;
} }
@ -458,13 +453,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
String userPublicKey = TextSecurePreferences.getLocalNumber(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return; if (userPublicKey == null) return;
if (poller != null) { if (poller != null) {
SnodeAPI.shared.setUserPublicKey(userPublicKey);
poller.setUserPublicKey(userPublicKey); poller.setUserPublicKey(userPublicKey);
return; return;
} }
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
poller = new Poller(); poller = new Poller();
closedGroupPoller = new ClosedGroupPoller(); closedGroupPoller = new ClosedGroupPoller();
} }

View File

@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
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;

View File

@ -52,6 +52,8 @@ import androidx.viewpager.widget.ViewPager;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager; import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.database.MediaDatabase;
@ -351,6 +353,12 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR, saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()])); attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
actionMode.finish(); actionMode.finish();
// Sending a Data extraction notification (for incoming attachments only)
boolean containsIncoming = mediaRecords.parallelStream().anyMatch(m -> !m.isOutgoing());
if (containsIncoming) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
} }
}.execute(); }.execute();
}) })
@ -358,6 +366,16 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
}, mediaRecords.size()); }, mediaRecords.size());
} }
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (recipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, recipient.getAddress());
}
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) { private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
int recordCount = mediaRecords.size(); int recordCount = mediaRecords.size();

View File

@ -39,7 +39,8 @@ import android.view.Window;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@ -52,7 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
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;
@ -352,11 +352,26 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
saveTask.executeOnExecutor( saveTask.executeOnExecutor(
AsyncTask.THREAD_POOL_EXECUTOR, AsyncTask.THREAD_POOL_EXECUTOR,
new Attachment(mediaItem.uri, mediaItem.type, saveDate, null)); new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
// Sending a Data extraction notification (for incoming attachments only)
if(!mediaItem.outgoing) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
}) })
.execute(); .execute();
}); });
} }
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (conversationRecipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, conversationRecipient.getAddress());
}
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private void deleteMedia() { private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem(); MediaItem mediaItem = getCurrentMediaItem();

View File

@ -40,6 +40,7 @@ import androidx.loader.content.Loader;
import org.session.libsession.messaging.messages.visible.LinkPreview; import org.session.libsession.messaging.messages.visible.LinkPreview;
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.sending_receiving.MessageSender; 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.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus; import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
@ -64,7 +65,6 @@ import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsession.utilities.ExpirationUtil; import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.sql.Date; import java.sql.Date;
@ -263,7 +263,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
} }
toFrom.setText(toFromRes); toFrom.setText(toFromRes);
long threadID = messageRecord.getThreadId(); long threadID = messageRecord.getThreadId();
PublicChat openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID); OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID);
if (openGroup != null && messageRecord.isOutgoing()) { if (openGroup != null && messageRecord.isOutgoing()) {
toFrom.setVisibility(View.GONE); toFrom.setVisibility(View.GONE);
separator.setVisibility(View.GONE); separator.setVisibility(View.GONE);

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.
if (keyCachingService != null) {
keyCachingService.setMasterSecret(new Object()); 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

@ -5,6 +5,7 @@ import android.text.TextUtils
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
@ -26,7 +27,6 @@ import org.thoughtcrime.securesms.util.MediaUtil
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider { class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), MessageDataProvider {
override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? { override fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? {
@ -104,6 +104,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp) return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
} }
override fun getOpenGroup(threadID: Long): OpenGroup? {
return null // TODO: Implement
}
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
val database = DatabaseFactory.getAttachmentDatabase(context) val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
@ -133,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

@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; 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.quotes.QuoteModel;
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.libsession.utilities.TextSecurePreferences;

View File

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import network.loki.messenger.R; import network.loki.messenger.R;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;

View File

@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -35,7 +36,6 @@ import org.session.libsession.messaging.threads.recipients.RecipientModifiedList
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.ThemeUtil;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import java.util.List; import java.util.List;
@ -201,7 +201,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient); long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient);
String senderHexEncodedPublicKey = author.getAddress().serialize(); String senderHexEncodedPublicKey = author.getAddress().serialize();
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID); OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) { if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext()); quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
} else if (publicChat != null) { } else if (publicChat != null) {

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.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.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.api.opengroups.PublicChat; import org.session.libsignal.service.loki.Mention;
import org.session.libsignal.service.loki.utilities.mentions.Mention;
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
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,9 +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.OutgoingExpirationUpdateMessage;
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;
@ -169,30 +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.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
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.linkpreview.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;
@ -376,10 +374,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this); MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
PublicChat 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);
@ -806,15 +807,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime); DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime);
ExpirationTimerUpdate message = new ExpirationTimerUpdate(null, expirationTime); ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime);
message.setRecipient(recipient.getAddress().serialize()); // we need the recipient in ExpiringMessageManager.insertOutgoingExpirationTimerMessage
message.setSentTimestamp(System.currentTimeMillis()); message.setSentTimestamp(System.currentTimeMillis());
OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient); ExpiringMessageManager expiringMessageManager = ApplicationContext.getInstance(getApplicationContext()).getExpiringMessageManager();
try { expiringMessageManager.setExpirationTimer(message);
message.setId(DatabaseFactory.getMmsDatabase(ConversationActivity.this).insertMessageOutbox(outgoingMessage, getAllocatedThreadId(ConversationActivity.this), false, null));
MessageSender.send(message, recipient.getAddress()); MessageSender.send(message, recipient.getAddress());
} catch (MmsException e) {
Log.w(TAG, e);
}
return null; return null;
} }
@ -1401,12 +1400,18 @@ 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) {
PublicChat 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
@ -1723,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
@ -2339,11 +2344,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteIndicatorImageView.setVisibility(View.VISIBLE); muteIndicatorImageView.setVisibility(View.VISIBLE);
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")) {
PublicChat 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,48 +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.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.opengroups.OpenGroupAPI; import org.session.libsession.messaging.open_groups.OpenGroup;
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.linkpreview.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.libsignal.service.loki.api.opengroups.PublicChat;
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;
@ -395,8 +398,9 @@ public class ConversationFragment extends Fragment
boolean isGroupChat = recipient.isGroupRecipient(); boolean isGroupChat = recipient.isGroupRecipient();
if (isGroupChat) { if (isGroupChat) {
PublicChat 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<>();
@ -406,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;
@ -507,7 +515,8 @@ public class ConversationFragment extends Fragment
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount)); builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true); builder.setCancelable(true);
PublicChat 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
@ -518,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 {
@ -537,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());
@ -554,6 +563,24 @@ 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) {
@ -590,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);
PublicChat 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();
@ -609,7 +637,17 @@ 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;
});
} 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; return null;
}); });
} else { } else {
@ -745,6 +783,11 @@ public class ConversationFragment extends Fragment
if (!Util.isEmpty(attachments)) { if (!Util.isEmpty(attachments)) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity()); SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0])); saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
// Sending a Data extraction notification (for incoming attachments only)
if(!message.isOutgoing()) {
//TODO uncomment line below when Data extraction will be activated
//sendMediaSavedNotificationIfNeeded();
}
return; return;
} }
@ -757,6 +800,16 @@ public class ConversationFragment extends Fragment
}); });
} }
/**
* Send a MediaSaved notification to the recipient
*/
private void sendMediaSavedNotificationIfNeeded() {
// we don't send media saved notification for groups
if (recipient.isGroupRecipient()) return;
DataExtractionNotification message = new DataExtractionNotification(new DataExtractionNotification.Kind.MediaSaved(System.currentTimeMillis()));
MessageSender.send(message, recipient.getAddress());
}
@Override @Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) { public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.i(TAG, "onCreateLoader"); Log.i(TAG, "onCreateLoader");

View File

@ -54,10 +54,13 @@ import com.annimon.stream.Stream;
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;
import org.session.libsession.messaging.opengroups.OpenGroupAPI; import org.session.libsession.messaging.open_groups.OpenGroup;
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.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener; import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.GroupUtil;
@ -67,7 +70,6 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil; import org.session.libsession.utilities.ViewUtil;
import org.session.libsession.utilities.views.Stub; import org.session.libsession.utilities.views.Stub;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
@ -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();
PublicChat 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
} }
@ -911,10 +919,14 @@ public class ConversationItem extends LinearLayout
profilePictureView.setVisibility(VISIBLE); profilePictureView.setVisibility(VISIBLE);
int visibility = View.GONE; int visibility = View.GONE;
PublicChat 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

@ -14,11 +14,10 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.utilities.GroupDescription;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
@ -106,6 +105,8 @@ public class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isCallLog()) setCallRecord(messageRecord); else if (messageRecord.isCallLog()) setCallRecord(messageRecord);
else if (messageRecord.isJoined()) setJoinedRecord(messageRecord); else if (messageRecord.isJoined()) setJoinedRecord(messageRecord);
else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord); else if (messageRecord.isExpirationTimerUpdate()) setTimerRecord(messageRecord);
else if (messageRecord.isScreenshotExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT);
else if (messageRecord.isMediaSavedExtraction()) setDataExtractionRecord(messageRecord, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED);
else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord); else if (messageRecord.isEndSession()) setEndSessionRecord(messageRecord);
else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord); else if (messageRecord.isIdentityUpdate()) setIdentityRecord(messageRecord);
else if (messageRecord.isIdentityVerified() || else if (messageRecord.isIdentityVerified() ||
@ -149,6 +150,22 @@ public class ConversationUpdateItem extends LinearLayout
date.setVisibility(GONE); date.setVisibility(GONE);
} }
private void setDataExtractionRecord(final MessageRecord messageRecord, DataExtractionNotificationInfoMessage.Kind kind) {
@ColorInt int color = GeneralUtilitiesKt.getColorWithID(getResources(), R.color.text, getContext().getTheme());
if (kind == DataExtractionNotificationInfoMessage.Kind.SCREENSHOT) {
icon.setImageResource(R.drawable.quick_camera_dark);
} else if (kind == DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED) {
icon.setImageResource(R.drawable.ic_file_download_white_36dp);
}
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(VISIBLE);
body.setVisibility(VISIBLE);
date.setVisibility(GONE);
}
private void setIdentityRecord(final MessageRecord messageRecord) { private void setIdentityRecord(final MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_security_white_24dp); icon.setImageResource(R.drawable.ic_security_white_24dp);
icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY)); icon.setColorFilter(new PorterDuffColorFilter(Color.parseColor("#757575"), PorterDuff.Mode.MULTIPLY));
@ -174,8 +191,6 @@ public class ConversationUpdateItem extends LinearLayout
private void setGroupRecord(MessageRecord messageRecord) { private void setGroupRecord(MessageRecord messageRecord) {
icon.setImageResource(R.drawable.ic_group_grey600_24dp); icon.setImageResource(R.drawable.ic_group_grey600_24dp);
icon.clearColorFilter(); icon.clearColorFilter();
GroupDescription.Companion.getDescription(getContext(), messageRecord.getBody()).addListener(this);
body.setText(messageRecord.getDisplayBody(getContext())); body.setText(messageRecord.getDisplayBody(getContext()));
title.setVisibility(GONE); title.setVisibility(GONE);

View File

@ -25,10 +25,9 @@ import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer; import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol; import org.session.libsignal.service.loki.LokiOpenGroupDatabaseProtocol;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -44,6 +43,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
static final String GROUP_ID = "group_id"; static final String GROUP_ID = "group_id";
private static final String TITLE = "title"; private static final String TITLE = "title";
private static final String MEMBERS = "members"; private static final String MEMBERS = "members";
private static final String ZOMBIE_MEMBERS = "zombie_members";
private static final String AVATAR = "avatar"; private static final String AVATAR = "avatar";
private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_KEY = "avatar_key";
@ -64,6 +64,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
GROUP_ID + " TEXT, " + GROUP_ID + " TEXT, " +
TITLE + " TEXT, " + TITLE + " TEXT, " +
MEMBERS + " TEXT, " + MEMBERS + " TEXT, " +
ZOMBIE_MEMBERS + " TEXT, " +
AVATAR + " BLOB, " + AVATAR + " BLOB, " +
AVATAR_ID + " INTEGER, " + AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " + AVATAR_KEY + " BLOB, " +
@ -81,7 +82,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}; };
private static final String[] GROUP_PROJECTION = { private static final String[] GROUP_PROJECTION = {
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, GROUP_ID, TITLE, MEMBERS, ZOMBIE_MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS
}; };
@ -162,7 +163,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) { public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
List<Address> members = getCurrentMembers(groupId); List<Address> members = getCurrentMembers(groupId, false);
List<Recipient> recipients = new LinkedList<>(); List<Recipient> recipients = new LinkedList<>();
for (Address member : members) { for (Address member : members) {
@ -177,6 +178,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return recipients; return recipients;
} }
public @NonNull List<Recipient> getGroupZombieMembers(String groupId) {
List<Address> members = getCurrentZombieMembers(groupId);
List<Recipient> recipients = new LinkedList<>();
for (Address member : members) {
recipients.add(Recipient.from(context, member, false));
}
return recipients;
}
public long create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members, public long create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins, @NonNull Long formationTimestamp) @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins, @NonNull Long formationTimestamp)
{ {
@ -300,6 +312,16 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}); });
} }
public void updateZombieMembers(String groupId, List<Address> members) {
Collections.sort(members);
ContentValues contents = new ContentValues();
contents.put(ZOMBIE_MEMBERS, Address.toSerializedList(members, ','));
contents.put(ACTIVE, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId});
}
public void updateAdmins(String groupId, List<Address> admins) { public void updateAdmins(String groupId, List<Address> admins) {
Collections.sort(admins); Collections.sort(admins);
@ -311,7 +333,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
public void removeMember(String groupId, Address source) { public void removeMember(String groupId, Address source) {
List<Address> currentMembers = getCurrentMembers(groupId); List<Address> currentMembers = getCurrentMembers(groupId, false);
currentMembers.remove(source); currentMembers.remove(source);
ContentValues contents = new ContentValues(); ContentValues contents = new ContentValues();
@ -329,17 +351,21 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}); });
} }
private List<Address> getCurrentMembers(String groupId) { private List<Address> getCurrentMembers(String groupId, boolean zombieMembers) {
Cursor cursor = null; Cursor cursor = null;
String membersColumn = MEMBERS;
if (zombieMembers) membersColumn = ZOMBIE_MEMBERS;
try { try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS}, cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {membersColumn},
GROUP_ID + " = ?", GROUP_ID + " = ?",
new String[] {groupId}, new String[] {groupId},
null, null, null); null, null, null);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)); String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(membersColumn));
if (serializedMembers != null && !serializedMembers.isEmpty())
return Address.fromSerializedList(serializedMembers, ','); return Address.fromSerializedList(serializedMembers, ',');
} }
@ -350,6 +376,10 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
} }
private List<Address> getCurrentZombieMembers(String groupId) {
return getCurrentMembers(groupId, true);
}
public boolean isActive(String groupId) { public boolean isActive(String groupId) {
Optional<GroupRecord> record = getGroup(groupId); Optional<GroupRecord> record = getGroup(groupId);
return record.isPresent() && record.get().isActive(); return record.isPresent() && record.get().isActive();

View File

@ -28,7 +28,6 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.mms.pdu_alt.NotificationInd; import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduHeaders; import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
@ -60,7 +59,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment; import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; 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.quotes.QuoteModel;
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;
@ -514,15 +513,7 @@ public class MmsDatabase extends MessagingDatabase {
} }
} }
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches);
return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, quote, contacts, previews);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
boolean expirationTimer = (outboxType & Types.EXPIRATION_TIMER_UPDATE_BIT) != 0;
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, expirationTimer, distributionType, quote, contacts, previews, networkFailures, mismatches);
if (Types.isSecureType(outboxType)) { if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message); return new OutgoingSecureMediaMessage(message);
@ -532,8 +523,6 @@ public class MmsDatabase extends MessagingDatabase {
} }
throw new NoSuchMessageException("No record found for id: " + messageId); throw new NoSuchMessageException("No record found for id: " + messageId);
} catch (IOException e) {
throw new MmsException(e);
} finally { } finally {
if (cursor != null) if (cursor != null)
cursor.close(); cursor.close();
@ -689,8 +678,19 @@ public class MmsDatabase extends MessagingDatabase {
{ {
if (threadId == -1) { if (threadId == -1) {
if(retrieved.isGroup()) { if(retrieved.isGroup()) {
ByteString decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupContext().getId(); String decodedGroupId;
String groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId.toByteArray()); if (retrieved instanceof OutgoingExpirationUpdateMessage) {
decodedGroupId = ((OutgoingExpirationUpdateMessage)retrieved).getGroupId();
} else {
decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupId();
}
String groupId;
try {
groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId);
} catch (IOException e) {
Log.e(TAG, "Couldn't encrypt group ID");
throw new MmsException(e);
}
Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false); Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false);
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group); threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group);
} else { } else {
@ -718,6 +718,14 @@ public class MmsDatabase extends MessagingDatabase {
type |= Types.EXPIRATION_TIMER_UPDATE_BIT; type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
} }
if (retrieved.isScreenshotDataExtraction()) {
type |= Types.SCREENSHOT_EXTRACTION_BIT;
}
if (retrieved.isMediaSavedDataExtraction()) {
type |= Types.MEDIA_SAVED_EXTRACTION_BIT;
}
return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp); return insertMessageInbox(retrieved, "", threadId, type, serverTimestamp);
} }
@ -745,9 +753,8 @@ public class MmsDatabase extends MessagingDatabase {
if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT);
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
if (message.isGroup()) { if (message.isGroup() && message instanceof OutgoingGroupMediaMessage) {
if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT; if (((OutgoingGroupMediaMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT;
else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
} }
if (message.isExpirationUpdate()) { if (message.isExpirationUpdate()) {

View File

@ -70,6 +70,11 @@ public interface MmsSmsColumns {
protected static final long GROUP_UPDATE_BIT = 0x10000; protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000; protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000; protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
protected static final long GROUP_UPDATE_MESSAGE_BIT = 0x80000;
// Data Extraction Information
protected static final long MEDIA_SAVED_EXTRACTION_BIT = 0x01000;
protected static final long SCREENSHOT_EXTRACTION_BIT = 0x02000;
// Encrypted Storage Information XXX // Encrypted Storage Information XXX
public static final long ENCRYPTION_MASK = 0xFF000000; public static final long ENCRYPTION_MASK = 0xFF000000;
@ -197,6 +202,14 @@ public interface MmsSmsColumns {
return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0; return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0;
} }
public static boolean isMediaSavedExtraction(long type) {
return (type & MEDIA_SAVED_EXTRACTION_BIT) != 0;
}
public static boolean isScreenshotExtraction(long type) {
return (type & SCREENSHOT_EXTRACTION_BIT) != 0;
}
public static boolean isIncomingCall(long type) { public static boolean isIncomingCall(long type) {
return type == INCOMING_CALL_TYPE; return type == INCOMING_CALL_TYPE;
} }
@ -213,6 +226,8 @@ public interface MmsSmsColumns {
return (type & GROUP_UPDATE_BIT) != 0; return (type & GROUP_UPDATE_BIT) != 0;
} }
public static boolean isGroupUpdateMessage(long type) { return (type & GROUP_UPDATE_MESSAGE_BIT) != 0; }
public static boolean isGroupQuit(long type) { public static boolean isGroupQuit(long type) {
return (type & GROUP_QUIT_BIT) != 0; return (type & GROUP_QUIT_BIT) != 0;
} }

View File

@ -347,8 +347,7 @@ public class SmsDatabase extends MessagingDatabase {
type |= Types.SECURE_MESSAGE_BIT; type |= Types.SECURE_MESSAGE_BIT;
} else if (message.isGroup()) { } else if (message.isGroup()) {
type |= Types.SECURE_MESSAGE_BIT; type |= Types.SECURE_MESSAGE_BIT;
if (((IncomingGroupMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT; if (((IncomingGroupMessage)message).isUpdateMessage()) type |= Types.GROUP_UPDATE_MESSAGE_BIT;
else if (((IncomingGroupMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT;
} }
if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.google.protobuf.ByteString 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,14 +13,18 @@ import org.session.libsession.messaging.messages.signal.*
import org.session.libsession.messaging.messages.signal.IncomingTextMessage 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.opengroups.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.linkpreview.LinkPreview import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
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.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.Address.Companion.fromSerialized
import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.GroupRecord
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageData
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
@ -30,7 +34,6 @@ import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
@ -83,6 +86,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return recipient.profileKey return recipient.profileKey
} }
override fun getDisplayNameForRecipient(recipientPublicKey: String): String? {
val database = DatabaseFactory.getLokiUserDatabase(context)
return database.getDisplayName(recipientPublicKey)
}
override fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) { override fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) {
val address = Address.fromSerialized(recipientPublicKey) val address = Address.fromSerialized(recipientPublicKey)
val recipient = Recipient.from(context, address, false) val recipient = Recipient.from(context, address, false)
@ -138,7 +146,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
val insertResult = if (message.sender == getUserPublicKey()) { val insertResult = if (message.sender == getUserPublicKey()) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull()) val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.beginTransaction() mmsDatabase.beginTransaction()
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
@ -221,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
@ -230,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)
@ -249,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)
} }
@ -266,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)
} }
@ -310,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)
@ -320,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? {
@ -386,6 +456,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value) DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
} }
override fun getZombieMember(groupID: String): Set<String> {
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
}
override fun removeMember(groupID: String, member: Address) { override fun removeMember(groupID: String, member: Address) {
DatabaseFactory.getGroupDatabase(context).removeMember(groupID, member) DatabaseFactory.getGroupDatabase(context).removeMember(groupID, member)
} }
@ -394,33 +468,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
} }
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) { override fun updateZombieMembers(groupID: String, members: List<Address>) {
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder() DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) }
.setType(type0)
.setName(name) override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
.addAllMembers(members) val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
.addAllAdmins(admins)
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") val updateData = ClosedGroupUpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON()
val infoMessage = IncomingGroupMessage(m, groupID, updateData, true)
val smsDB = DatabaseFactory.getSmsDatabase(context) val smsDB = DatabaseFactory.getSmsDatabase(context)
smsDB.insertMessageInbox(infoMessage) smsDB.insertMessageInbox(infoMessage)
} }
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) { override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
val userPublicKey = getUserPublicKey() val userPublicKey = getUserPublicKey()
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) val updateData = ClosedGroupUpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: ""
.setType(type) val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf())
.setName(name)
.addAllMembers(members)
.addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, false, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsDB = DatabaseFactory.getMmsDatabase(context)
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return if (mmsSmsDB.getMessageFor(sentTimestamp, userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true) mmsDB.markAsSent(infoMessageID, true)
} }
@ -471,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> {
@ -509,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)
} }
@ -578,4 +675,26 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri { override fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentThumbnailUri(attachmentId) return PartAuthority.getAttachmentThumbnailUri(attachmentId)
} }
// Data Extraction Notification
override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) {
val database = DatabaseFactory.getMmsDatabase(context)
val address = fromSerialized(senderPublicKey)
val recipient = Recipient.from(context, address, false)
if (recipient.isBlocked) return
val mediaMessage = IncomingMediaMessage(address, sentTimestamp, -1,
0, false,
false,
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.of(message))
database.insertSecureDecryptedMessageInbox(mediaMessage, -1)
}
} }

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

@ -54,9 +54,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV20 = 41; private static final int lokiV20 = 41;
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 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 = lokiV22; 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;
@ -124,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);
@ -272,6 +276,21 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
"SendDeliveryReceiptJob"); "SendDeliveryReceiptJob");
} }
if (oldVersion < lokiV23) {
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();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -109,6 +109,7 @@ public abstract class DisplayRecord {
public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); } public boolean isLokiSessionRestoreDone() { return SmsDatabase.Types.isLokiSessionRestoreDoneType(type); }
// TODO isGroupUpdate and isGroupQuit are kept for compatibility with old update messages, they can be removed later on
public boolean isGroupUpdate() { public boolean isGroupUpdate() {
return SmsDatabase.Types.isGroupUpdate(type); return SmsDatabase.Types.isGroupUpdate(type);
} }
@ -117,14 +118,33 @@ public abstract class DisplayRecord {
return SmsDatabase.Types.isGroupQuit(type); return SmsDatabase.Types.isGroupQuit(type);
} }
public boolean isGroupUpdateMessage() {
return SmsDatabase.Types.isGroupUpdateMessage(type);
}
//TODO isGroupAction can be replaced by isGroupUpdateMessage in the code when the 2 functions above are removed
public boolean isGroupAction() { public boolean isGroupAction() {
return isGroupUpdate() || isGroupQuit(); return isGroupUpdate() || isGroupQuit() || isGroupUpdateMessage();
} }
public boolean isExpirationTimerUpdate() { public boolean isExpirationTimerUpdate() {
return SmsDatabase.Types.isExpirationTimerUpdate(type); return SmsDatabase.Types.isExpirationTimerUpdate(type);
} }
// Data extraction
public boolean isMediaSavedExtraction() {
return MmsSmsColumns.Types.isMediaSavedExtraction(type);
}
public boolean isScreenshotExtraction() {
return MmsSmsColumns.Types.isScreenshotExtraction(type);
}
public boolean isDataExtraction() {
return isMediaSavedExtraction() || isScreenshotExtraction();
}
public boolean isCallLog() { public boolean isCallLog() {
return SmsDatabase.Types.isCallLog(type); return SmsDatabase.Types.isCallLog(type);
} }

View File

@ -23,7 +23,7 @@ import android.text.SpannableString;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status; import org.thoughtcrime.securesms.database.SmsDatabase.Status;
import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.database.documents.IdentityKeyMismatch;

View File

@ -24,14 +24,16 @@ import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import network.loki.messenger.R; import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageBuilder;
import org.session.libsession.messaging.utilities.ClosedGroupUpdateMessageData;
import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure; import org.session.libsession.database.documents.NetworkFailure;
import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.ExpirationUtil;
import org.thoughtcrime.securesms.loki.utilities.GroupDescription;
import java.util.List; import java.util.List;
@ -90,39 +92,25 @@ public abstract class MessageRecord extends DisplayRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
if (isGroupUpdate() && isOutgoing()) { if(isGroupUpdateMessage()) {
ClosedGroupUpdateMessageData updateMessageData = ClosedGroupUpdateMessageData.Companion.fromJSON(getBody());
return new SpannableString(ClosedGroupUpdateMessageBuilder.INSTANCE.buildGroupUpdateMessage(context, updateMessageData, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
} else if (isExpirationTimerUpdate()) {
int seconds = (int) (getExpiresIn() / 1000);
return new SpannableString(ClosedGroupUpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, seconds, getIndividualRecipient().getAddress().serialize(), isOutgoing()));
} else if (isDataExtraction()) {
if (isScreenshotExtraction()) return new SpannableString((ClosedGroupUpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.SCREENSHOT, getIndividualRecipient().getAddress().serialize())));
else if (isMediaSavedExtraction()) return new SpannableString((ClosedGroupUpdateMessageBuilder.INSTANCE.buildDataExtractionMessage(context, DataExtractionNotificationInfoMessage.Kind.MEDIA_SAVED, getIndividualRecipient().getAddress().serialize())));
}
// TODO below lines are left here for compatibility with older group update messages, it can be deleted later on
else if (isGroupUpdate() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group));
} else if (isGroupUpdate()) { } else if (isGroupUpdate()) {
return new SpannableString(GroupDescription.Companion.getDescription(context, getBody()).toString(getIndividualRecipient())); return new SpannableString(context.getString(R.string.MessageRecord_s_updated_group, getIndividualRecipient().toShortString()));
} else if (isGroupQuit() && isOutgoing()) { } else if (isGroupQuit() && isOutgoing()) {
return new SpannableString(context.getString(R.string.MessageRecord_left_group)); return new SpannableString(context.getString(R.string.MessageRecord_left_group));
} else if (isGroupQuit()) { } else if (isGroupQuit()) {
return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString())); return new SpannableString(context.getString(R.string.ConversationItem_group_action_left, getIndividualRecipient().toShortString()));
} else if (isIncomingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_called_you, getIndividualRecipient().toShortString()));
} else if (isOutgoingCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_you_called));
} else if (isMissedCall()) {
return new SpannableString(context.getString(R.string.MessageRecord_missed_call));
} else if (isJoined()) {
return new SpannableString(context.getString(R.string.MessageRecord_s_joined_signal, getIndividualRecipient().toShortString()));
} else if (isExpirationTimerUpdate()) {
int seconds = (int)(getExpiresIn() / 1000);
if (seconds <= 0) {
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages))
: new SpannableString(context.getString(R.string.MessageRecord_s_disabled_disappearing_messages, getIndividualRecipient().toShortString()));
}
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return isOutgoing() ? new SpannableString(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time))
: new SpannableString(context.getString(R.string.MessageRecord_s_set_disappearing_message_time_to_s, getIndividualRecipient().toShortString(), time));
} else if (isIdentityUpdate()) {
return new SpannableString(context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, getIndividualRecipient().toShortString()));
} else if (isIdentityVerified()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified, getIndividualRecipient().toShortString()));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device, getIndividualRecipient().toShortString()));
} else if (isIdentityDefault()) {
if (isOutgoing()) return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified, getIndividualRecipient().toShortString()));
else return new SpannableString(context.getString(R.string.MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device, getIndividualRecipient().toShortString()));
} }
return new SpannableString(getBody()); return new SpannableString(getBody());
@ -175,7 +163,7 @@ public abstract class MessageRecord extends DisplayRecord {
} }
public boolean isUpdate() { public boolean isUpdate() {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isDataExtraction() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone(); isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || isLokiSessionRestoreSent() || isLokiSessionRestoreDone();
} }

View File

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.database.documents.IdentityKeyMismatch; import org.session.libsession.database.documents.IdentityKeyMismatch;

View File

@ -73,7 +73,7 @@ public class ThreadRecord extends DisplayRecord {
@Override @Override
public SpannableString getDisplayBody(@NonNull Context context) { public SpannableString getDisplayBody(@NonNull Context context) {
Recipient recipient = getRecipient(); Recipient recipient = getRecipient();
if (isGroupUpdate()) { if (isGroupUpdate() || isGroupUpdateMessage()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated)); return emphasisAdded(context.getString(R.string.ThreadRecord_group_updated));
} else if (isGroupQuit()) { } else if (isGroupQuit()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group)); return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group));
@ -103,12 +103,16 @@ public class ThreadRecord extends DisplayRecord {
} else if (SmsDatabase.Types.isJoinedType(type)) { } else if (SmsDatabase.Types.isJoinedType(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString())); return emphasisAdded(context.getString(R.string.ThreadRecord_s_is_on_signal, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) { } else if (SmsDatabase.Types.isExpirationTimerUpdate(type)) {
int seconds = (int)(getExpiresIn() / 1000); int seconds = (int) (getExpiresIn() / 1000);
if (seconds <= 0) { if (seconds <= 0) {
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled)); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_messages_disabled));
} }
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time)); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else if (MmsSmsColumns.Types.isMediaSavedExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_media_saved_by_s, getRecipient().toShortString()));
} else if (MmsSmsColumns.Types.isScreenshotExtraction(type)) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_took_a_screenshot, getRecipient().toShortString()));
} else if (SmsDatabase.Types.isIdentityUpdate(type)) { } else if (SmsDatabase.Types.isIdentityUpdate(type)) {
if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed)); if (getRecipient().isGroupRecipient()) return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString())); else return emphasisAdded(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, getRecipient().toShortString()));

View File

@ -4,6 +4,8 @@ import android.graphics.Bitmap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.session.libsession.messaging.threads.GroupRecord; import org.session.libsession.messaging.threads.GroupRecord;
@ -22,6 +24,7 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException; import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -92,7 +95,18 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
attachment.deleteOnExit(); attachment.deleteOnExit();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url); SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
DownloadUtilities.downloadFile(attachment, pointer.getUrl(), MAX_AVATAR_SIZE, null);
// Assume we're retrieving an attachment for an open group server if the digest is not set
InputStream inputStream;
if (!pointer.getDigest().isPresent()) {
inputStream = new FileInputStream(attachment);
} else {
inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
database.updateProfilePicture(groupId, avatar); database.updateProfilePicture(groupId, avatar);

View File

@ -10,9 +10,11 @@ import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.service.api.SignalServiceMessageReceiver; import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.crypto.ProfileCipherInputStream;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException; import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -22,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -102,7 +105,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
try { try {
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); DownloadUtilities.downloadFile(downloadDestination, profileAvatar, MAX_PROFILE_SIZE_BYTES, null);
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination)); Util.copy(avatarStream, new FileOutputStream(decryptDestination));

View File

@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment; import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import org.session.libsession.utilities.concurrent.SignalExecutors; import org.session.libsession.utilities.concurrent.SignalExecutors;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;

View File

@ -13,9 +13,8 @@ import org.session.libsession.utilities.Debouncer;
import org.session.libsession.utilities.Util; import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import java.util.Collections;
import java.util.List; import java.util.List;

View File

@ -32,6 +32,10 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
set(newValue) { field = newValue; invalidateOptionsMenu() } set(newValue) { field = newValue; invalidateOptionsMenu() }
private var members = listOf<String>() private var members = listOf<String>()
set(value) { field = value; selectContactsAdapter.members = value } set(value) { field = value; selectContactsAdapter.members = value }
private val publicKey: String
get() {
return TextSecurePreferences.getLocalNumber(this)!!
}
private val selectContactsAdapter by lazy { private val selectContactsAdapter by lazy {
SelectContactsAdapter(this, GlideApp.with(this)) SelectContactsAdapter(this, GlideApp.with(this))
@ -72,7 +76,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
} }
private fun update(members: List<String>) { private fun update(members: List<String>) {
this.members = members //if there is a Note to self conversation, it loads self in the list, so we need to remove it here
this.members = members.minus(publicKey)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()

View File

@ -37,8 +37,14 @@ import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() { class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>() private val originalMembers = HashSet<String>()
private val zombies = HashSet<String>()
private val members = HashSet<String>() private val members = HashSet<String>()
private val allMembers: Set<String>
get() {
return members + zombies
}
private var hasNameChanged = false private var hasNameChanged = false
private var isSelfAdmin = false
private var isLoading = false private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() } set(newValue) { field = newValue; invalidateOptionsMenu() }
@ -54,7 +60,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
private val memberListAdapter by lazy { private val memberListAdapter by lazy {
EditClosedGroupMembersAdapter(this, GlideApp.with(this), this::onMemberClick) if (isSelfAdmin)
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin, this::onMemberClick)
else
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin)
} }
private lateinit var mainContentContainer: LinearLayout private lateinit var mainContentContainer: LinearLayout
@ -81,7 +90,10 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable)) ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!! groupID = intent.getStringExtra(groupIDKey)!!
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title val groupInfo = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get()
originalName = groupInfo.title
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
name = originalName name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer) mainContentContainer = findViewById(R.id.mainContentContainer)
@ -116,31 +128,35 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> { LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<GroupMembers> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<GroupMembers> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID) return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
} }
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) { override fun onLoadFinished(loader: Loader<GroupMembers>, groupMembers: GroupMembers) {
// We no longer need any subsequent loading events // We no longer need any subsequent loading events
// (they will occur on every activity resume). // (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID) LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
members.clear()
members.addAll(groupMembers.members.toHashSet())
zombies.clear()
zombies.addAll(groupMembers.zombieMembers.toHashSet())
originalMembers.clear() originalMembers.clear()
originalMembers.addAll(members.toHashSet()) originalMembers.addAll(members + zombies)
updateMembers(originalMembers) updateMembers()
} }
override fun onLoaderReset(loader: Loader<List<String>>) { override fun onLoaderReset(loader: Loader<GroupMembers>) {
updateMembers(setOf()) updateMembers()
} }
}) })
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu) menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return members.isNotEmpty() && !isLoading return allMembers.isNotEmpty() && !isLoading
} }
// endregion // endregion
@ -153,8 +169,8 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet() val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet()
val changedMembers = members + selectedContacts members.addAll(selectedContacts)
updateMembers(changedMembers) updateMembers()
} }
} }
} }
@ -173,17 +189,12 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun updateMembers(members: Set<String>) { private fun updateMembers() {
this.members.clear() memberListAdapter.setMembers(allMembers)
this.members.addAll(members) memberListAdapter.setZombieMembers(zombies)
memberListAdapter.setMembers(members)
val admins = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().admins.map { it.toString() }.toMutableSet() mainContentContainer.visibility = if (allMembers.isEmpty()) View.GONE else View.VISIBLE
admins.remove(TextSecurePreferences.getLocalNumber(this)) emptyStateContainer.visibility = if (allMembers.isEmpty()) View.VISIBLE else View.GONE
memberListAdapter.setLockedMembers(admins)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -200,8 +211,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onMemberClick(member: String) { private fun onMemberClick(member: String) {
val bottomSheet = ClosedGroupEditingOptionsBottomSheet() val bottomSheet = ClosedGroupEditingOptionsBottomSheet()
bottomSheet.onRemoveTapped = { bottomSheet.onRemoveTapped = {
val changedMembers = members - member if (zombies.contains(member)) zombies.remove(member)
updateMembers(changedMembers) else members.remove(member)
updateMembers()
bottomSheet.dismiss() bottomSheet.dismiss()
} }
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet") bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
@ -209,7 +221,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onAddMembersClick() { private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java) val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.toTypedArray()) intent.putExtra(SelectContactsActivity.usersToExcludeKey, allMembers.toTypedArray())
intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add") intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode) startActivityForResult(intent, addUsersRequestCode)
} }
@ -229,7 +241,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
private fun commitChanges() { private fun commitChanges() {
val hasMemberListChanges = (members != originalMembers) val hasMemberListChanges = (allMembers != originalMembers)
if (!hasNameChanged && !hasMemberListChanges) { if (!hasNameChanged && !hasMemberListChanges) {
return finish() return finish()
@ -237,15 +249,13 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
val name = if (hasNameChanged) this.name else originalName val name = if (hasNameChanged) this.name else originalName
val members = this.members.map { val members = this.allMembers.map {
Recipient.from(this, Address.fromSerialized(it), false) Recipient.from(this, Address.fromSerialized(it), false)
}.toSet() }.toSet()
val originalMembers = this.originalMembers.map { val originalMembers = this.originalMembers.map {
Recipient.from(this, Address.fromSerialized(it), false) Recipient.from(this, Address.fromSerialized(it), false)
}.toSet() }.toSet()
val admins = members.toSet() //TODO For now, consider all the users to be admins.
var isClosedGroup: Boolean var isClosedGroup: Boolean
var groupPublicKey: String? var groupPublicKey: String?
try { try {
@ -303,4 +313,6 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
} }
class GroupMembers(val members: List<String>, val zombieMembers: List<String>) { }
} }

View File

@ -4,12 +4,19 @@ import android.content.Context
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.AsyncLoader import org.thoughtcrime.securesms.util.AsyncLoader
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<List<String>>(context) { class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<EditClosedGroupActivity.GroupMembers>(context) {
override fun loadInBackground(): List<String> { override fun loadInBackground(): EditClosedGroupActivity.GroupMembers {
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true) val groupDatabase = DatabaseFactory.getGroupDatabase(context)
return members.map { val members = groupDatabase.getGroupMembers(groupID, true)
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
return EditClosedGroupActivity.GroupMembers(
members.map {
it.address.toString()
},
zombieMembers.map {
it.address.toString() it.address.toString()
} }
)
} }
} }

View File

@ -7,15 +7,17 @@ import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.loki.views.UserView import org.thoughtcrime.securesms.loki.views.UserView
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
class EditClosedGroupMembersAdapter( class EditClosedGroupMembersAdapter(
private val context: Context, private val context: Context,
private val glide: GlideRequests, private val glide: GlideRequests,
private val admin: Boolean,
private val memberClickListener: ((String) -> Unit)? = null private val memberClickListener: ((String) -> Unit)? = null
) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() { ) : RecyclerView.Adapter<EditClosedGroupMembersAdapter.ViewHolder>() {
private val members = ArrayList<String>() private val members = ArrayList<String>()
private val lockedMembers = HashSet<String>() private val zombieMembers = ArrayList<String>()
fun setMembers(members: Collection<String>) { fun setMembers(members: Collection<String>) {
this.members.clear() this.members.clear()
@ -23,9 +25,9 @@ class EditClosedGroupMembersAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
fun setLockedMembers(members: Collection<String>) { fun setZombieMembers(members: Collection<String>) {
this.lockedMembers.clear() this.zombieMembers.clear()
this.lockedMembers.addAll(members) this.zombieMembers.addAll(members)
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -39,15 +41,20 @@ class EditClosedGroupMembersAdapter(
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val member = members[position] val member = members[position]
val lockedMember = lockedMembers.contains(member) val unlocked = admin && member != TextSecurePreferences.getLocalNumber(context)
viewHolder.view.bind(Recipient.from( viewHolder.view.bind(Recipient.from(
context, context,
Address.fromSerialized(member), false), Address.fromSerialized(member), false),
glide, glide,
if (lockedMember) UserView.ActionIndicator.None else UserView.ActionIndicator.Menu) if (unlocked) UserView.ActionIndicator.Menu else UserView.ActionIndicator.None)
if (!lockedMember) { if (zombieMembers.contains(member))
viewHolder.view.alpha = 0.5F
else
viewHolder.view.alpha = 1F
if (unlocked) {
viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) } viewHolder.view.setOnClickListener { this.memberClickListener?.invoke(member) }
} }
} }

View File

@ -30,10 +30,10 @@ 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.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.utilities.* import org.session.libsession.utilities.*
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
@ -139,7 +139,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
val userDB = DatabaseFactory.getLokiUserDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this)
if (userPublicKey != null) { if (userPublicKey != null) {
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB) MentionsManager.configureIfNeeded(userPublicKey, userDB)
application.publicChatManager.startPollersIfNeeded() application.publicChatManager.startPollersIfNeeded()
JobQueue.shared.resumePendingJobs() JobQueue.shared.resumePendingJobs()
} }
@ -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

@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
@ -45,7 +45,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
private var restoreJob: Job? = null private var restoreJob: Job? = null
override fun onBackPressed() { override fun onBackPressed() {
if (restoreJob?.isActive == true) return // don't allow going back with pending job if (restoreJob?.isActive == true) return // Don't allow going back with a pending job
super.onBackPressed() super.onBackPressed()
} }
@ -53,14 +53,12 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@LinkDeviceActivity, true) setHasViewedSeed(this@LinkDeviceActivity, true)
setConfigurationMessageSynced(this@LinkDeviceActivity, false) setConfigurationMessageSynced(this@LinkDeviceActivity, false)
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@LinkDeviceActivity, 0) setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
} }
// registration variables are synced
setContentView(R.layout.activity_link_device) setContentView(R.layout.activity_link_device)
viewPager.adapter = adapter viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) tabLayout.setupWithViewPager(viewPager)

View File

@ -19,12 +19,12 @@ import android.widget.Toast
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import kotlinx.android.synthetic.main.activity_path.* import kotlinx.android.synthetic.main.activity_path.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.GlowViewUtilities import org.thoughtcrime.securesms.loki.views.GlowViewUtilities
import org.thoughtcrime.securesms.loki.views.PathDotView import org.thoughtcrime.securesms.loki.views.PathDotView
import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
class PathActivity : PassphraseRequiredActionBarActivity() { class PathActivity : PassphraseRequiredActionBarActivity() {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>() private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.KeyHelper import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
@ -30,14 +30,12 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true) setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true)
setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false) setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false)
setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis())
} }
// registration variables are synced
setContentView(R.layout.activity_recovery_phrase_restore) setContentView(R.layout.activity_recovery_phrase_restore)
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }

View File

@ -39,14 +39,12 @@ class RegisterActivity : BaseActionBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register) setContentView(R.layout.activity_register)
setUpActionBarSessionLogo() setUpActionBarSessionLogo()
// Set the registration sync variables
TextSecurePreferences.apply { TextSecurePreferences.apply {
setHasViewedSeed(this@RegisterActivity, false) setHasViewedSeed(this@RegisterActivity, false)
setConfigurationMessageSynced(this@RegisterActivity, true) setConfigurationMessageSynced(this@RegisterActivity, true)
setRestorationTime(this@RegisterActivity, 0) setRestorationTime(this@RegisterActivity, 0)
setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis())
} }
// registration variables are synced
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
copyButton.setOnClickListener { copyPublicKey() } copyButton.setOnClickListener { copyPublicKey() }
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy")

View File

@ -16,7 +16,7 @@ import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
class SeedActivity : BaseActionBarActivity() { class SeedActivity : BaseActionBarActivity() {

View File

@ -36,6 +36,27 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli
isSelected) isSelected)
} }
override fun onBindViewHolder(viewHolder: ViewHolder,
position: Int,
payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
// Because these updates can be batched,
// there can be multiple payloads for a single bind
when (payloads[0]) {
Payload.MEMBER_CLICKED -> {
val member = members[position]
val isSelected = selectedMembers.contains(member)
viewHolder.view.toggleCheckbox(isSelected)
}
}
} else {
// When payload list is empty,
// or we don't have logic to handle a given type,
// default to full bind:
this.onBindViewHolder(viewHolder, position)
}
}
private fun onMemberClick(member: String) { private fun onMemberClick(member: String) {
if (selectedMembers.contains(member)) { if (selectedMembers.contains(member)) {
selectedMembers.remove(member) selectedMembers.remove(member)
@ -43,6 +64,11 @@ class SelectContactsAdapter(private val context: Context, private val glide: Gli
selectedMembers.add(member) selectedMembers.add(member)
} }
val index = members.indexOf(member) val index = members.indexOf(member)
notifyItemChanged(index) notifyItemChanged(index, Payload.MEMBER_CLICKED)
}
// define below the different events used to notify the adapter
enum class Payload {
MEMBER_CLICKED
} }
} }

View File

@ -28,13 +28,13 @@ import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.alwaysUi
import org.session.libsession.messaging.avatars.AvatarHelper import org.session.libsession.messaging.avatars.AvatarHelper
import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.file_server.FileServerAPI
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
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.session.libsignal.service.api.util.StreamDetails import org.session.libsignal.service.api.util.StreamDetails
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection

View File

@ -8,13 +8,16 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.opengroups.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.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.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.api.SnodeAPI
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
@ -70,7 +73,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
// Private chats // Private chats
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes -> val privateChatsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
envelopes.map { envelope -> envelopes.map { envelope ->
MessageReceiveJob(envelope.toByteArray(), false).executeAsync() MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
} }
@ -78,7 +81,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
promises.addAll(privateChatsPromise.get()) promises.addAll(privateChatsPromise.get())
// Closed groups // Closed groups
promises.addAll(ApplicationContext.getInstance(context).closedGroupPoller.pollOnce()) promises.addAll(ClosedGroupPoller().pollOnce())
// Open Groups // Open Groups
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { (_,chat)-> val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { (_,chat)->
@ -89,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

@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.loki.api
import android.content.Context import android.content.Context
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.* import okhttp3.*
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.api.PushNotificationAPI
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
import org.session.libsignal.service.loki.utilities.retryIfNeeded import org.session.libsignal.service.loki.utilities.retryIfNeeded
object LokiPushNotificationManager { object LokiPushNotificationManager {
@ -16,10 +16,10 @@ object LokiPushNotificationManager {
private val tokenExpirationInterval = 12 * 60 * 60 * 1000 private val tokenExpirationInterval = 12 * 60 * 60 * 1000
private val server by lazy { private val server by lazy {
PushNotificationAPI.shared.server PushNotificationAPI.server
} }
private val pnServerPublicKey by lazy { private val pnServerPublicKey by lazy {
PushNotificationAPI.pnServerPublicKey PushNotificationAPI.serverPublicKey
} }
enum class ClosedGroupOperation { enum class ClosedGroupOperation {

View File

@ -2,9 +2,9 @@ package org.thoughtcrime.securesms.loki.api
import android.content.Context import android.content.Context
import androidx.work.* import androidx.work.*
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.session.libsignal.service.loki.api.opengroups.PublicChat
/** /**
* Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager. * Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager.
@ -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,8 +58,12 @@ 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 = PublicChat.getId(channel, serverUrl) val isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
if (!isOpenGroupV2) {
val publicChatId = OpenGroup.getId(channel, serverUrl)
return try { return try {
Log.v(TAG, "Updating open group info for $publicChatId.") Log.v(TAG, "Updating open group info for $publicChatId.")
@ -51,5 +74,19 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
Log.e(TAG, "Failed to update open group info for $publicChatId", e) Log.e(TAG, "Failed to update open group info for $publicChatId", e)
Result.failure() 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()
}
}
} }
} }

View File

@ -5,14 +5,12 @@ import android.database.ContentObserver
import android.graphics.Bitmap 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.MessagingConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.open_groups.*
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.opengroups.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.session.libsignal.service.loki.api.opengroups.PublicChat
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
@ -21,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
} }
@ -53,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
} }
@ -75,7 +86,7 @@ class PublicChatManager(private val context: Context) {
@WorkerThread @WorkerThread
public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup { public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup {
val chat = PublicChat(channel, server, info.displayName, true) val chat = OpenGroup(channel, server, info.displayName, true)
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context) var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
var profilePicture: Bitmap? = null var profilePicture: Bitmap? = null
// Create the group if we don't have one // Create the group if we don't have one
@ -96,12 +107,42 @@ class PublicChatManager(private val context: Context) {
// Start polling // Start polling
Util.runOnMain { startPollersIfNeeded() } Util.runOnMain { startPollersIfNeeded() }
return OpenGroup.from(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 = PublicChat.getId(channel, server) val groupId = OpenGroup.getId(channel, server)
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
GroupManager.deleteGroup(groupAddress, context)
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 threadId = GroupManager.getOpenGroupThreadID(groupId, context)
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize() val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
GroupManager.deleteGroup(groupAddress, context) GroupManager.deleteGroup(groupAddress, context)
@ -110,13 +151,15 @@ class PublicChatManager(private val context: Context) {
} }
private fun refreshChatsAndPollers() { private fun refreshChatsAndPollers() {
val storage = MessagingConfiguration.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) {
@ -133,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

@ -6,8 +6,8 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.api.MessageWrapper
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 org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationChannels

View File

@ -7,8 +7,8 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.loki.api.Snode import org.session.libsignal.service.loki.Snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation import org.session.libsignal.service.loki.utilities.PublicKeyValidation
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
@ -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

@ -2,15 +2,11 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import org.session.libsession.messaging.threads.Address
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.session.libsignal.service.loki.LokiMessageDatabaseProtocol
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.session.libsignal.service.loki.database.LokiMessageDatabaseProtocol
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
@ -23,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)
} }
} }
@ -82,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

@ -3,22 +3,20 @@ package org.thoughtcrime.securesms.loki.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroup
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.* 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.service.loki.api.opengroups.PublicChat
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.database.LokiThreadDatabaseProtocol
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol { class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object { companion object {
private val sessionResetTable = "loki_thread_session_reset_database" private val sessionResetTable = "loki_thread_session_reset_database"
@ -27,27 +25,31 @@ 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);"
} }
override fun getThreadID(hexEncodedPublicKey: String): Long { fun getThreadID(hexEncodedPublicKey: String): Long {
val address = Address.fromSerialized(hexEncodedPublicKey) val address = Address.fromSerialized(hexEncodedPublicKey)
val recipient = Recipient.from(context, address, false) val recipient = Recipient.from(context, address, false)
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
} }
fun getAllPublicChats(): Map<Long, PublicChat> { fun getAllPublicChats(): Map<Long, OpenGroup> {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
var cursor: Cursor? = null var cursor: Cursor? = null
val result = mutableMapOf<Long, PublicChat>() val result = mutableMapOf<Long, OpenGroup>()
try { try {
cursor = database.rawQuery("select * from $publicChatTable", null) cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID) val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat) val string = cursor.getString(publicChat)
val publicChat = PublicChat.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
@ -57,29 +59,74 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return result 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()
}
return result
}
fun getAllPublicChatServers(): Set<String> { fun getAllPublicChatServers(): Set<String> {
return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) }
} }
override fun getPublicChat(threadID: Long): PublicChat? { 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)
PublicChat.fromJSON(publicChatAsJSON) OpenGroup.fromJSON(publicChatAsJSON)
} }
} }
override fun setPublicChat(publicChat: PublicChat, threadID: Long) { fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
if (threadID < 0) { return } 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) {
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()))
} }
override 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

@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
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.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol import org.session.libsignal.service.loki.LokiUserDatabaseProtocol
class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol { class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol {

View File

@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.dialog_seed.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.IdentityKeyUtil import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.session.libsignal.service.loki.crypto.MnemonicCodec import org.session.libsignal.service.loki.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey

View File

@ -9,7 +9,6 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
@ -32,6 +31,7 @@ import org.session.libsession.utilities.TextSecurePreferences
import java.util.* import java.util.*
object ClosedGroupsProtocolV2 { object ClosedGroupsProtocolV2 {
@JvmStatic @JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return } if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
@ -41,7 +41,6 @@ object ClosedGroupsProtocolV2 {
DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey) DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> handleClosedGroupMemberLeft(context, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.UPDATE -> handleClosedGroupUpdate(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey) DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR -> handleGroupEncryptionKeyPair(context, closedGroupUpdate, groupPublicKey, senderPublicKey)
else -> { else -> {
Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}") Log.d("Loki","Can't handle closed group update of unknown type: ${closedGroupUpdate.type}")
@ -65,7 +64,6 @@ object ClosedGroupsProtocolV2 {
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> { DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
senderPublicKey.isNotEmpty() senderPublicKey.isNotEmpty()
} }
DataMessage.ClosedGroupControlMessage.Type.UPDATE,
DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> { DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE -> {
!closedGroupUpdate.name.isNullOrEmpty() !closedGroupUpdate.name.isNullOrEmpty()
} }
@ -104,11 +102,11 @@ object ClosedGroupsProtocolV2 {
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user (if we didn't make the group) // Notify the user (if we didn't make the group)
if (userPublicKey != senderPublicKey) { if (userPublicKey != senderPublicKey) {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
} else if (prevGroup == null) { } else if (prevGroup == null) {
// only notify if we created this group // only notify if we created this group
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} }
// Notify the PN server // Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
@ -156,14 +154,14 @@ object ClosedGroupsProtocolV2 {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers) MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
} }
} }
val (contextType, signalType) = val type =
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT if (senderLeft) SignalServiceGroup.Type.QUIT
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE else SignalServiceGroup.Type.UPDATE
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else { } else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
} }
} }
@ -191,9 +189,9 @@ object ClosedGroupsProtocolV2 {
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else { } else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
} }
if (userPublicKey in admins) { if (userPublicKey in admins) {
// send current encryption key to the latest added members // send current encryption key to the latest added members
@ -230,9 +228,9 @@ object ClosedGroupsProtocolV2 {
// Notify the user // Notify the user
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else { } else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
} }
} }
@ -272,9 +270,9 @@ object ClosedGroupsProtocolV2 {
// Notify user // Notify user
if (userLeft) { if (userLeft) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, sentTimestamp)
} else { } else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
} }
} }
@ -385,14 +383,13 @@ object ClosedGroupsProtocolV2 {
} }
// Notify the user // Notify the user
val wasSenderRemoved = !members.contains(senderPublicKey) val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
val admins = group.admins.map { it.toString() } val admins = group.admins.map { it.toString() }
if (userPublicKey == senderPublicKey) { if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp) DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type, name, members, admins, threadID, sentTimestamp)
} else { } else {
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp) DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type, name, members, admins, sentTimestamp)
} }
} }
// endregion // endregion

View File

@ -2,24 +2,13 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
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.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.preferences.ProfileKeyUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
object MultiDeviceProtocol { object MultiDeviceProtocol {
@ -28,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 ->
@ -51,80 +40,4 @@ object MultiDeviceProtocol {
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis()) TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
} }
// TODO: remove this after we migrate to new message receiving pipeline
@JvmStatic
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
synchronized(this) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, timestamp)) return
if (senderPublicKey != userPublicKey) return
TextSecurePreferences.setConfigurationMessageSynced(context, true)
TextSecurePreferences.setLastProfileUpdateTime(context, timestamp)
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
val storage = MessagingConfiguration.shared.storage
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
val ourRecipient = Recipient.from(context, Address.fromSerialized(userPublicKey),false)
for (closedGroup in configurationMessage.closedGroups) {
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
closedGroupUpdate.name = closedGroup.name
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair!!.privateKey.serialize())
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in configurationMessage.openGroups) {
if (allOpenGroups.contains(openGroup)) continue
OpenGroupUtilities.addGroup(context, openGroup, 1)
}
if (configurationMessage.displayName.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, configurationMessage.displayName)
recipientDatabase.setProfileName(ourRecipient, configurationMessage.displayName)
}
if (configurationMessage.profileKey.isNotEmpty()) {
val profileKey = Base64.encodeBytes(configurationMessage.profileKey)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
recipientDatabase.setProfileKey(ourRecipient, configurationMessage.profileKey)
if (!configurationMessage.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != configurationMessage.profilePicture) {
TextSecurePreferences.setProfilePictureURL(context, configurationMessage.profilePicture)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, configurationMessage.profilePicture))
}
}
for (contact in configurationMessage.contacts) {
val address = Address.fromSerialized(contact.publicKey)
val recipient = Recipient.from(context, address, true)
if (!contact.profilePicture.isNullOrEmpty()) {
recipientDatabase.setProfileAvatar(recipient, contact.profilePicture)
}
if (contact.profileKey?.isNotEmpty() == true) {
recipientDatabase.setProfileKey(recipient, contact.profileKey)
}
if (contact.name.isNotEmpty()) {
recipientDatabase.setProfileName(recipient, contact.name)
}
recipientDatabase.setProfileSharing(recipient, true)
recipientDatabase.setRegistered(recipient, Recipient.RegisteredState.REGISTERED)
// create Thread if needed
threadDatabase.getOrCreateThreadIdFor(recipient)
}
if (configurationMessage.contacts.isNotEmpty()) {
threadDatabase.notifyUpdatedFromConfig()
}
}
}
} }

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

@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.loki.utilities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.session.libsignal.service.loki.Broadcaster
class Broadcaster(private val context: Context) : org.session.libsignal.service.loki.utilities.Broadcaster { class Broadcaster(private val context: Context) : Broadcaster {
override fun broadcast(event: String) { override fun broadcast(event: String) {
val intent = Intent(event) val intent = Intent(event)

View File

@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.loki.utilities
import android.content.Context
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.service.internal.push.SignalServiceProtos
import java.util.*
import network.loki.messenger.R
import org.session.libsignal.utilities.logging.Log
import java.io.IOException
class GroupDescription(context: Context, groupContext: SignalServiceProtos.GroupContext?) {
private val context: Context
private val groupContext: SignalServiceProtos.GroupContext?
private val newMembers: MutableList<Recipient>
private val removedMembers: MutableList<Recipient>
private var wasCurrentUserRemoved: Boolean = false
private fun toRecipient(hexEncodedPublicKey: String): Recipient {
val address = Address.fromSerialized(hexEncodedPublicKey)
return Recipient.from(context, address, false)
}
fun toString(sender: Recipient): String {
if (wasCurrentUserRemoved) {
return context.getString(R.string.GroupUtil_you_were_removed_from_group)
}
val description = StringBuilder()
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString()))
if (groupContext == null) {
return description.toString()
}
val title = groupContext.name
if (!newMembers.isEmpty()) {
description.append("\n")
description.append(context.resources.getQuantityString(R.plurals.GroupUtil_joined_the_group,
newMembers.size, toString(newMembers)))
}
if (!removedMembers.isEmpty()) {
description.append("\n")
description.append(context.resources.getQuantityString(R.plurals.GroupUtil_removed_from_the_group,
removedMembers.size, toString(removedMembers)))
}
if (title != null && !title.trim { it <= ' ' }.isEmpty()) {
val separator = if (!newMembers.isEmpty() || !removedMembers.isEmpty()) " " else "\n"
description.append(separator)
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title))
}
return description.toString()
}
fun addListener(listener: RecipientModifiedListener?) {
if (!newMembers.isEmpty()) {
for (member in newMembers) {
member.addListener(listener)
}
}
}
private fun toString(recipients: List<Recipient>): String {
var result = ""
for (i in recipients.indices) {
result += recipients[i].toShortString()
if (i != recipients.size - 1) result += ", "
}
return result
}
init {
this.context = context.applicationContext
this.groupContext = groupContext
newMembers = LinkedList()
removedMembers = LinkedList()
if (groupContext != null) {
val newMembers = groupContext.newMembersList
for (member in newMembers) {
this.newMembers.add(toRecipient(member))
}
val removedMembers = groupContext.removedMembersList
for (member in removedMembers) {
this.removedMembers.add(toRecipient(member))
}
wasCurrentUserRemoved = removedMembers.contains(TextSecurePreferences.getLocalNumber(context))
}
}
companion object {
fun getDescription(context: Context, encodedGroup: String?): GroupDescription {
return if (encodedGroup == null) {
GroupDescription(context, null)
} else try {
val groupContext = SignalServiceProtos.GroupContext.parseFrom(Base64.decode(encodedGroup))
GroupDescription(context, groupContext)
} catch (e: IOException) {
Log.w("Loki", e)
GroupDescription(context, null)
}
}
}
}

View File

@ -7,7 +7,7 @@ import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import com.opencsv.CSVReader import com.opencsv.CSVReader
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream

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.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
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
object MentionManagerUtilities { object MentionManagerUtilities {

View File

@ -1,10 +1,6 @@
package org.thoughtcrime.securesms.loki.utilities package org.thoughtcrime.securesms.loki.utilities
import android.content.Context import android.content.Context
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
object MnemonicUtilities { object MnemonicUtilities {

View File

@ -3,30 +3,56 @@ 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.opengroups.OpenGroup import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.open_groups.OpenGroup
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.session.libsignal.service.loki.api.opengroups.PublicChat
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)
fun addGroup(context: Context, url: String, channel: Long): OpenGroup { fun addGroup(context: Context, url: String, channel: Long): OpenGroup {
// Check for an existing group. // Check for an existing group.
val groupID = PublicChat.getId(channel, url) val groupID = OpenGroup.getId(channel, url)
val threadID = GroupManager.getOpenGroupThreadID(groupID, context) val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
if (openGroup != null) { return OpenGroup.from(openGroup) } if (openGroup != null) { return openGroup }
// Add the new group. // Add the new group.
val application = ApplicationContext.getInstance(context) val application = ApplicationContext.getInstance(context)
@ -56,7 +82,7 @@ object OpenGroupUtilities {
@Throws(Exception::class) @Throws(Exception::class)
fun updateGroupInfo(context: Context, url: String, channel: Long) { fun updateGroupInfo(context: Context, url: String, channel: Long) {
// Check if open group has a related DB record. // Check if open group has a related DB record.
val groupId = GroupUtil.getEncodedOpenGroupID(PublicChat.getId(channel, url).toByteArray()) val groupId = GroupUtil.getEncodedOpenGroupID(OpenGroup.getId(channel, url).toByteArray())
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) { if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId") throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
} }
@ -68,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

@ -10,7 +10,7 @@ import android.widget.ListView
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.session.libsignal.service.loki.utilities.mentions.Mention import org.session.libsignal.service.loki.Mention
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var mentionCandidates = listOf<Mention>() private var mentionCandidates = listOf<Mention>()

View File

@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.* import kotlinx.android.synthetic.main.view_mention_candidate.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.opengroups.OpenGroupAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsignal.service.loki.utilities.mentions.Mention import org.session.libsignal.service.loki.Mention
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {

View File

@ -11,9 +11,9 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.snode.OnionRequestAPI
import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI
class PathStatusView : View { class PathStatusView : View {
private val broadcastReceivers = mutableListOf<BroadcastReceiver>() private val broadcastReceivers = mutableListOf<BroadcastReceiver>()

View File

@ -11,10 +11,10 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.view_profile_picture.view.* import kotlinx.android.synthetic.main.view_profile_picture.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.avatars.ProfileContactPhoto import org.session.libsession.messaging.avatars.ProfileContactPhoto
import org.session.libsession.messaging.mentions.MentionsManager
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.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator import org.thoughtcrime.securesms.loki.utilities.AvatarPlaceholderGenerator
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -146,13 +146,13 @@ class ProfilePictureView : RelativeLayout {
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
if (publicKey.isNotEmpty()) { if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return if (imagesCached.contains(publicKey)) return
val signalProfilePicture = recipient.contactPhoto val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.clear(imageView) glide.clear(imageView)
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
imagesCached.add(recipient.profileAvatar.orEmpty()) imagesCached.add(publicKey)
} else { } else {
val sizeInPX = resources.getDimensionPixelSize(sizeResId) val sizeInPX = resources.getDimensionPixelSize(sizeResId)
glide.clear(imageView) glide.clear(imageView)
@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout {
publicKey, publicKey,
displayName displayName
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
imagesCached.add(recipient.profileAvatar.orEmpty()) imagesCached.add(publicKey)
} }
} else { } else {
imageView.setImageDrawable(null) imageView.setImageDrawable(null)

View File

@ -82,6 +82,11 @@ class UserView : LinearLayout {
} }
} }
fun toggleCheckbox(isSelected: Boolean = false) {
actionIndicatorImageView.visibility = View.VISIBLE
actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
}
fun unbind() { fun unbind() {
} }

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI; import org.session.libsession.messaging.file_server.FileServerAPI;
public class PushMediaConstraints extends MediaConstraints { public class PushMediaConstraints extends MediaConstraints {

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

@ -2,22 +2,17 @@ package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import com.google.protobuf.ByteString;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate; import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.Address; 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.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceGroup; import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.internal.push.SignalServiceProtos;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -28,7 +23,6 @@ import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@ -79,8 +73,8 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
String senderPublicKey = message.getSender(); String senderPublicKey = message.getSender();
// Notify the user // Notify the user
if (userPublicKey.equals(senderPublicKey)) { if (senderPublicKey == null || userPublicKey.equals(senderPublicKey)) {
// sender is a linked device // sender is self or a linked device
insertOutgoingExpirationTimerMessage(message); insertOutgoingExpirationTimerMessage(message);
} else { } else {
insertIncomingExpirationTimerMessage(message); insertIncomingExpirationTimerMessage(message);
@ -100,7 +94,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
int duration = message.getDuration(); int duration = message.getDuration();
Optional<SignalServiceGroup> groupInfo = Optional.absent(); Optional<SignalServiceGroup> groupInfo = Optional.absent();
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); Address address = Address.fromSerialized(senderPublicKey);
Recipient recipient = Recipient.from(context, address, false); Recipient recipient = Recipient.from(context, address, false);
// if the sender is blocked, we don't display the update, except if it's in a closed group // if the sender is blocked, we don't display the update, except if it's in a closed group
@ -123,6 +117,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.absent(), Optional.absent(),
Optional.absent(),
Optional.absent()); Optional.absent());
//insert the timer update message //insert the timer update message
database.insertSecureDecryptedMessageInbox(mediaMessage, -1); database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
@ -138,41 +133,21 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) { private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
String senderPublicKey = message.getSender();
Long sentTimestamp = message.getSentTimestamp(); Long sentTimestamp = message.getSentTimestamp();
String groupId = message.getGroupPublicKey(); String groupId = message.getGroupPublicKey();
int duration = message.getDuration(); int duration = message.getDuration();
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : message.getRecipient());
Recipient recipient = Recipient.from(context, address, false); Recipient recipient = Recipient.from(context, address, false);
try { try {
OutgoingExpirationUpdateMessage timerUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, sentTimestamp, duration * 1000L, groupId);
database.insertSecureDecryptedMessageOutbox(timerUpdateMessage, -1, sentTimestamp);
if (groupId != null) { if (groupId != null) {
// conversation is a closed group
GroupContext groupContext = SignalServiceProtos.GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId))).build();
OutgoingGroupMediaMessage infoMessage = new OutgoingGroupMediaMessage(recipient, groupContext, null, sentTimestamp, duration * 1000L, true, null, Collections.emptyList(), Collections.emptyList());
database.insertSecureDecryptedMessageOutbox(infoMessage, -1, sentTimestamp);
// we need the group ID as recipient for setExpireMessages below // we need the group ID as recipient for setExpireMessages below
recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false); recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false);
} else {
// conversation is a 1-1
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
null,
Collections.emptyList(),
message.getSentTimestamp(),
-1,
duration * 1000L,
true,
DistributionTypes.DEFAULT,
null,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList());
database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp);
} }
//set the timer to the conversation //set the timer to the conversation
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration); DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);

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,4 +0,0 @@
package org.thoughtcrime.securesms.sskenvironment
class DataExtractionNotificationManager {
}

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

@ -98,13 +98,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/home_activity_gradient" /> android:background="@drawable/home_activity_gradient" />
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
<LinearLayout <LinearLayout
android:id="@+id/emptyStateContainer" android:id="@+id/emptyStateContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -131,6 +124,13 @@
</LinearLayout> </LinearLayout>
<org.thoughtcrime.securesms.loki.views.NewConversationButtonSetView
android:id="@+id/newConversationButtonSet"
android:layout_width="276dp"
android:layout_height="236dp"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>

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

@ -418,6 +418,16 @@
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Vous avez reçu un message chiffré avec une ancienne version de Signal qui nest plus prise en charge. Veuillez demander à lexpéditeur de mettre Session à jour vers la version la plus récente et de renvoyer son message.</string> <string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Vous avez reçu un message chiffré avec une ancienne version de Signal qui nest plus prise en charge. Veuillez demander à lexpéditeur de mettre Session à jour vers la version la plus récente et de renvoyer son message.</string>
<string name="MessageRecord_left_group">Vous avez quitté le groupe</string> <string name="MessageRecord_left_group">Vous avez quitté le groupe</string>
<string name="MessageRecord_you_updated_group">Vous avez mis le groupe à jour.</string> <string name="MessageRecord_you_updated_group">Vous avez mis le groupe à jour.</string>
<string name="MessageRecord_you_created_a_new_group">Vous avez créée un nouveau groupe.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté au groupe.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en \'%1$s\'</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en \'%2$s\'</string>
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez supprimé %1$s du groupe.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été supprimé du groupe.</string>
<string name="MessageRecord_you">Vous</string>
<string name="MessageRecord_you_called">Vous avez appelé</string> <string name="MessageRecord_you_called">Vous avez appelé</string>
<string name="MessageRecord_called_you">Contact appelé</string> <string name="MessageRecord_called_you">Contact appelé</string>
<string name="MessageRecord_missed_call">Appel manqué</string> <string name="MessageRecord_missed_call">Appel manqué</string>
@ -430,6 +440,8 @@
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string> <string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s.</string> <string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini lexpiration des messages éphémères à %1$s.</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s.</string> <string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini lexpiration des messages éphémères à %2$s.</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture décran.</string>
<string name="MessageRecord_media_saved_by_s">%1$s a sauvegardé un media.</string>
<string name="MessageRecord_your_safety_number_with_s_has_changed">Votre numéro de sécurité avec %s a changé.</string> <string name="MessageRecord_your_safety_number_with_s_has_changed">Votre numéro de sécurité avec %s a changé.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">Vous avez marqué votre numéro de sécurité avec %s comme vérifié</string> <string name="MessageRecord_you_marked_your_safety_number_with_s_verified">Vous avez marqué votre numéro de sécurité avec %s comme vérifié</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">Vous avez marqué votre numéro de sécurité avec %s comme vérifié à partir dun autre appareil</string> <string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">Vous avez marqué votre numéro de sécurité avec %s comme vérifié à partir dun autre appareil</string>
@ -1483,4 +1495,35 @@ Vous avez reçu un message déchange de clés pour une version de protocole i
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string> <string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string> <string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
<!-- Next round of translation -->
<string name="menu_apply_button">Appliquer</string>
<string name="menu_done_button">Terminé</string>
<string name="activity_edit_closed_group_title">Modifier le groupe</string>
<string name="activity_edit_closed_group_edit_text_hint">Saisissez un nouveau nom de groupe</string>
<string name="activity_edit_closed_group_edit_members">Membres</string>
<string name="activity_edit_closed_group_add_members">Ajouter des membres</string>
<string name="activity_edit_closed_group_group_name_missing_error">Le nom du groupe ne peut pas être vide</string>
<string name="activity_edit_closed_group_group_name_too_long_error">Veuillez saisir un nom plus court</string>
<string name="activity_edit_closed_group_not_enough_group_members_error">Les groupes doivent avoir au moins un membre</string>
<string name="activity_edit_closed_group_invalid_session_id_error">Un des membres du group a un Session ID invalide</string>
<string name="activity_edit_closed_group_confirm_removal">Êtes vous sûr de vouloir suprimer ce membre du groupe?</string>
<string name="activity_edit_closed_group_member_removed">Membre supprimé du groupe</string>
<string name="fragment_edit_group_bottom_sheet_remove">Supprimer le membre du groupe</string>
<string name="activity_select_contacts_title">Contacts</string>
<string name="dialog_ui_mode_title">Thème</string>
<string name="dialog_ui_mode_option_day">Jour</string>
<string name="dialog_ui_mode_option_night">Nuit</string>
<string name="dialog_ui_mode_option_system_default">Système</string>
<string name="activity_conversation_menu_copy_session_id">Copier le Session ID</string>
<string name="attachment">Pièce jointe</string>
<string name="attachment_type_voice_message">Message vocal</string>
<string name="details">Détails</string>
</resources> </resources>

View File

@ -493,6 +493,16 @@
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string> <string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Session that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string>
<string name="MessageRecord_left_group">You have left the group.</string> <string name="MessageRecord_left_group">You have left the group.</string>
<string name="MessageRecord_you_updated_group">You updated the group.</string> <string name="MessageRecord_you_updated_group">You updated the group.</string>
<string name="MessageRecord_you_created_a_new_group">You created a new group.</string>
<string name="MessageRecord_s_added_you_to_the_group">%1$s added you to the group.</string>
<string name="MessageRecord_you_renamed_the_group_to_s">You renamed the group to \'%1$s\'</string>
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s renamed the group to \'%2$s\'</string>
<string name="MessageRecord_you_added_s_to_the_group">You added %1$s to the group.</string>
<string name="MessageRecord_s_added_s_to_the_group">%1$s added %2$s to the group.</string>
<string name="MessageRecord_you_removed_s_from_the_group">You removed %1$s from the group.</string>
<string name="MessageRecord_s_removed_s_from_the_group">%1$s removed %2$s from the group.</string>
<string name="MessageRecord_you_were_removed_from_the_group">You were removed from the group.</string>
<string name="MessageRecord_you">You</string>
<string name="MessageRecord_you_called">You called</string> <string name="MessageRecord_you_called">You called</string>
<string name="MessageRecord_called_you">Contact called</string> <string name="MessageRecord_called_you">Contact called</string>
<string name="MessageRecord_missed_call">Missed call</string> <string name="MessageRecord_missed_call">Missed call</string>
@ -505,6 +515,8 @@
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string> <string name="MessageRecord_s_disabled_disappearing_messages">%1$s disabled disappearing messages.</string>
<string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string> <string name="MessageRecord_you_set_disappearing_message_time_to_s">You set the disappearing message timer to %1$s</string>
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string> <string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s set the disappearing message timer to %2$s</string>
<string name="MessageRecord_s_took_a_screenshot">%1$s took a screenshot.</string>
<string name="MessageRecord_media_saved_by_s">Media saved by %1$s.</string>
<string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string> <string name="MessageRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string> <string name="MessageRecord_you_marked_your_safety_number_with_s_verified">You marked your safety number with %s verified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string> <string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>
@ -703,6 +715,8 @@
<string name="ThreadRecord_s_is_on_signal">%s is on Session!</string> <string name="ThreadRecord_s_is_on_signal">%s is on Session!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string> <string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string> <string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
<string name="ThreadRecord_s_took_a_screenshot">%s took a screenshot.</string>
<string name="ThreadRecord_media_saved_by_s">Media saved by %s.</string>
<string name="ThreadRecord_safety_number_changed">Safety number changed</string> <string name="ThreadRecord_safety_number_changed">Safety number changed</string>
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string> <string name="ThreadRecord_your_safety_number_with_s_has_changed">Your safety number with %s has changed.</string>
<string name="ThreadRecord_you_marked_verified">You marked verified</string> <string name="ThreadRecord_you_marked_verified">You marked verified</string>
@ -1884,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

@ -1,5 +1,6 @@
package org.session.libsession.database package org.session.libsession.database
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
@ -10,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?
@ -30,7 +32,6 @@ interface MessageDataProvider {
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
fun updateAttachmentAfterUploadFailed(attachmentId: Long) fun updateAttachmentAfterUploadFailed(attachmentId: Long)
// Quotes
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
fun getMessageBodyFor(timestamp: Long, author: String): String fun getMessageBodyFor(timestamp: Long, author: String): String
@ -38,4 +39,5 @@ interface MessageDataProvider {
fun getAttachmentIDsFor(messageID: Long): List<Long> fun getAttachmentIDsFor(messageID: Long): List<Long>
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
fun getOpenGroup(threadID: Long): OpenGroup?
} }

View File

@ -1,25 +0,0 @@
package org.session.libsession.messaging
import android.content.Context
import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
class MessagingConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol)
{
companion object {
lateinit var shared: MessagingConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
sessionProtocol: SessionProtocol
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingConfiguration(context, storage, messageDataProvider, sessionProtocol)
}
}
}

View File

@ -0,0 +1,26 @@
package org.session.libsession.messaging
import android.content.Context
import org.session.libsession.database.MessageDataProvider
import org.session.libsignal.service.loki.api.crypto.SessionProtocol
class MessagingModuleConfiguration(
val context: Context,
val storage: StorageProtocol,
val messageDataProvider: MessageDataProvider,
val sessionProtocol: SessionProtocol)
{
companion object {
lateinit var shared: MessagingModuleConfiguration
fun configure(context: Context,
storage: StorageProtocol,
messageDataProvider: MessageDataProvider,
sessionProtocol: SessionProtocol
) {
if (Companion::shared.isInitialized) { return }
shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol)
}
}
}

View File

@ -9,10 +9,12 @@ import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage 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.opengroups.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.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview 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.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord import org.session.libsession.messaging.threads.GroupRecord
@ -20,7 +22,6 @@ import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSe
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
interface StorageProtocol { interface StorageProtocol {
@ -34,6 +35,7 @@ interface StorageProtocol {
fun setUserProfilePictureUrl(newProfilePicture: String) fun setUserProfilePictureUrl(newProfilePicture: String)
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
// Signal Protocol // Signal Protocol
@ -51,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
@ -68,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>
@ -107,8 +110,10 @@ interface StorageProtocol {
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long) fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
fun isGroupActive(groupPublicKey: String): Boolean fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean) fun setActive(groupID: String, value: Boolean)
fun getZombieMember(groupID: String): Set<String>
fun removeMember(groupID: String, member: Address) fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>) fun updateMembers(groupID: String, members: List<Address>)
fun updateZombieMembers(groupID: String, members: List<Address>)
// Closed Group // Closed Group
fun getAllClosedGroupPublicKeys(): Set<String> fun getAllClosedGroupPublicKeys(): Set<String>
fun getAllActiveClosedGroupPublicKeys(): Set<String> fun getAllActiveClosedGroupPublicKeys(): Set<String>
@ -116,9 +121,9 @@ interface StorageProtocol {
fun removeClosedGroupPublicKey(groupPublicKey: String) fun removeClosedGroupPublicKey(groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
fun isClosedGroup(publicKey: String): Boolean fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair> fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
@ -134,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?
@ -158,4 +164,31 @@ interface StorageProtocol {
// Message Handling // Message Handling
/// Returns the ID of the `TSIncomingMessage` that was constructed. /// Returns the ID of the `TSIncomingMessage` that was constructed.
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
// Data Extraction Notification
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

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars; package org.session.libsession.messaging.avatars;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars; package org.session.libsession.messaging.avatars;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
@ -19,5 +18,4 @@ public interface ContactPhoto extends Key {
@Nullable Uri getUri(@NonNull Context context); @Nullable Uri getUri(@NonNull Context context);
boolean isProfilePhoto(); boolean isProfilePhoto();
} }

View File

@ -7,5 +7,4 @@ public interface FallbackContactPhoto {
public Drawable asDrawable(Context context, int color); public Drawable asDrawable(Context context, int color);
public Drawable asDrawable(Context context, int color, boolean inverted); public Drawable asDrawable(Context context, int color, boolean inverted);
} }

View File

@ -19,7 +19,6 @@ import org.session.libsession.utilities.ViewUtil;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class GeneratedContactPhoto implements FallbackContactPhoto { public class GeneratedContactPhoto implements FallbackContactPhoto {
private static final Pattern PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+"); private static final Pattern PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+");

View File

@ -1,13 +1,12 @@
package org.session.libsession.messaging.avatars; package org.session.libsession.messaging.avatars;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.session.libsession.messaging.MessagingConfiguration; import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.StorageProtocol; import org.session.libsession.messaging.StorageProtocol;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.GroupRecord; import org.session.libsession.messaging.threads.GroupRecord;
@ -32,7 +31,7 @@ public class GroupRecordContactPhoto implements ContactPhoto {
@Override @Override
public InputStream openInputStream(Context context) throws IOException { public InputStream openInputStream(Context context) throws IOException {
StorageProtocol groupDatabase = MessagingConfiguration.shared.getStorage(); StorageProtocol groupDatabase = MessagingModuleConfiguration.shared.getStorage();
Optional<GroupRecord> groupRecord = Optional.of(groupDatabase.getGroup(address.toGroupString())); Optional<GroupRecord> groupRecord = Optional.of(groupDatabase.getGroup(address.toGroupString()));
if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) { if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) {

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars; package org.session.libsession.messaging.avatars;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.avatars; package org.session.libsession.messaging.avatars;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;

View File

@ -1,17 +1,16 @@
package org.session.libsession.messaging.fileserver package org.session.libsession.messaging.file_server
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Request import okhttp3.Request
import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.* import org.session.libsignal.service.loki.utilities.*
import java.net.URL import java.net.URL
import java.util.concurrent.ConcurrentHashMap
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() { class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {

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,23 +1,24 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingConfiguration import okhttp3.HttpUrl
import org.session.libsession.messaging.fileserver.FileServerAPI import org.session.libsession.messaging.MessagingModuleConfiguration
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
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
// Error // Error
internal sealed class Error(val description: String) : Exception(description) { internal sealed class Error(val description: String) : Exception(description) {
object NoAttachment : Error("No such attachment.") object NoAttachment : Error("No such attachment.")
@ -25,44 +26,53 @@ 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"
//keys used for database storage purpose // Keys used for database storage
private val KEY_ATTACHMENT_ID = "attachment_id" private val KEY_ATTACHMENT_ID = "attachment_id"
private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id" private val KEY_TS_INCOMING_MESSAGE_ID = "tsIncoming_message_id"
} }
override fun execute() { override fun execute() {
val handleFailure: (java.lang.Exception) -> Unit = { exception -> val handleFailure: (java.lang.Exception) -> Unit = { exception ->
if(exception is Error && exception == Error.NoAttachment) { if (exception == Error.NoAttachment) {
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else if (exception is DotNetAPI.Error && exception == DotNetAPI.Error.ParsingFailed) { } else if (exception == DotNetAPI.Error.ParsingFailed) {
// No need to retry if the response is invalid. Most likely this means we (incorrectly) // No need to retry if the response is invalid. Most likely this means we (incorrectly)
// got a "Cannot GET ..." error from the file server. // got a "Cannot GET ..." error from the file server.
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
this.handlePermanentFailure(exception) this.handlePermanentFailure(exception)
} else { } else {
this.handleFailure(exception) this.handleFailure(exception)
} }
} }
try { try {
val messageDataProvider = MessagingConfiguration.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, MAX_ATTACHMENT_SIZE, null) val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
// DECRYPTION
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 // 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) if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest) 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) {
@ -84,13 +94,11 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} }
private fun createTempFile(): File { private fun createTempFile(): File {
val file = File.createTempFile("push-attachment", "tmp", MessagingConfiguration.shared.context.cacheDir) val file = File.createTempFile("push-attachment", "tmp", MessagingModuleConfiguration.shared.context.cacheDir)
file.deleteOnExit() file.deleteOnExit()
return file return file
} }
//database functions
override fun serialize(): Data { override fun serialize(): Data {
return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID) return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID)
.putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID) .putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID)
@ -101,7 +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

@ -3,9 +3,10 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.fileserver.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
@ -14,11 +15,10 @@ import org.session.libsignal.service.internal.crypto.PaddingInputStream
import org.session.libsignal.service.internal.push.PushAttachmentData import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory import org.session.libsignal.service.loki.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : 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
@ -34,9 +34,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val TAG = AttachmentUploadJob::class.simpleName val TAG = AttachmentUploadJob::class.simpleName
val KEY: String = "AttachmentUploadJob" val KEY: String = "AttachmentUploadJob"
val maxFailureCount: Int = 20 // Keys used for database storage
//keys used for database storage purpose
private val KEY_ATTACHMENT_ID = "attachment_id" private val KEY_ATTACHMENT_ID = "attachment_id"
private val KEY_THREAD_ID = "thread_id" private val KEY_THREAD_ID = "thread_id"
private val KEY_MESSAGE = "message" private val KEY_MESSAGE = "message"
@ -45,17 +43,18 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
override fun execute() { override fun execute() {
try { try {
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID) val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment) ?: return handleFailure(Error.NoAttachment)
var server = FileServerAPI.shared.server
var shouldEncrypt = true
val usePadding = false val usePadding = false
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID) val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
openGroup?.let { val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
server = it.server val server = openGroup?.let {
shouldEncrypt = false 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
@ -65,11 +64,14 @@ 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 is Error && e == Error.NoAttachment) { if (e == Error.NoAttachment) {
this.handlePermanentFailure(e) this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) { } else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e) this.handlePermanentFailure(e)
@ -77,33 +79,32 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
this.handleFailure(e) this.handleFailure(e)
} }
} }
} }
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
Log.w(TAG, "Attachment uploaded successfully.") Log.w(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this) delegate?.handleJobSucceeded(this)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
} }
private fun handlePermanentFailure(e: Exception) { private fun handlePermanentFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.") Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e) delegate?.handleJobFailedPermanently(this, e)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID) MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
failAssociatedMessageSendJob(e) failAssociatedMessageSendJob(e)
} }
private fun handleFailure(e: Exception) { private fun handleFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed due to error: $this.") Log.w(TAG, "Attachment upload failed due to error: $this.")
delegate?.handleJobFailed(this, e) delegate?.handleJobFailed(this, e)
if (failureCount + 1 == AttachmentUploadJob.maxFailureCount) { if (failureCount + 1 == maxFailureCount) {
failAssociatedMessageSendJob(e) failAssociatedMessageSendJob(e)
} }
} }
private fun failAssociatedMessageSendJob(e: Exception) { private fun failAssociatedMessageSendJob(e: Exception) {
val storage = MessagingConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val messageSendJob = storage.getMessageSendJob(messageSendJobID) val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message, e) MessageSender.handleFailedMessageSend(this.message, e)
if (messageSendJob != null) { if (messageSendJob != null) {
@ -111,10 +112,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
} }
} }
//database functions
override fun serialize(): Data { override fun serialize(): Data {
//serialize Message property
val kryo = Kryo() val kryo = Kryo()
kryo.isRegistrationRequired = false kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096) val serializedMessage = ByteArray(4096)
@ -133,9 +131,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
} }
class Factory: Job.Factory<AttachmentUploadJob> { class Factory: Job.Factory<AttachmentUploadJob> {
override fun create(data: Data): AttachmentUploadJob { override fun create(data: Data): AttachmentUploadJob {
val serializedMessage = data.getByteArray(KEY_MESSAGE) val serializedMessage = data.getByteArray(KEY_MESSAGE)
//deserialize Message property
val kryo = Kryo() val kryo = Kryo()
val input = Input(serializedMessage) val input = Input(serializedMessage)
val message: Message = kryo.readObject(input, Message::class.java) val message: Message = kryo.readObject(input, Message::class.java)

View File

@ -12,7 +12,6 @@ import org.session.libsession.utilities.ParcelableUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
// TODO AC: For now parcelable objects utilize byteArrays field to store their data into.
// Introduce a dedicated Map<String, byte[]> field specifically for parcelable needs. // Introduce a dedicated Map<String, byte[]> field specifically for parcelable needs.
public class Data { public class Data {

View File

@ -8,15 +8,13 @@ interface Job {
val maxFailureCount: Int val maxFailureCount: Int
companion object { companion object {
//keys used for database storage purpose // Keys used for database storage
private val KEY_ID = "id" private val KEY_ID = "id"
private val KEY_FAILURE_COUNT = "failure_count" private val KEY_FAILURE_COUNT = "failure_count"
} }
fun execute() fun execute()
//database functions
fun serialize(): Data fun serialize(): Data
/** /**

Some files were not shown because too many files have changed in this diff Show More