mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into specific-group-updates
This commit is contained in:
commit
30b47a32cb
@ -157,7 +157,7 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 147
|
def canonicalVersionCode = 150
|
||||||
def canonicalVersionName = "1.9.0"
|
def canonicalVersionName = "1.9.0"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
|
@ -21,9 +21,9 @@ import android.content.Intent;
|
|||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
@ -32,32 +32,31 @@ import androidx.multidex.MultiDexApplication;
|
|||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.session.libsession.messaging.MessagingConfiguration;
|
import org.session.libsession.messaging.MessagingConfiguration;
|
||||||
import org.session.libsession.messaging.avatars.AvatarHelper;
|
import org.session.libsession.messaging.avatars.AvatarHelper;
|
||||||
import org.session.libsession.snode.SnodeConfiguration;
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
import org.session.libsession.utilities.SSKEnvironment;
|
import org.session.libsession.messaging.opengroups.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.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.utilities.IdentityKeyUtil;
|
||||||
|
import org.session.libsession.utilities.SSKEnvironment;
|
||||||
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.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||||
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.messages.SignalServiceEnvelope;
|
|
||||||
import org.session.libsignal.service.api.util.StreamDetails;
|
import org.session.libsignal.service.api.util.StreamDetails;
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos;
|
|
||||||
import org.session.libsignal.service.loki.api.Poller;
|
|
||||||
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
import org.session.libsignal.service.loki.api.PushNotificationAPI;
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI;
|
import org.session.libsignal.service.loki.api.SnodeAPI;
|
||||||
import org.session.libsignal.service.loki.api.SwarmAPI;
|
import org.session.libsignal.service.loki.api.SwarmAPI;
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI;
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol;
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
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;
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||||
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule;
|
||||||
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
|
import org.thoughtcrime.securesms.jobmanager.DependencyInjector;
|
||||||
@ -65,13 +64,11 @@ import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
import org.thoughtcrime.securesms.jobs.FastJobStorage;
|
||||||
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
import org.thoughtcrime.securesms.jobs.JobManagerFactories;
|
||||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob;
|
|
||||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
|
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
|
||||||
import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller;
|
|
||||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
||||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
||||||
@ -142,10 +139,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
public Poller poller = null;
|
public Poller poller = null;
|
||||||
public ClosedGroupPoller closedGroupPoller = null;
|
public ClosedGroupPoller closedGroupPoller = null;
|
||||||
public PublicChatManager publicChatManager = null;
|
public PublicChatManager publicChatManager = null;
|
||||||
private PublicChatAPI publicChatAPI = null;
|
|
||||||
public Broadcaster broadcaster = null;
|
public Broadcaster broadcaster = null;
|
||||||
public SignalCommunicationModule communicationModule;
|
public SignalCommunicationModule communicationModule;
|
||||||
private Job firebaseInstanceIdJob;
|
private Job firebaseInstanceIdJob;
|
||||||
|
private Handler threadNotificationHandler;
|
||||||
|
|
||||||
private volatile boolean isAppVisible;
|
private volatile boolean isAppVisible;
|
||||||
|
|
||||||
@ -153,7 +150,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
return (ApplicationContext) context.getApplicationContext();
|
return (ApplicationContext) context.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Handler getThreadNotificationHandler() {
|
||||||
|
return this.threadNotificationHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
@ -168,6 +169,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
// ========
|
// ========
|
||||||
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier());
|
||||||
broadcaster = new Broadcaster(this);
|
broadcaster = new Broadcaster(this);
|
||||||
|
threadNotificationHandler = new Handler(Looper.getMainLooper());
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
||||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||||
@ -193,16 +195,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
// Set application UI mode (day/night theme) to the user selected one.
|
// Set application UI mode (day/night theme) to the user selected one.
|
||||||
UiModeUtilities.setupUiModeToUserSelected(this);
|
UiModeUtilities.setupUiModeToUserSelected(this);
|
||||||
// ========
|
// ========
|
||||||
initializeJobManager();
|
|
||||||
initializeExpiringMessageManager();
|
initializeExpiringMessageManager();
|
||||||
initializeTypingStatusRepository();
|
initializeTypingStatusRepository();
|
||||||
initializeTypingStatusSender();
|
initializeTypingStatusSender();
|
||||||
initializeReadReceiptManager();
|
initializeReadReceiptManager();
|
||||||
initializeProfileManager();
|
initializeProfileManager();
|
||||||
initializePeriodicTasks();
|
initializePeriodicTasks();
|
||||||
|
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
||||||
|
initializeJobManager();
|
||||||
initializeWebRtc();
|
initializeWebRtc();
|
||||||
initializeBlobProvider();
|
initializeBlobProvider();
|
||||||
SSKEnvironment.Companion.configure(getTypingStatusRepository(), getReadReceiptManager(), getProfileManager(), messageNotifier, getExpiringMessageManager());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -232,14 +234,14 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
if (closedGroupPoller != null) {
|
if (closedGroupPoller != null) {
|
||||||
closedGroupPoller.stopIfNeeded();
|
closedGroupPoller.stopIfNeeded();
|
||||||
}
|
}
|
||||||
if (publicChatManager != null) {
|
|
||||||
publicChatManager.stopPollers();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTerminate() {
|
public void onTerminate() {
|
||||||
stopKovenant(); // Loki
|
stopKovenant(); // Loki
|
||||||
|
if (publicChatManager != null) {
|
||||||
|
publicChatManager.stopPollers();
|
||||||
|
}
|
||||||
super.onTerminate();
|
super.onTerminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,22 +289,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loki
|
// Loki
|
||||||
public @Nullable
|
|
||||||
PublicChatAPI getPublicChatAPI() {
|
|
||||||
if (publicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) {
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null) {
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
|
||||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
|
||||||
GroupDatabase groupDB = DatabaseFactory.getGroupDatabase(this);
|
|
||||||
publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB, groupDB);
|
|
||||||
return publicChatAPI;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeSecurityProvider() {
|
private void initializeSecurityProvider() {
|
||||||
try {
|
try {
|
||||||
@ -347,6 +333,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
|
.setJobStorage(new FastJobStorage(DatabaseFactory.getJobDatabase(this)))
|
||||||
.setDependencyInjector(this)
|
.setDependencyInjector(this)
|
||||||
.build());
|
.build());
|
||||||
|
JobQueue.getShared().resumePendingJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeDependencyInjection() {
|
private void initializeDependencyInjection() {
|
||||||
@ -478,17 +465,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||||
Context context = this;
|
|
||||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||||
poller = new Poller(userPublicKey, apiDB, envelopes -> {
|
poller = new Poller();
|
||||||
for (SignalServiceProtos.Envelope envelope : envelopes) {
|
closedGroupPoller = new ClosedGroupPoller();
|
||||||
new PushContentReceiveJob(context).processEnvelope(new SignalServiceEnvelope(envelope), false);
|
|
||||||
}
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
ClosedGroupPoller.Companion.configureIfNeeded(this);
|
|
||||||
closedGroupPoller = ClosedGroupPoller.Companion.getShared();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startPollingIfNeeded() {
|
public void startPollingIfNeeded() {
|
||||||
@ -539,21 +519,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
|
|
||||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
public void updateOpenGroupProfilePicturesIfNeeded() {
|
||||||
AsyncTask.execute(() -> {
|
AsyncTask.execute(() -> {
|
||||||
PublicChatAPI publicChatAPI = null;
|
|
||||||
try {
|
|
||||||
publicChatAPI = getPublicChatAPI();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
if (publicChatAPI == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
String url = TextSecurePreferences.getProfilePictureURL(this);
|
||||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||||
for (String server : servers) {
|
for (String server : servers) {
|
||||||
if (profileKey != null) {
|
if (profileKey != null) {
|
||||||
publicChatAPI.setProfilePicture(server, profileKey, url);
|
OpenGroupAPI.setProfilePicture(server, profileKey, url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -18,9 +18,6 @@ package org.thoughtcrime.securesms;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
@ -30,16 +27,6 @@ import android.os.Build;
|
|||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.core.util.Pair;
|
|
||||||
import androidx.viewpager.widget.PagerAdapter;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@ -53,16 +40,28 @@ import android.widget.FrameLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.core.util.Pair;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
import androidx.loader.content.Loader;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
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;
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||||
import org.session.libsession.utilities.Util;
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.MediaView;
|
import org.thoughtcrime.securesms.components.MediaView;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
@ -312,6 +311,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
private int cleanupMedia() {
|
private int cleanupMedia() {
|
||||||
int restartItem = mediaPager.getCurrentItem();
|
int restartItem = mediaPager.getCurrentItem();
|
||||||
|
|
||||||
|
PagerAdapter adapter = mediaPager.getAdapter();
|
||||||
|
if (adapter instanceof CursorPagerAdapter) {
|
||||||
|
((CursorPagerAdapter)adapter).cursor.close();
|
||||||
|
}
|
||||||
|
|
||||||
mediaPager.removeAllViews();
|
mediaPager.removeAllViews();
|
||||||
mediaPager.setAdapter(null);
|
mediaPager.setAdapter(null);
|
||||||
|
|
||||||
|
@ -66,19 +66,19 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
|
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageForQuote(timestamp: Long, author: Address): Long? {
|
override fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>? {
|
||||||
val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context)
|
val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
return messagingDatabase.getMessageFor(timestamp, author)?.id
|
val message = messagingDatabase.getMessageFor(timestamp, author)
|
||||||
|
return if (message != null) Pair(message.id, message.isMms) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment> {
|
override fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment> {
|
||||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
return DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(mmsId)
|
||||||
return attachmentDatabase.getAttachmentsForMessage(messageID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMessageBodyFor(messageID: Long): String {
|
override fun getMessageBodyFor(timestamp: Long, author: String): String {
|
||||||
val messagingDatabase = DatabaseFactory.getSmsDatabase(context)
|
val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
return messagingDatabase.getMessage(messageID).body
|
return messagingDatabase.getMessageFor(timestamp, author)!!.body
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttachmentIDsFor(messageID: Long): List<Long> {
|
override fun getAttachmentIDsFor(messageID: Long): List<Long> {
|
||||||
@ -93,9 +93,9 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
|
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun insertAttachment(messageId: Long, attachmentId: Long, stream: InputStream) {
|
override fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream: InputStream) {
|
||||||
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId, 0), stream)
|
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, attachmentId, stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
override fun isOutgoingMessage(timestamp: Long): Boolean {
|
||||||
@ -190,6 +190,10 @@ fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
|
|||||||
return SessionServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
|
return SessionServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SessionServiceAttachmentPointer.toSignalPointer(): SignalServiceAttachmentPointer {
|
||||||
|
return SignalServiceAttachmentPointer(id,contentType,key?.toByteArray() ?: byteArrayOf(), size, preview, width, height, digest, fileName, voiceNote, caption, url)
|
||||||
|
}
|
||||||
|
|
||||||
fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttachmentStream {
|
fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttachmentStream {
|
||||||
val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!)
|
val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!)
|
||||||
val listener = SignalServiceAttachment.ProgressListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(this, total, progress))}
|
val listener = SignalServiceAttachment.ProgressListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(this, total, progress))}
|
||||||
|
@ -807,7 +807,7 @@ 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(expirationTime);
|
ExpirationTimerUpdate message = new ExpirationTimerUpdate(null, expirationTime);
|
||||||
message.setSentTimestamp(System.currentTimeMillis());
|
message.setSentTimestamp(System.currentTimeMillis());
|
||||||
String displayedMessage = UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(getApplicationContext(), expirationTime, null, false);
|
String displayedMessage = UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(getApplicationContext(), expirationTime, null, false);
|
||||||
OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient, displayedMessage);
|
OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient, displayedMessage);
|
||||||
@ -1013,7 +1013,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (isClosedGroup) {
|
if (isClosedGroup) {
|
||||||
MessageSender.explicitLeave(groupPublicKey);
|
MessageSender.explicitLeave(groupPublicKey, true);
|
||||||
initializeEnabledCheck();
|
initializeEnabledCheck();
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||||
|
@ -52,11 +52,21 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||||
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.linkpreview.LinkPreview;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||||
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.ThemeUtil;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
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.service.loki.api.opengroups.PublicChat;
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||||
@ -69,7 +79,6 @@ import org.thoughtcrime.securesms.components.LinkPreviewView;
|
|||||||
import org.thoughtcrime.securesms.components.QuoteView;
|
import org.thoughtcrime.securesms.components.QuoteView;
|
||||||
import org.thoughtcrime.securesms.components.StickerView;
|
import org.thoughtcrime.securesms.components.StickerView;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
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.model.MediaMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
@ -78,7 +87,6 @@ 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.jobs.AttachmentDownloadJob;
|
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||||
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;
|
||||||
@ -89,22 +97,11 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
import org.thoughtcrime.securesms.util.LongClickCopySpan;
|
||||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.ThemeUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.views.Stub;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -916,7 +913,7 @@ public class ConversationItem extends LinearLayout
|
|||||||
|
|
||||||
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
boolean isModerator = PublicChatAPI.Companion.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,32 +37,26 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
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.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.DatabaseAttachmentAudioExtras;
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsignal.utilities.JsonUtil;
|
||||||
|
import org.session.libsignal.utilities.externalstorage.ExternalStorageUtil;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
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.DatabaseAttachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras;
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.MediaStream;
|
import org.thoughtcrime.securesms.mms.MediaStream;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.session.libsignal.utilities.externalstorage.ExternalStorageUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
|
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
|
||||||
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
|
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
|
||||||
@ -240,7 +234,11 @@ public class AttachmentDatabase extends Database {
|
|||||||
null, null, null);
|
null, null, null);
|
||||||
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
results.addAll(getAttachment(cursor));
|
List<DatabaseAttachment> attachments = getAttachment(cursor);
|
||||||
|
for (DatabaseAttachment attachment : attachments) {
|
||||||
|
if (attachment.isQuote()) continue;
|
||||||
|
results.add(attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
@ -16,11 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.thoughtcrime.securesms.database;
|
package org.thoughtcrime.securesms.database;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.Debouncer;
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -31,10 +35,13 @@ public abstract class Database {
|
|||||||
|
|
||||||
protected SQLCipherOpenHelper databaseHelper;
|
protected SQLCipherOpenHelper databaseHelper;
|
||||||
protected final Context context;
|
protected final Context context;
|
||||||
|
private final Debouncer threadNotificationDebouncer;
|
||||||
|
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
public Database(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.databaseHelper = databaseHelper;
|
this.databaseHelper = databaseHelper;
|
||||||
|
this.threadNotificationDebouncer = new Debouncer(ApplicationContext.getInstance(context).getThreadNotificationHandler(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListeners(Set<Long> threadIds) {
|
protected void notifyConversationListeners(Set<Long> threadIds) {
|
||||||
@ -47,7 +54,7 @@ public abstract class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyConversationListListeners() {
|
protected void notifyConversationListListeners() {
|
||||||
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
|
threadNotificationDebouncer.publish(()->context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifyStickerListeners() {
|
protected void notifyStickerListeners() {
|
||||||
|
@ -518,7 +518,9 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, distributionType, quote, contacts, previews, networkFailures, mismatches);
|
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);
|
||||||
@ -774,6 +776,11 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDuplicate(message, threadId)) {
|
||||||
|
Log.w(TAG, "Ignoring duplicate media message (" + message.getSentTimeMillis() + ")");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
||||||
|
|
||||||
if (message.getRecipient().getAddress().isGroup()) {
|
if (message.getRecipient().getAddress().isGroup()) {
|
||||||
@ -945,6 +952,19 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isDuplicate(OutgoingMediaMessage message, long threadId) {
|
||||||
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
|
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
||||||
|
new String[]{String.valueOf(message.getSentTimeMillis()), message.getRecipient().getAddress().serialize(), String.valueOf(threadId)},
|
||||||
|
null, null, null, "1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cursor != null && cursor.moveToFirst();
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSent(long messageId) {
|
public boolean isSent(long messageId) {
|
||||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
try (Cursor cursor = database.query(TABLE_NAME, new String[] { MESSAGE_BOX }, ID + " = ?", new String[] { String.valueOf(messageId)}, null, null, null)) {
|
try (Cursor cursor = database.query(TABLE_NAME, new String[] { MESSAGE_BOX }, ID + " = ?", new String[] { String.valueOf(messageId)}, null, null, null)) {
|
||||||
|
@ -18,19 +18,19 @@ package org.thoughtcrime.securesms.database;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
import net.sqlcipher.database.SQLiteQueryBuilder;
|
import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.threads.Address;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -79,18 +79,16 @@ public class MmsSmsDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable MessageRecord getMessageForTimestamp(long timestamp) {
|
public @Nullable MessageRecord getMessageForTimestamp(long timestamp) {
|
||||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
|
||||||
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
||||||
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
|
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||||
return reader.getNext();
|
return reader.getNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
|
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
|
||||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
|
||||||
|
|
||||||
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
|
||||||
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
|
MmsSmsDatabase.Reader reader = readerFor(cursor);
|
||||||
|
|
||||||
MessageRecord messageRecord;
|
MessageRecord messageRecord;
|
||||||
|
|
||||||
|
@ -456,7 +456,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
contentValues.put(THREAD_ID, threadId);
|
contentValues.put(THREAD_ID, threadId);
|
||||||
contentValues.put(BODY, message.getMessageBody());
|
contentValues.put(BODY, message.getMessageBody());
|
||||||
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
|
contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||||
contentValues.put(DATE_SENT, date);
|
contentValues.put(DATE_SENT, message.getSentTimestampMillis());
|
||||||
contentValues.put(READ, 1);
|
contentValues.put(READ, 1);
|
||||||
contentValues.put(TYPE, type);
|
contentValues.put(TYPE, type);
|
||||||
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
|
contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId());
|
||||||
@ -464,6 +464,11 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum());
|
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum());
|
||||||
contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum());
|
contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum());
|
||||||
|
|
||||||
|
if (isDuplicate(message, threadId)) {
|
||||||
|
Log.w(TAG, "Duplicate message (" + message.getSentTimestampMillis() + "), ignoring...");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
|
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
|
||||||
if (insertListener != null) {
|
if (insertListener != null) {
|
||||||
@ -530,6 +535,19 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isDuplicate(OutgoingTextMessage message, long threadId) {
|
||||||
|
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||||
|
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",
|
||||||
|
new String[]{String.valueOf(message.getSentTimestampMillis()), message.getRecipient().getAddress().serialize(), String.valueOf(threadId)},
|
||||||
|
null, null, null, "1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return cursor != null && cursor.moveToFirst();
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*package */void deleteThread(long threadId) {
|
/*package */void deleteThread(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
|
||||||
|
@ -8,11 +8,14 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
|||||||
import org.session.libsession.messaging.jobs.Job
|
import org.session.libsession.messaging.jobs.Job
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||||
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
|
import org.session.libsession.messaging.messages.signal.*
|
||||||
|
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.opengroups.OpenGroup
|
||||||
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.PointerAttachment
|
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.linkpreview.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
|
||||||
@ -20,31 +23,24 @@ 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.UpdateMessageBuilder
|
import org.session.libsession.messaging.utilities.UpdateMessageBuilder
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsession.utilities.IdentityKeyUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||||
import org.session.libsignal.libsignal.util.KeyHelper
|
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.SignalServiceAttachment
|
|
||||||
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.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil
|
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
|
||||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||||
import org.thoughtcrime.securesms.loki.utilities.get
|
import org.thoughtcrime.securesms.loki.utilities.get
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
|
||||||
|
|
||||||
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
|
||||||
override fun getUserPublicKey(): String? {
|
override fun getUserPublicKey(): String? {
|
||||||
@ -73,6 +69,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return TextSecurePreferences.getProfilePictureURL(context)
|
return TextSecurePreferences.getProfilePictureURL(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setUserProfilePictureUrl(newProfilePicture: String) {
|
||||||
|
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
|
||||||
|
Recipient.from(context, it, false)
|
||||||
|
}
|
||||||
|
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
|
||||||
|
RetrieveProfileAvatarJob(ourRecipient, newProfilePicture)
|
||||||
|
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? {
|
override fun getProfileKeyForRecipient(recipientPublicKey: String): 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)
|
||||||
@ -84,6 +89,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.getDisplayName(recipientPublicKey)
|
return database.getDisplayName(recipientPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) {
|
||||||
|
val address = Address.fromSerialized(recipientPublicKey)
|
||||||
|
val recipient = Recipient.from(context, address, false)
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setProfileKey(recipient, profileKey)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getOrGenerateRegistrationID(): Int {
|
override fun getOrGenerateRegistrationID(): Int {
|
||||||
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
|
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
|
||||||
if (registrationID == 0) {
|
if (registrationID == 0) {
|
||||||
@ -99,50 +110,52 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.insertAttachments(messageId, databaseAttachments)
|
return database.insertAttachments(messageId, databaseAttachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?): Long? {
|
override fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment> {
|
||||||
|
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
|
return database.getAttachmentsForMessage(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
|
||||||
var messageID: Long? = null
|
var messageID: Long? = null
|
||||||
val senderAddress = Address.fromSerialized(message.sender!!)
|
val senderAddress = Address.fromSerialized(message.sender!!)
|
||||||
val senderRecipient = Recipient.from(context, senderAddress, false)
|
val isUserSender = message.sender!! == getUserPublicKey()
|
||||||
var group: Optional<SignalServiceGroup> = Optional.absent()
|
val group: Optional<SignalServiceGroup> = when {
|
||||||
if (openGroupID != null) {
|
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
||||||
group = Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
|
groupPublicKey != null -> {
|
||||||
} else if (groupPublicKey != null) {
|
val doubleEncoded = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
group = Optional.of(SignalServiceGroup(groupPublicKey.toByteArray(), SignalServiceGroup.GroupType.SIGNAL))
|
Optional.of(SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(doubleEncoded), SignalServiceGroup.GroupType.SIGNAL))
|
||||||
|
}
|
||||||
|
else -> Optional.absent()
|
||||||
}
|
}
|
||||||
if (message.isMediaMessage()) {
|
val pointerAttachments = attachments.mapNotNull {
|
||||||
|
it.toSignalAttachment()
|
||||||
|
}
|
||||||
|
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
|
||||||
|
Address.fromSerialized(message.syncTarget!!)
|
||||||
|
} else if (group.isPresent) {
|
||||||
|
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
|
||||||
|
} else {
|
||||||
|
senderAddress
|
||||||
|
}
|
||||||
|
val targetRecipient = Recipient.from(context, targetAddress, false)
|
||||||
|
|
||||||
|
if (message.isMediaMessage() || attachments.isNotEmpty()) {
|
||||||
val quote: Optional<QuoteModel> = if (quotes != null) Optional.of(quotes) else Optional.absent()
|
val quote: Optional<QuoteModel> = if (quotes != null) Optional.of(quotes) else Optional.absent()
|
||||||
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)
|
||||||
mmsDatabase.beginTransaction()
|
|
||||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||||
val targetAddress = if (message.syncTarget != null) {
|
|
||||||
Address.fromSerialized(message.syncTarget!!)
|
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointerAttachments, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
|
||||||
} else {
|
mmsDatabase.beginTransaction()
|
||||||
if (group.isPresent) {
|
|
||||||
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
|
|
||||||
} else {
|
|
||||||
Log.d("Loki", "Cannot handle message from self.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val attachments = message.attachmentIDs.mapNotNull {
|
|
||||||
DatabaseFactory.getAttachmentProvider(context).getSignalAttachmentPointer(it)
|
|
||||||
}.mapNotNull {
|
|
||||||
PointerAttachment.forPointer(Optional.of(it)).orNull()
|
|
||||||
}
|
|
||||||
val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull().firstOrNull())
|
|
||||||
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
|
||||||
val attachments: Optional<List<SignalServiceAttachment>> = Optional.of(message.attachmentIDs.mapNotNull {
|
val signalServiceAttachments = attachments.mapNotNull {
|
||||||
DatabaseFactory.getAttachmentProvider(context).getSignalAttachmentPointer(it)
|
it.toSignalPointer()
|
||||||
})
|
|
||||||
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, senderRecipient.expireMessages * 1000L, group, attachments, quote, linkPreviews)
|
|
||||||
if (group.isPresent) {
|
|
||||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
|
|
||||||
} else {
|
|
||||||
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1)
|
|
||||||
}
|
}
|
||||||
|
val mediaMessage = IncomingMediaMessage.from(message, senderAddress, targetRecipient.expireMessages * 1000L, group, signalServiceAttachments, quote, linkPreviews)
|
||||||
|
mmsDatabase.beginTransaction()
|
||||||
|
mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.receivedTimestamp ?: 0)
|
||||||
}
|
}
|
||||||
if (insertResult.isPresent) {
|
if (insertResult.isPresent) {
|
||||||
mmsDatabase.setTransactionSuccessful()
|
mmsDatabase.setTransactionSuccessful()
|
||||||
@ -152,28 +165,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
} else {
|
} else {
|
||||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||||
val insertResult = if (message.sender == getUserPublicKey()) {
|
val insertResult = if (message.sender == getUserPublicKey()) {
|
||||||
val targetAddress = if (message.syncTarget != null) {
|
val textMessage = OutgoingTextMessage.from(message, targetRecipient)
|
||||||
Address.fromSerialized(message.syncTarget!!)
|
|
||||||
} else {
|
|
||||||
if (group.isPresent) {
|
|
||||||
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
|
|
||||||
} else {
|
|
||||||
Log.d("Loki", "Cannot handle message from self.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val textMessage = OutgoingTextMessage.from(message, Recipient.from(context, targetAddress, false))
|
|
||||||
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!)
|
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
val textMessage = IncomingTextMessage.from(message, senderAddress, group, senderRecipient.expireMessages * 1000L)
|
val textMessage = IncomingTextMessage.from(message, senderAddress, group, targetRecipient.expireMessages * 1000L)
|
||||||
if (group.isPresent) {
|
val encrypted = IncomingEncryptedMessage(textMessage, textMessage.messageBody)
|
||||||
smsDatabase.insertMessageInbox(textMessage, message.sentTimestamp!!)
|
smsDatabase.insertMessageInbox(encrypted, message.receivedTimestamp ?: 0)
|
||||||
} else {
|
|
||||||
smsDatabase.insertMessageInbox(textMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (insertResult.isPresent) {
|
insertResult.orNull()?.let { result ->
|
||||||
messageID = insertResult.get().messageId
|
messageID = result.messageId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return messageID
|
return messageID
|
||||||
@ -206,8 +206,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
|
|
||||||
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
|
override fun resumeMessageSendJobIfNeeded(messageSendJobID: String) {
|
||||||
val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return
|
val job = DatabaseFactory.getSessionJobDatabase(context).getMessageSendJob(messageSendJobID) ?: return
|
||||||
job.delegate = JobQueue.shared
|
JobQueue.shared.add(job)
|
||||||
job.execute()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isJobCanceled(job: Job): Boolean {
|
override fun isJobCanceled(job: Job): Boolean {
|
||||||
@ -286,12 +285,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean {
|
override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean {
|
||||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
return getReceivedMessageTimestamps().contains(timestamp)
|
||||||
return if (sender.isEmpty()) {
|
|
||||||
database.getMessageForTimestamp(timestamp) != null
|
|
||||||
} else {
|
|
||||||
database.getMessageFor(timestamp, sender) != null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
||||||
@ -374,6 +368,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||||
smsDatabase.markAsSentFailed(messageRecord.getId())
|
smsDatabase.markAsSentFailed(messageRecord.getId())
|
||||||
}
|
}
|
||||||
|
if (error.localizedMessage != null) {
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setErrorMessage(messageRecord.getId(), error.localizedMessage!!)
|
||||||
|
} else {
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setErrorMessage(messageRecord.getId(), error.javaClass.simpleName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGroup(groupID: String): GroupRecord? {
|
override fun getGroup(groupID: String): GroupRecord? {
|
||||||
@ -385,6 +384,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getGroupDatabase(context).create(groupId, title, members, avatar, relay, admins, formationTimestamp)
|
DatabaseFactory.getGroupDatabase(context).create(groupId, title, members, avatar, relay, admins, formationTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isGroupActive(groupPublicKey: String): Boolean {
|
||||||
|
return DatabaseFactory.getGroupDatabase(context).getGroup(GroupUtil.doubleEncodeGroupID(groupPublicKey)).orNull()?.isActive == true
|
||||||
|
}
|
||||||
|
|
||||||
override fun setActive(groupID: String, value: Boolean) {
|
override fun setActive(groupID: String, value: Boolean) {
|
||||||
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
|
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
|
||||||
}
|
}
|
||||||
@ -447,6 +450,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
|
return DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAllActiveClosedGroupPublicKeys(): Set<String> {
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys().filter {
|
||||||
|
getGroup(GroupUtil.doubleEncodeGroupID(it))?.isActive == true
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
override fun addClosedGroupPublicKey(groupPublicKey: String) {
|
override fun addClosedGroupPublicKey(groupPublicKey: String) {
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupPublicKey(groupPublicKey)
|
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupPublicKey(groupPublicKey)
|
||||||
}
|
}
|
||||||
@ -463,8 +472,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllOpenGroups(): Map<Long, PublicChat> {
|
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
|
||||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats()
|
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().mapValues { (_,chat)->
|
||||||
|
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOpenGroup(server: String, channel: Long) {
|
override fun addOpenGroup(server: String, channel: Long) {
|
||||||
@ -488,10 +499,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
|
override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long {
|
||||||
val database = DatabaseFactory.getThreadDatabase(context)
|
val database = DatabaseFactory.getThreadDatabase(context)
|
||||||
if (!openGroupID.isNullOrEmpty()) {
|
if (!openGroupID.isNullOrEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(openGroupID), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false)
|
||||||
return database.getOrCreateThreadIdFor(recipient)
|
return database.getOrCreateThreadIdFor(recipient)
|
||||||
} else if (!groupPublicKey.isNullOrEmpty()) {
|
} else if (!groupPublicKey.isNullOrEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(groupPublicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false)
|
||||||
return database.getOrCreateThreadIdFor(recipient)
|
return database.getOrCreateThreadIdFor(recipient)
|
||||||
} else {
|
} else {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||||
@ -525,6 +536,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setDisplayName(publicKey: String, newName: String) {
|
||||||
|
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
|
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverID, publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverID, publicKey)
|
||||||
}
|
}
|
||||||
@ -538,6 +553,31 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
|
return if (recipientSettings.isPresent) { recipientSettings.get() } else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
|
||||||
|
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||||
|
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||||
|
for (contact in 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 (contacts.isNotEmpty()) {
|
||||||
|
threadDatabase.notifyUpdatedFromConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
||||||
return PartAuthority.getAttachmentDataUri(attachmentId)
|
return PartAuthority.getAttachmentDataUri(attachmentId)
|
||||||
}
|
}
|
||||||
|
@ -323,6 +323,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
|||||||
attachments,
|
attachments,
|
||||||
message.getTimestamp(), -1,
|
message.getTimestamp(), -1,
|
||||||
message.getExpiresInSeconds() * 1000,
|
message.getExpiresInSeconds() * 1000,
|
||||||
|
false,
|
||||||
DistributionTypes.DEFAULT, quote.orNull(),
|
DistributionTypes.DEFAULT, quote.orNull(),
|
||||||
sharedContacts.or(Collections.emptyList()),
|
sharedContacts.or(Collections.emptyList()),
|
||||||
linkPreviews.or(Collections.emptyList()),
|
linkPreviews.or(Collections.emptyList()),
|
||||||
@ -471,7 +472,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
|
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
|
||||||
body, message.getExpiresInSeconds(), -1);
|
body, message.getExpiresInSeconds(), -1, message.getTimestamp());
|
||||||
|
|
||||||
// Ignore the message if it has no body
|
// Ignore the message if it has no body
|
||||||
if (tm.getMessageBody().length() == 0) { return; }
|
if (tm.getMessageBody().length() == 0) { return; }
|
||||||
|
@ -3,11 +3,11 @@ package org.thoughtcrime.securesms.jobs;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.session.libsession.messaging.threads.Address;
|
import org.session.libsession.messaging.threads.Address;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
|
||||||
public abstract class PushReceivedJob extends BaseJob {
|
public abstract class PushReceivedJob extends BaseJob {
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
loaderContainer.fadeIn()
|
loaderContainer.fadeIn()
|
||||||
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
|
||||||
MessageSender.explicitLeave(groupPublicKey!!)
|
MessageSender.explicitLeave(groupPublicKey!!, true)
|
||||||
} else {
|
} else {
|
||||||
task {
|
task {
|
||||||
if (hasNameChanged) {
|
if (hasNameChanged) {
|
||||||
|
@ -26,10 +26,12 @@ import kotlinx.coroutines.flow.*
|
|||||||
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 org.session.libsession.messaging.sending_receiving.MessageSender
|
|
||||||
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.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
|
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.mentions.MentionsManager
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
@ -139,6 +141,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
|
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
|
||||||
application.publicChatManager.startPollersIfNeeded()
|
application.publicChatManager.startPollersIfNeeded()
|
||||||
|
JobQueue.shared.resumePendingJobs()
|
||||||
}
|
}
|
||||||
IP2Country.configureIfNeeded(this)
|
IP2Country.configureIfNeeded(this)
|
||||||
application.registerForFCMIfNeeded(false)
|
application.registerForFCMIfNeeded(false)
|
||||||
@ -341,7 +344,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
isClosedGroup = false
|
isClosedGroup = false
|
||||||
}
|
}
|
||||||
if (isClosedGroup) {
|
if (isClosedGroup) {
|
||||||
MessageSender.explicitLeave(groupPublicKey!!)
|
MessageSender.explicitLeave(groupPublicKey!!, false)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||||
return@launch
|
return@launch
|
||||||
@ -357,8 +360,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatAPI!!
|
OpenGroupAPI.leave(publicChat.channel, publicChat.server)
|
||||||
.leave(publicChat.channel, publicChat.server)
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
.removeChat(publicChat.server, publicChat.channel)
|
.removeChat(publicChat.server, publicChat.channel)
|
||||||
|
@ -28,6 +28,7 @@ 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.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
|
||||||
@ -179,11 +180,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val promises = mutableListOf<Promise<*, Exception>>()
|
val promises = mutableListOf<Promise<*, Exception>>()
|
||||||
val displayName = displayNameToBeUploaded
|
val displayName = displayNameToBeUploaded
|
||||||
if (displayName != null) {
|
if (displayName != null) {
|
||||||
val publicChatAPI = ApplicationContext.getInstance(this).publicChatAPI
|
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||||
if (publicChatAPI != null) {
|
promises.addAll(servers.map { OpenGroupAPI.setDisplayName(displayName, it) })
|
||||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
|
||||||
promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
|
|
||||||
}
|
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
}
|
}
|
||||||
val profilePicture = profilePictureToBeUploaded
|
val profilePicture = profilePictureToBeUploaded
|
||||||
|
@ -7,12 +7,14 @@ import androidx.work.*
|
|||||||
import nl.komponents.kovenant.Promise
|
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.thoughtcrime.securesms.database.DatabaseFactory
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
import org.session.libsignal.service.loki.api.SnodeAPI
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
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) {
|
||||||
@ -69,20 +71,21 @@ 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.shared.getMessages(userPublicKey).map { envelopes ->
|
||||||
envelopes.forEach {
|
envelopes.map { envelope ->
|
||||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
MessageReceiveJob(envelope.toByteArray(), false).executeAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
promises.add(privateChatsPromise)
|
promises.addAll(privateChatsPromise.get())
|
||||||
|
|
||||||
// Closed groups
|
// Closed groups
|
||||||
ClosedGroupPoller.configureIfNeeded(context)
|
promises.addAll(ApplicationContext.getInstance(context).closedGroupPoller.pollOnce())
|
||||||
promises.addAll(ClosedGroupPoller.shared.pollOnce())
|
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value }
|
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { (_,chat)->
|
||||||
|
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
|
||||||
|
}
|
||||||
for (openGroup in openGroups) {
|
for (openGroup in openGroups) {
|
||||||
val poller = PublicChatPoller(context, openGroup)
|
val poller = OpenGroupPoller(openGroup)
|
||||||
promises.add(poller.pollForNewMessages())
|
promises.add(poller.pollForNewMessages())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.loki.api
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
|
||||||
import org.session.libsignal.service.loki.api.SwarmAPI
|
|
||||||
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
|
|
||||||
class ClosedGroupPoller private constructor(private val context: Context) {
|
|
||||||
private var isPolling = false
|
|
||||||
private val handler: Handler by lazy { Handler() }
|
|
||||||
|
|
||||||
private val task = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
poll()
|
|
||||||
handler.postDelayed(this, pollInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val pollInterval: Long = 4 * 1000
|
|
||||||
|
|
||||||
public lateinit var shared: ClosedGroupPoller
|
|
||||||
|
|
||||||
public fun configureIfNeeded(context: Context) {
|
|
||||||
if (::shared.isInitialized) { return; }
|
|
||||||
shared = ClosedGroupPoller(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Error
|
|
||||||
class InsufficientSnodesException() : Exception("No snodes left to poll.")
|
|
||||||
class PollingCanceledException() : Exception("Polling canceled.")
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Public API
|
|
||||||
fun startIfNeeded() {
|
|
||||||
if (isPolling) { return }
|
|
||||||
isPolling = true
|
|
||||||
task.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pollOnce(): List<Promise<Unit, Exception>> {
|
|
||||||
if (isPolling) { return listOf() }
|
|
||||||
isPolling = true
|
|
||||||
return poll()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopIfNeeded() {
|
|
||||||
isPolling = false
|
|
||||||
handler.removeCallbacks(task)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Private API
|
|
||||||
private fun poll(): List<Promise<Unit, Exception>> {
|
|
||||||
if (!isPolling) { return listOf() }
|
|
||||||
val publicKeys = DatabaseFactory.getLokiAPIDatabase(context).getAllClosedGroupPublicKeys()
|
|
||||||
return publicKeys.map { publicKey ->
|
|
||||||
val promise = SwarmAPI.shared.getSwarm(publicKey).bind { swarm ->
|
|
||||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
|
||||||
if (!isPolling) { throw PollingCanceledException() }
|
|
||||||
SnodeAPI.shared.getRawMessages(snode, publicKey).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode, publicKey) }
|
|
||||||
}
|
|
||||||
promise.successBackground { messages ->
|
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
|
||||||
}
|
|
||||||
messages.forEach {
|
|
||||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
promise.fail {
|
|
||||||
Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.")
|
|
||||||
}
|
|
||||||
promise.map { Unit }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -5,22 +5,26 @@ 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.thoughtcrime.securesms.ApplicationContext
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
|
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.utilities.TextSecurePreferences
|
||||||
|
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
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import java.util.concurrent.Executors
|
||||||
import org.session.libsession.utilities.Util
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatInfo
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
class PublicChatManager(private val context: Context) {
|
class PublicChatManager(private val context: Context) {
|
||||||
private var chats = mutableMapOf<Long, PublicChat>()
|
private var chats = mutableMapOf<Long, OpenGroup>()
|
||||||
private val pollers = mutableMapOf<Long, PublicChatPoller>()
|
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
|
||||||
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)
|
||||||
|
|
||||||
public fun areAllCaughtUp(): Boolean {
|
public fun areAllCaughtUp(): Boolean {
|
||||||
var areAllCaughtUp = true
|
var areAllCaughtUp = true
|
||||||
@ -35,7 +39,7 @@ class PublicChatManager(private val context: Context) {
|
|||||||
public fun markAllAsNotCaughtUp() {
|
public fun markAllAsNotCaughtUp() {
|
||||||
refreshChatsAndPollers()
|
refreshChatsAndPollers()
|
||||||
for ((threadID, chat) in chats) {
|
for ((threadID, chat) in chats) {
|
||||||
val poller = pollers[threadID] ?: PublicChatPoller(context, chat)
|
val poller = pollers[threadID] ?: OpenGroupPoller(chat, executorService)
|
||||||
poller.isCaughtUp = false
|
poller.isCaughtUp = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +48,7 @@ class PublicChatManager(private val context: Context) {
|
|||||||
refreshChatsAndPollers()
|
refreshChatsAndPollers()
|
||||||
|
|
||||||
for ((threadId, chat) in chats) {
|
for ((threadId, chat) in chats) {
|
||||||
val poller = pollers[threadId] ?: PublicChatPoller(context, chat)
|
val poller = pollers[threadId] ?: OpenGroupPoller(chat, executorService)
|
||||||
poller.startIfNeeded()
|
poller.startIfNeeded()
|
||||||
listenToThreadDeletion(threadId)
|
listenToThreadDeletion(threadId)
|
||||||
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
||||||
@ -55,32 +59,29 @@ class PublicChatManager(private val context: Context) {
|
|||||||
public fun stopPollers() {
|
public fun stopPollers() {
|
||||||
pollers.values.forEach { it.stop() }
|
pollers.values.forEach { it.stop() }
|
||||||
isPolling = false
|
isPolling = false
|
||||||
|
executorService.shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO Declare a specific type of checked exception instead of "Exception".
|
//TODO Declare a specific type of checked exception instead of "Exception".
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(java.lang.Exception::class)
|
@Throws(java.lang.Exception::class)
|
||||||
public fun addChat(server: String, channel: Long): PublicChat {
|
public fun addChat(server: String, channel: Long): OpenGroup {
|
||||||
val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
|
||||||
?: throw IllegalStateException("LokiPublicChatAPI is not set!")
|
|
||||||
|
|
||||||
// Ensure the auth token is acquired.
|
// Ensure the auth token is acquired.
|
||||||
groupChatAPI.getAuthToken(server).get()
|
OpenGroupAPI.getAuthToken(server).get()
|
||||||
|
|
||||||
val channelInfo = groupChatAPI.getChannelInfo(channel, server).get()
|
val channelInfo = OpenGroupAPI.getChannelInfo(channel, server).get()
|
||||||
return addChat(server, channel, channelInfo)
|
return addChat(server, channel, channelInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public fun addChat(server: String, channel: Long, info: PublicChatInfo): PublicChat {
|
public fun addChat(server: String, channel: Long, info: OpenGroupInfo): OpenGroup {
|
||||||
val chat = PublicChat(channel, server, info.displayName, true)
|
val chat = PublicChat(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
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
if (info.profilePictureURL.isNotEmpty()) {
|
if (info.profilePictureURL.isNotEmpty()) {
|
||||||
val profilePictureAsByteArray = ApplicationContext.getInstance(context).publicChatAPI
|
val profilePictureAsByteArray = OpenGroupAPI.downloadOpenGroupProfilePicture(server, info.profilePictureURL)
|
||||||
?.downloadOpenGroupProfilePicture(server, info.profilePictureURL)
|
|
||||||
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
||||||
}
|
}
|
||||||
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, chat.displayName)
|
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, chat.displayName)
|
||||||
@ -90,12 +91,12 @@ class PublicChatManager(private val context: Context) {
|
|||||||
// Set our name on the server
|
// Set our name on the server
|
||||||
val displayName = TextSecurePreferences.getProfileName(context)
|
val displayName = TextSecurePreferences.getProfileName(context)
|
||||||
if (!TextUtils.isEmpty(displayName)) {
|
if (!TextUtils.isEmpty(displayName)) {
|
||||||
ApplicationContext.getInstance(context).publicChatAPI?.setDisplayName(displayName, server)
|
OpenGroupAPI.setDisplayName(displayName, server)
|
||||||
}
|
}
|
||||||
// Start polling
|
// Start polling
|
||||||
Util.runOnMain { startPollersIfNeeded() }
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
|
||||||
return chat
|
return OpenGroup.from(chat)
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun removeChat(server: String, channel: Long) {
|
public fun removeChat(server: String, channel: Long) {
|
||||||
@ -109,7 +110,8 @@ class PublicChatManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshChatsAndPollers() {
|
private fun refreshChatsAndPollers() {
|
||||||
val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats()
|
val storage = MessagingConfiguration.shared.storage
|
||||||
|
val chatsInDB = storage.getAllOpenGroups()
|
||||||
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() }
|
||||||
|
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.loki.api
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil
|
|
||||||
import org.session.libsession.messaging.threads.Address
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceContent
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceGroup
|
|
||||||
import org.session.libsignal.service.api.push.SignalServiceAddress
|
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI
|
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatMessage
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PublicChatPoller(private val context: Context, private val group: PublicChat) {
|
|
||||||
private val handler by lazy { Handler() }
|
|
||||||
private var hasStarted = false
|
|
||||||
private var isPollOngoing = false
|
|
||||||
public var isCaughtUp = false
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
|
||||||
private var displayNameUpdatees = setOf<String>()
|
|
||||||
|
|
||||||
private val api: PublicChatAPI
|
|
||||||
get() = {
|
|
||||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
|
||||||
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
|
|
||||||
val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context)
|
|
||||||
val openGroupDatabase = DatabaseFactory.getGroupDatabase(context)
|
|
||||||
PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase, openGroupDatabase)
|
|
||||||
}()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Tasks
|
|
||||||
private val pollForNewMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForNewMessages()
|
|
||||||
handler.postDelayed(this, pollForNewMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDeletedMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDeletedMessages()
|
|
||||||
handler.postDelayed(this, pollForDeletedMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForModeratorsTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForModerators()
|
|
||||||
handler.postDelayed(this, pollForModeratorsInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDisplayNamesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDisplayNames()
|
|
||||||
handler.postDelayed(this, pollForDisplayNamesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val pollForNewMessagesInterval: Long = 4 * 1000
|
|
||||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
|
||||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
|
||||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Lifecycle
|
|
||||||
fun startIfNeeded() {
|
|
||||||
if (hasStarted) return
|
|
||||||
pollForNewMessagesTask.run()
|
|
||||||
pollForDeletedMessagesTask.run()
|
|
||||||
pollForModeratorsTask.run()
|
|
||||||
pollForDisplayNamesTask.run()
|
|
||||||
hasStarted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
handler.removeCallbacks(pollForNewMessagesTask)
|
|
||||||
handler.removeCallbacks(pollForDeletedMessagesTask)
|
|
||||||
handler.removeCallbacks(pollForModeratorsTask)
|
|
||||||
handler.removeCallbacks(pollForDisplayNamesTask)
|
|
||||||
hasStarted = false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Polling
|
|
||||||
private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage {
|
|
||||||
val id = group.id.toByteArray()
|
|
||||||
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
|
|
||||||
val quote = if (message.quote != null) {
|
|
||||||
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val attachments = message.attachments.mapNotNull { attachment ->
|
|
||||||
if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
|
||||||
SignalServiceAttachmentPointer(
|
|
||||||
attachment.serverID,
|
|
||||||
attachment.contentType,
|
|
||||||
ByteArray(0),
|
|
||||||
Optional.of(attachment.size),
|
|
||||||
Optional.absent(),
|
|
||||||
attachment.width, attachment.height,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.of(attachment.fileName),
|
|
||||||
false,
|
|
||||||
Optional.fromNullable(attachment.caption),
|
|
||||||
attachment.url)
|
|
||||||
}
|
|
||||||
val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview }
|
|
||||||
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
|
|
||||||
if (linkPreview != null) {
|
|
||||||
val attachment = SignalServiceAttachmentPointer(
|
|
||||||
linkPreview.serverID,
|
|
||||||
linkPreview.contentType,
|
|
||||||
ByteArray(0),
|
|
||||||
Optional.of(linkPreview.size),
|
|
||||||
Optional.absent(),
|
|
||||||
linkPreview.width, linkPreview.height,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.of(linkPreview.fileName),
|
|
||||||
false,
|
|
||||||
Optional.fromNullable(linkPreview.caption),
|
|
||||||
linkPreview.url)
|
|
||||||
signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
|
|
||||||
}
|
|
||||||
val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
|
|
||||||
val syncTarget = if (message.senderPublicKey == userHexEncodedPublicKey) group.id else null
|
|
||||||
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, 0, false, null, quote, null, signalLinkPreviews, null, syncTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
|
||||||
if (isPollOngoing) { return Promise.of(Unit) }
|
|
||||||
isPollOngoing = true
|
|
||||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
|
||||||
FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB)
|
|
||||||
// Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below
|
|
||||||
val promise = api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages ->
|
|
||||||
Promise.of(messages)
|
|
||||||
}
|
|
||||||
promise.successBackground { messages ->
|
|
||||||
// Process messages in the background
|
|
||||||
messages.forEach { message ->
|
|
||||||
// If the sender of the current message is not a slave device, set the display name in the database
|
|
||||||
val senderDisplayName = "${message.displayName} (...${message.senderPublicKey.takeLast(8)})"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.senderPublicKey, senderDisplayName)
|
|
||||||
val senderHexEncodedPublicKey = message.senderPublicKey
|
|
||||||
val serviceDataMessage = getDataMessage(message)
|
|
||||||
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.serverTimestamp, false)
|
|
||||||
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
|
|
||||||
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
|
||||||
} else {
|
|
||||||
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
|
||||||
}
|
|
||||||
// Update profile picture if needed
|
|
||||||
val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false)
|
|
||||||
if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) {
|
|
||||||
val profileKey = message.profilePicture!!.profileKey
|
|
||||||
val url = message.profilePicture!!.url
|
|
||||||
if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) {
|
|
||||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
|
||||||
database.setProfileKey(senderAsRecipient, profileKey)
|
|
||||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isCaughtUp = true
|
|
||||||
isPollOngoing = false
|
|
||||||
}
|
|
||||||
promise.fail {
|
|
||||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
|
||||||
isPollOngoing = false
|
|
||||||
}
|
|
||||||
return promise.map { Unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDisplayNames() {
|
|
||||||
if (displayNameUpdatees.isEmpty()) { return }
|
|
||||||
val hexEncodedPublicKeys = displayNameUpdatees
|
|
||||||
displayNameUpdatees = setOf()
|
|
||||||
api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping ->
|
|
||||||
for (pair in mapping.entries) {
|
|
||||||
val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDeletedMessages() {
|
|
||||||
api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs ->
|
|
||||||
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) }
|
|
||||||
val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
|
|
||||||
val mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context)
|
|
||||||
deletedMessageIDs.forEach {
|
|
||||||
smsMessageDatabase.deleteMessage(it)
|
|
||||||
mmsMessageDatabase.delete(it)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForModerators() {
|
|
||||||
api.getModerators(group.channel, group.server)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -4,13 +4,13 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.service.loki.api.MessageWrapper
|
import org.session.libsignal.service.loki.api.MessageWrapper
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||||
|
|
||||||
class PushNotificationService : FirebaseMessagingService() {
|
class PushNotificationService : FirebaseMessagingService() {
|
||||||
|
|
||||||
@ -27,8 +27,7 @@ class PushNotificationService : FirebaseMessagingService() {
|
|||||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
try {
|
try {
|
||||||
val envelope = MessageWrapper.unwrap(data)
|
JobQueue.shared.add(MessageReceiveJob(MessageWrapper.unwrap(data).toByteArray(),true))
|
||||||
PushContentReceiveJob(this).processEnvelope(SignalServiceEnvelope(envelope), true)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("Loki", "Failed to unwrap data for message due to error: $e.")
|
Log.d("Loki", "Failed to unwrap data for message due to error: $e.")
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,21 @@ 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.utilities.IdentityKeyUtil
|
||||||
|
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.database.LokiAPIDatabaseProtocol
|
||||||
|
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
||||||
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.session.libsignal.service.loki.api.Snode
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
|
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
|
||||||
@ -416,7 +416,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
val index = "$groupPublicKey-$timestamp"
|
val index = "$groupPublicKey-$timestamp"
|
||||||
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
|
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
|
||||||
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
|
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
|
||||||
val row = wrap(mapOf( Companion.closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
|
val row = wrap(mapOf(closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
|
||||||
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
|
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
|
||||||
database.insertOrUpdate(closedGroupEncryptionKeyPairsTable, row, "${Companion.closedGroupsEncryptionKeyPairIndex} = ?", wrap(index))
|
database.insertOrUpdate(closedGroupEncryptionKeyPairsTable, row, "${Companion.closedGroupsEncryptionKeyPairIndex} = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,11 @@ import org.thoughtcrime.securesms.loki.utilities.*
|
|||||||
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val sessionJobTable = "session_job_database"
|
private const val sessionJobTable = "session_job_database"
|
||||||
val jobID = "job_id"
|
const val jobID = "job_id"
|
||||||
val jobType = "job_type"
|
const val jobType = "job_type"
|
||||||
val failureCount = "failure_count"
|
const val failureCount = "failure_count"
|
||||||
val serializedData = "serialized_data"
|
const val serializedData = "serialized_data"
|
||||||
@JvmStatic val createSessionJobTableCommand = "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
|
@JvmStatic val createSessionJobTableCommand = "CREATE TABLE $sessionJobTable ($jobID INTEGER PRIMARY KEY, $jobType STRING, $failureCount INTEGER DEFAULT 0, $serializedData TEXT);"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,10 +85,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionJobHelper() {
|
object SessionJobHelper {
|
||||||
|
val dataSerializer: Data.Serializer = JsonDataSerializer()
|
||||||
companion object {
|
val sessionJobInstantiator: SessionJobInstantiator = SessionJobInstantiator(SessionJobManagerFactories.getSessionJobFactories())
|
||||||
val dataSerializer: Data.Serializer = JsonDataSerializer()
|
|
||||||
val sessionJobInstantiator: SessionJobInstantiator = SessionJobInstantiator(SessionJobManagerFactories.getSessionJobFactories())
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -126,6 +126,5 @@ object MultiDeviceProtocol {
|
|||||||
threadDatabase.notifyUpdatedFromConfig()
|
threadDatabase.notifyUpdatedFromConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: handle new configuration message fields or handle in new pipeline
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,16 +3,15 @@ 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.thoughtcrime.securesms.ApplicationContext
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
|
||||||
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.libsignal.service.loki.api.opengroups.PublicChat
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||||
import java.lang.Exception
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import java.lang.IllegalStateException
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import kotlin.jvm.Throws
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
|
|
||||||
//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 {
|
||||||
@ -22,29 +21,27 @@ object OpenGroupUtilities {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun addGroup(context: Context, url: String, channel: Long): PublicChat {
|
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 = PublicChat.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 }
|
if (openGroup != null) { return OpenGroup.from(openGroup) }
|
||||||
|
|
||||||
// Add the new group.
|
// Add the new group.
|
||||||
val application = ApplicationContext.getInstance(context)
|
val application = ApplicationContext.getInstance(context)
|
||||||
val displayName = TextSecurePreferences.getProfileName(context)
|
val displayName = TextSecurePreferences.getProfileName(context)
|
||||||
val lokiPublicChatAPI = application.publicChatAPI
|
|
||||||
?: throw IllegalStateException("LokiPublicChatAPI is not initialized.")
|
|
||||||
|
|
||||||
val group = application.publicChatManager.addChat(url, channel)
|
val group = application.publicChatManager.addChat(url, channel)
|
||||||
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
|
||||||
lokiPublicChatAPI.getMessages(channel, url)
|
OpenGroupAPI.getMessages(channel, url)
|
||||||
lokiPublicChatAPI.setDisplayName(displayName, url)
|
OpenGroupAPI.setDisplayName(displayName, url)
|
||||||
lokiPublicChatAPI.join(channel, url)
|
OpenGroupAPI.join(channel, url)
|
||||||
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context)
|
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context)
|
||||||
val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context)
|
val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context)
|
||||||
lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl)
|
OpenGroupAPI.setProfilePicture(url, profileKey, profileUrl)
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,18 +55,15 @@ object OpenGroupUtilities {
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
||||||
val publicChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
|
||||||
?: throw IllegalStateException("Public chat API is not initialized!")
|
|
||||||
|
|
||||||
// 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(PublicChat.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")
|
||||||
}
|
}
|
||||||
|
|
||||||
val info = publicChatAPI.getChannelInfo(channel, url).get()
|
val info = OpenGroupAPI.getChannelInfo(channel, url).get()
|
||||||
|
|
||||||
publicChatAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
OpenGroupAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
||||||
|
|
||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ 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.thoughtcrime.securesms.mms.GlideRequests
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI
|
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.Mention
|
import org.session.libsignal.service.loki.utilities.mentions.Mention
|
||||||
|
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) {
|
||||||
var mentionCandidate = Mention("", "")
|
var mentionCandidate = Mention("", "")
|
||||||
@ -38,7 +38,7 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
|
|||||||
profilePictureView.glide = glide!!
|
profilePictureView.glide = glide!!
|
||||||
profilePictureView.update()
|
profilePictureView.update()
|
||||||
if (publicChatServer != null && publicChatChannel != null) {
|
if (publicChatServer != null && publicChatChannel != null) {
|
||||||
val isUserModerator = PublicChatAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
|
val isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
} else {
|
} else {
|
||||||
moderatorIconImageView.visibility = View.GONE
|
moderatorIconImageView.visibility = View.GONE
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
package org.thoughtcrime.securesms.mediapreview;
|
package org.thoughtcrime.securesms.mediapreview;
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
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 androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@ -27,7 +28,9 @@ public class MediaPreviewViewModel extends ViewModel {
|
|||||||
|
|
||||||
public void setCursor(@NonNull Context context, @Nullable Cursor cursor, boolean leftIsRecent) {
|
public void setCursor(@NonNull Context context, @Nullable Cursor cursor, boolean leftIsRecent) {
|
||||||
boolean firstLoad = (this.cursor == null) && (cursor != null);
|
boolean firstLoad = (this.cursor == null) && (cursor != null);
|
||||||
|
if (this.cursor != null) {
|
||||||
|
this.cursor.close();
|
||||||
|
}
|
||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
this.leftIsRecent = leftIsRecent;
|
this.leftIsRecent = leftIsRecent;
|
||||||
|
|
||||||
|
@ -6,14 +6,13 @@ import android.os.Looper;
|
|||||||
import androidx.annotation.MainThread;
|
import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.utilities.Debouncer;
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
||||||
import org.session.libsession.utilities.Debouncer;
|
|
||||||
import org.session.libsignal.service.loki.api.Poller;
|
|
||||||
import org.session.libsignal.utilities.ThreadUtils;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -4,14 +4,17 @@ import android.content.Context;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||||
|
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.messaging.utilities.UpdateMessageBuilder;
|
import org.session.libsession.messaging.utilities.UpdateMessageBuilder;
|
||||||
|
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.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;
|
||||||
@ -21,6 +24,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
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.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;
|
||||||
@ -66,45 +71,77 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setExpirationTimer(@Nullable Long messageID, int duration, @NotNull String senderPublicKey, @NotNull SignalServiceProtos.Content content) {
|
public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) {
|
||||||
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
|
||||||
|
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||||
|
String senderPublicKey = message.getSender();
|
||||||
|
int duration = message.getDuration();
|
||||||
|
String groupPK = message.getGroupPublicKey();
|
||||||
|
Long sentTimestamp = message.getSentTimestamp();
|
||||||
|
|
||||||
|
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
||||||
|
Address address;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
if (groupPK != null) {
|
||||||
Address address = Address.fromSerialized(senderPublicKey);
|
String groupID = GroupUtil.doubleEncodeGroupID(groupPK);
|
||||||
|
groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL));
|
||||||
|
address = Address.fromSerialized(groupID);
|
||||||
|
} else {
|
||||||
|
address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
|
||||||
|
}
|
||||||
Recipient recipient = Recipient.from(context, address, false);
|
Recipient recipient = Recipient.from(context, address, false);
|
||||||
|
|
||||||
if (recipient.isBlocked()) return;
|
if (recipient.isBlocked()) return;
|
||||||
|
|
||||||
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
// Notify the user
|
||||||
if (content.getDataMessage().hasGroup()) {
|
if (userPublicKey.equals(senderPublicKey)) {
|
||||||
GroupContext groupContext = content.getDataMessage().getGroup();
|
// sender is a linked device
|
||||||
groupInfo = Optional.of(new SignalServiceGroup(groupContext.getId().toByteArray(), SignalServiceGroup.GroupType.SIGNAL));
|
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);
|
||||||
|
} else {
|
||||||
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
|
||||||
|
duration * 1000L, true,
|
||||||
|
false,
|
||||||
|
Optional.absent(),
|
||||||
|
groupInfo,
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent());
|
||||||
|
//insert the timer update message
|
||||||
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||||
}
|
}
|
||||||
String updateMessage = UpdateMessageBuilder.INSTANCE.buildExpirationTimerMessage(context, duration, senderPublicKey, false);
|
|
||||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, content.getDataMessage().getTimestamp(), -1,
|
|
||||||
duration * 1000L, true,
|
|
||||||
false,
|
|
||||||
Optional.of(updateMessage),
|
|
||||||
groupInfo,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent());
|
|
||||||
|
|
||||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
||||||
|
|
||||||
|
//set the timer to the conversation
|
||||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
|
||||||
|
|
||||||
if (messageID != null) {
|
if (message.getId() != null) {
|
||||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(messageID);
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId());
|
||||||
}
|
}
|
||||||
} catch (MmsException e) {
|
} catch (MmsException e) {
|
||||||
Log.e("Loki", "Failed to insert expiration update message.");
|
Log.e("Loki", "Failed to insert expiration update message.");
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
Log.e("Loki", "Failed to insert expiration update message.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disableExpirationTimer(@Nullable Long messageID, @NotNull String senderPublicKey, @NotNull SignalServiceProtos.Content content) {
|
public void disableExpirationTimer(@NotNull ExpirationTimerUpdate message) {
|
||||||
setExpirationTimer(messageID, 0, senderPublicKey, content);
|
setExpirationTimer(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
package org.thoughtcrime.securesms.sskenvironment
|
||||||
|
|
||||||
|
class DataExtractionNotificationManager {
|
||||||
|
}
|
24
app/src/main/res/raw/lf_session_cert.pem
Normal file
24
app/src/main/res/raw/lf_session_cert.pem
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEEzCCAvugAwIBAgIUY9RQqbjhsQEkdeSgV9L0os9xZ7AwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwfDELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
|
||||||
|
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
|
||||||
|
HzAdBgNVBAMMFnB1YmxpYy5sb2tpLmZvdW5kYXRpb24wHhcNMjEwNDA3MDExMDMx
|
||||||
|
WhcNMjMwNDA3MDExMDMxWjB8MQswCQYDVQQGEwJBVTERMA8GA1UECAwIVmljdG9y
|
||||||
|
aWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2YWN5IFRl
|
||||||
|
Y2ggRm91bmRhdGlvbjEfMB0GA1UEAwwWcHVibGljLmxva2kuZm91bmRhdGlvbjCC
|
||||||
|
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5dBJSIR5+VNNUxUOo6FG0e
|
||||||
|
RmZteRqBt50KXGbOi2A23a6sa57pLFh9Yw3hmlWV+QCL7ipG1X4IC55OStgoesf+
|
||||||
|
K65VwEMP6Mtq0sSJS3R5TiuV2ZSRdSZTVjUyRXVe5T4Aw6wXVTAbc/HsyS780tDh
|
||||||
|
GclfDHhonPhZpmTAnSbfMOS+BfOnBNvDxdto0kVh6k5nrGlkT4ECloulHTQF2lwJ
|
||||||
|
0D6IOtv9AJplPdg6s2c4dY7durOdvr3NNVfvn5PTeRvbEPqzZur4WUUKIPNGu6mY
|
||||||
|
PxImqd4eUsL0Vod4aAsTIx4YMmCTi0m9W6zJI6nXcK/6a+iiA3+NTNMzEA9gQhEC
|
||||||
|
AwEAAaOBjDCBiTAdBgNVHQ4EFgQU/zahokxLvvFUpbnM6z/pwS1KsvwwHwYDVR0j
|
||||||
|
BBgwFoAU/zahokxLvvFUpbnM6z/pwS1KsvwwDwYDVR0TAQH/BAUwAwEB/zAhBgNV
|
||||||
|
HREEGjAYghZwdWJsaWMubG9raS5mb3VuZGF0aW9uMBMGA1UdJQQMMAoGCCsGAQUF
|
||||||
|
BwMBMA0GCSqGSIb3DQEBCwUAA4IBAQBql+JvoqpaYrFFTOuDn08U+pdcd3GM7tbI
|
||||||
|
zRH5LU+YnIpp9aRheek+2COW8DXsIy/kUngETCMLmX6ZaUj/WdHnTDkB0KTgxSHv
|
||||||
|
ad3ZznKPKZ26qJOklr+0ZWj4J3jHbisSzql6mqq7R2Kp4ESwzwqxvkbykM5RUnmz
|
||||||
|
Go/3Ol7bpN/ZVwwEkGfD/5rRHf57E/gZn2pBO+zotlQgr7HKRsIXQ2hIXVQqWmPQ
|
||||||
|
lvfIwrwAZlfES7BARFnHOpyVQxV8uNcV5K5eXzuVFjHBqvq+BtyGhWkP9yKJCHS9
|
||||||
|
OUXxch0rzRsH2C/kRVVhEk0pI3qlFiRC8pCJs98SNE9l69EQtG7I
|
||||||
|
-----END CERTIFICATE-----
|
25
app/src/main/res/raw/seed1cert.pem
Normal file
25
app/src/main/res/raw/seed1cert.pem
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEITCCAwmgAwIBAgIUJsox1ZQPK/6iDsCC+MUJfNAlFuYwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
|
||||||
|
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
|
||||||
|
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQxLmxva2kubmV0d29yazAeFw0yMTA0MDcw
|
||||||
|
MTE5MjZaFw0yMzA0MDcwMTE5MjZaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
|
||||||
|
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
|
||||||
|
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMS5sb2tp
|
||||||
|
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtWH3Rz8Dd
|
||||||
|
kEmM7tcBWHrJ/G8drr/+qidboEVYzxpyRjszaDxKXVhx4eBBsAD5RuCWuTuZmM8k
|
||||||
|
TKEDLtf8xfb5SQ7YNX+346s9NXS5Poy4CIPASiW/QWXgIHFbVdv2hC+cKOP61OLM
|
||||||
|
OGnOxfig6tQyd6EaCkedpY1DvSa2lPnQSOwC/jXCx6Vboc0zTY5R2bHtNc9hjIFP
|
||||||
|
F4VClLAQSh2F4R1V9MH5KZMW+CCP6oaJY658W9JYXYRwlLrL2EFOVxHgcxq/6+fw
|
||||||
|
+axXK9OXJrGZjuA+hiz+L/uAOtE4WuxrSeuNMHSrMtM9QqVn4bBuMJ21mAzfNoMP
|
||||||
|
OIwgMT9DwUjVAgMBAAGjgZAwgY0wHQYDVR0OBBYEFOubJp9SoXIw+ONiWgkOaW8K
|
||||||
|
zI/TMB8GA1UdIwQYMBaAFOubJp9SoXIw+ONiWgkOaW8KzI/TMA8GA1UdEwEB/wQF
|
||||||
|
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMS5sb2tpLm5ldHdvcmswEwYD
|
||||||
|
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAIiHNhNrjYvwXVWs
|
||||||
|
gacx8T/dpqpu9GE3L17LotgQr4R+IYHpNtcmwOTdtWWFfUTr75OCs+c3DqgRKEoj
|
||||||
|
lnULOsVcalpAGIvW15/fmZWOf66Dpa4+ljDmAc3SOQiD0gGNtqblgI5zG1HF38QP
|
||||||
|
hjYRhCZ5CVeGOLucvQ8tVVwQvArPFIkBr0jH9jHVgRWEI2MeI3FsU2H93D4TfGln
|
||||||
|
N4SmmCfYBqygaaZBWkJEt0bYhn8uGHdU9UY9L2FPtfHVKkmFgO7cASGlvXS7B/TT
|
||||||
|
/8IgbtM3O8mZc2asmdQhGwoAKz93ryyCd8X2UZJg/IwCSCayOlYZWY2fR4OPQmmV
|
||||||
|
gxJsm+g=
|
||||||
|
-----END CERTIFICATE-----
|
25
app/src/main/res/raw/seed3cert.pem
Normal file
25
app/src/main/res/raw/seed3cert.pem
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEITCCAwmgAwIBAgIUc486Dy9Y00bUFfDeYmJIgSS5xREwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
|
||||||
|
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
|
||||||
|
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQzLmxva2kubmV0d29yazAeFw0yMTA0MDcw
|
||||||
|
MTIwNTJaFw0yMzA0MDcwMTIwNTJaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
|
||||||
|
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
|
||||||
|
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMy5sb2tp
|
||||||
|
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtokMlsFzf
|
||||||
|
piYeD0EVNikMyvjltpF6fUEde9NOVrTtNTQT6kkDk+/0HF5LYgPaatv6v7fpUQHi
|
||||||
|
kIwd6F0LTRGeWDFdsaWMdtlR1n/GxLPrOROsE8dcLt6GLavPf9rDabgva93m/JD6
|
||||||
|
XW+Ne+MPEwqS8dAmFGhZd0gju6AtKFoSHnIf5pSQN6fSZUF/JQtHLVprAKKWKDiS
|
||||||
|
ZwmWbmrZR2aofLD/VRpetabajnZlv9EeWloQwvUsw1C1hkAmmtFeeXtg7ePwrOzo
|
||||||
|
6CnmcUJwOmi+LWqQV4A+58RZPFKaZoC5pzaKd0OYB8eZ8HB1F41UjGJgheX5Cyl4
|
||||||
|
+amfF3l8dSq1AgMBAAGjgZAwgY0wHQYDVR0OBBYEFM9VSq4pGydjtX92Beul4+ml
|
||||||
|
jBKtMB8GA1UdIwQYMBaAFM9VSq4pGydjtX92Beul4+mljBKtMA8GA1UdEwEB/wQF
|
||||||
|
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMy5sb2tpLm5ldHdvcmswEwYD
|
||||||
|
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAAYxmhhkcKE1n6g1
|
||||||
|
JqOa3UCBo4EfbqY5+FDZ0FVqv/cwemwVpKLbe6luRIS8poomdPCyMOS45V7wN3H9
|
||||||
|
cFpfJ1TW19ydPVKmCXrl29ngmnY1q7YDwE/4qi3VK/UiqDkTHMKWjVPkenOyi8u6
|
||||||
|
VVQANXSnKrn6GtigNFjGyD38O+j7AUSXBtXOJczaoF6r6BWgwQZ2WmgjuwvKTWSN
|
||||||
|
4r8uObERoAQYVaeXfgdr4e9X/JdskBDaLFfoW/rrSozHB4FqVNFW96k+aIUgRa5p
|
||||||
|
9kv115QcBPCSh9qOyTHij4tswS6SyOFaiKrNC4hgHQXP4QgioKmtsR/2Y+qJ6ddH
|
||||||
|
6oo+4QU=
|
||||||
|
-----END CERTIFICATE-----
|
@ -3,4 +3,22 @@
|
|||||||
<domain-config cleartextTrafficPermitted="true">
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||||
</domain-config>
|
</domain-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="false">
|
||||||
|
<domain includeSubdomains="false">public.loki.foundation</domain>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="@raw/lf_session_cert"/>
|
||||||
|
</trust-anchors>
|
||||||
|
</domain-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="false">
|
||||||
|
<domain includeSubdomains="false">storage.seed1.loki.network</domain>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="@raw/seed1cert"/>
|
||||||
|
</trust-anchors>
|
||||||
|
</domain-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="false">
|
||||||
|
<domain includeSubdomains="false">storage.seed3.loki.network</domain>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="@raw/seed3cert"/>
|
||||||
|
</trust-anchors>
|
||||||
|
</domain-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
@ -23,7 +23,7 @@ interface MessageDataProvider {
|
|||||||
|
|
||||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||||
|
|
||||||
fun insertAttachment(messageId: Long, attachmentId: Long, stream : InputStream)
|
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
||||||
|
|
||||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||||
|
|
||||||
@ -31,9 +31,9 @@ interface MessageDataProvider {
|
|||||||
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
|
||||||
|
|
||||||
// Quotes
|
// Quotes
|
||||||
fun getMessageForQuote(timestamp: Long, author: Address): Long?
|
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||||
fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment>
|
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||||
fun getMessageBodyFor(messageID: Long): String
|
fun getMessageBodyFor(timestamp: Long, author: String): String
|
||||||
|
|
||||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||||
|
@ -6,22 +6,21 @@ import android.net.Uri
|
|||||||
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
|
||||||
import org.session.libsession.messaging.jobs.MessageSendJob
|
import org.session.libsession.messaging.jobs.MessageSendJob
|
||||||
import org.session.libsession.messaging.messages.Message
|
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.opengroups.OpenGroup
|
||||||
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.linkpreview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.linkpreview.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
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSettings
|
import org.session.libsession.messaging.threads.recipients.Recipient.RecipientSettings
|
||||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||||
import org.session.libsignal.libsignal.ecc.ECPrivateKey
|
|
||||||
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.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
|
||||||
|
|
||||||
interface StorageProtocol {
|
interface StorageProtocol {
|
||||||
|
|
||||||
@ -32,9 +31,11 @@ interface StorageProtocol {
|
|||||||
fun getUserDisplayName(): String?
|
fun getUserDisplayName(): String?
|
||||||
fun getUserProfileKey(): ByteArray?
|
fun getUserProfileKey(): ByteArray?
|
||||||
fun getUserProfilePictureURL(): String?
|
fun getUserProfilePictureURL(): String?
|
||||||
|
fun setUserProfilePictureUrl(newProfilePicture: String)
|
||||||
|
|
||||||
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
|
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
|
||||||
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
|
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
|
||||||
|
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
|
||||||
|
|
||||||
// Signal Protocol
|
// Signal Protocol
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ interface StorageProtocol {
|
|||||||
// Open Groups
|
// Open Groups
|
||||||
fun getOpenGroup(threadID: String): OpenGroup?
|
fun getOpenGroup(threadID: String): OpenGroup?
|
||||||
fun getThreadID(openGroupID: String): String?
|
fun getThreadID(openGroupID: String): String?
|
||||||
fun getAllOpenGroups(): Map<Long, PublicChat>
|
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||||
fun addOpenGroup(server: String, channel: Long)
|
fun addOpenGroup(server: String, channel: Long)
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
||||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||||
@ -95,6 +96,7 @@ interface StorageProtocol {
|
|||||||
// 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 getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||||
fun markAsSent(timestamp: Long, author: String)
|
fun markAsSent(timestamp: Long, author: String)
|
||||||
@ -104,11 +106,13 @@ interface StorageProtocol {
|
|||||||
// Closed Groups
|
// Closed Groups
|
||||||
fun getGroup(groupID: String): GroupRecord?
|
fun getGroup(groupID: String): GroupRecord?
|
||||||
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 setActive(groupID: String, value: Boolean)
|
fun setActive(groupID: String, value: Boolean)
|
||||||
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>)
|
||||||
// Closed Group
|
// Closed Group
|
||||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||||
|
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
||||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||||
fun removeClosedGroupPublicKey(groupPublicKey: String)
|
fun removeClosedGroupPublicKey(groupPublicKey: String)
|
||||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||||
@ -140,11 +144,13 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Loki User
|
// Loki User
|
||||||
fun getDisplayName(publicKey: String): String?
|
fun getDisplayName(publicKey: String): String?
|
||||||
|
fun setDisplayName(publicKey: String, newName: String)
|
||||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
||||||
fun getProfilePictureURL(publicKey: String): String?
|
fun getProfilePictureURL(publicKey: String): String?
|
||||||
|
|
||||||
// Recipient
|
// Recipient
|
||||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||||
|
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||||
|
|
||||||
// PartAuthority
|
// PartAuthority
|
||||||
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
|
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
|
||||||
@ -152,5 +158,5 @@ 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?): Long?
|
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
|
||||||
}
|
}
|
@ -5,6 +5,8 @@ import org.session.libsession.messaging.fileserver.FileServerAPI
|
|||||||
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.libsignal.service.api.crypto.AttachmentCipherInputStream
|
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
|
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
internal sealed class Error(val description: String) : Exception() {
|
internal sealed class Error(val description: String) : Exception(description) {
|
||||||
object NoAttachment : Error("No such attachment.")
|
object NoAttachment : Error("No such attachment.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,12 +34,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
val messageDataProvider = MessagingConfiguration.shared.messageDataProvider
|
|
||||||
val attachmentStream = messageDataProvider.getAttachmentStream(attachmentID) ?: return handleFailure(Error.NoAttachment)
|
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
|
||||||
val tempFile = createTempFile()
|
|
||||||
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
val handleFailure: (java.lang.Exception) -> Unit = { exception ->
|
||||||
tempFile.delete()
|
|
||||||
if(exception is Error && exception == Error.NoAttachment) {
|
if(exception is Error && exception == Error.NoAttachment) {
|
||||||
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||||
this.handlePermanentFailure(exception)
|
this.handlePermanentFailure(exception)
|
||||||
@ -51,24 +48,30 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
FileServerAPI.shared.downloadFile(tempFile, attachmentStream.url, MAX_ATTACHMENT_SIZE, attachmentStream.listener)
|
val messageDataProvider = MessagingConfiguration.shared.messageDataProvider
|
||||||
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment)
|
||||||
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
|
val tempFile = createTempFile()
|
||||||
|
|
||||||
|
FileServerAPI.shared.downloadFile(tempFile, attachment.url, MAX_ATTACHMENT_SIZE, null)
|
||||||
|
|
||||||
|
// DECRYPTION
|
||||||
|
|
||||||
|
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||||
|
val stream = if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
|
||||||
|
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||||
|
|
||||||
|
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
||||||
|
|
||||||
|
tempFile.delete()
|
||||||
|
handleSuccess()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return handleFailure(e)
|
return handleFailure(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DECRYPTION
|
|
||||||
|
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
|
||||||
var stream = if (!attachmentStream.digest.isPresent || attachmentStream.key == null) FileInputStream(tempFile)
|
|
||||||
else AttachmentCipherInputStream.createForAttachment(tempFile, attachmentStream.length.or(0).toLong(), attachmentStream.key?.toByteArray(), attachmentStream?.digest.get())
|
|
||||||
|
|
||||||
messageDataProvider.insertAttachment(databaseMessageID, attachmentID, stream)
|
|
||||||
|
|
||||||
tempFile.delete()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess() {
|
private fun handleSuccess() {
|
||||||
|
Log.w(AttachmentUploadJob.TAG, "Attachment downloaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ 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.utilities.PlaintextOutputStreamFactory
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
|
||||||
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 {
|
||||||
@ -25,7 +24,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
internal sealed class Error(val description: String) : Exception() {
|
internal sealed class Error(val description: String) : Exception(description) {
|
||||||
object NoAttachment : Error("No such attachment.")
|
object NoAttachment : Error("No such attachment.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,41 +44,40 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
ThreadUtils.queue {
|
try {
|
||||||
try {
|
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||||
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
?: return@queue handleFailure(Error.NoAttachment)
|
|
||||||
|
|
||||||
var server = FileServerAPI.shared.server
|
var server = FileServerAPI.shared.server
|
||||||
var shouldEncrypt = true
|
var shouldEncrypt = true
|
||||||
val usePadding = false
|
val usePadding = false
|
||||||
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
|
||||||
openGroup?.let {
|
openGroup?.let {
|
||||||
server = it.server
|
server = it.server
|
||||||
shouldEncrypt = false
|
shouldEncrypt = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachmentKey = Util.getSecretBytes(64)
|
val attachmentKey = Util.getSecretBytes(64)
|
||||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||||
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
|
||||||
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
|
||||||
|
|
||||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||||
|
|
||||||
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
||||||
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 is Error && 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)
|
||||||
} else {
|
} else {
|
||||||
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) {
|
||||||
|
@ -1,32 +1,51 @@
|
|||||||
package org.session.libsession.messaging.jobs
|
package org.session.libsession.messaging.jobs
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import java.util.Timer
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
|
||||||
class JobQueue : JobDelegate {
|
class JobQueue : JobDelegate {
|
||||||
private var hasResumedPendingJobs = false // Just for debugging
|
private var hasResumedPendingJobs = false // Just for debugging
|
||||||
|
|
||||||
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
private val scope = GlobalScope + SupervisorJob()
|
||||||
|
private val queue = Channel<Job>(UNLIMITED)
|
||||||
|
val timer = Timer()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// process jobs
|
||||||
|
scope.launch(dispatcher) {
|
||||||
|
while (isActive) {
|
||||||
|
queue.receive().let { job ->
|
||||||
|
job.delegate = this@JobQueue
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
val shared: JobQueue by lazy { JobQueue() }
|
val shared: JobQueue by lazy { JobQueue() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun add(job: Job) {
|
fun add(job: Job) {
|
||||||
addWithoutExecuting(job)
|
addWithoutExecuting(job)
|
||||||
job.execute()
|
queue.offer(job) // offer always called on unlimited capacity
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addWithoutExecuting(job: Job) {
|
private fun addWithoutExecuting(job: Job) {
|
||||||
job.id = System.currentTimeMillis().toString()
|
job.id = System.currentTimeMillis().toString()
|
||||||
MessagingConfiguration.shared.storage.persistJob(job)
|
MessagingConfiguration.shared.storage.persistJob(job)
|
||||||
job.delegate = this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumePendingJobs() {
|
fun resumePendingJobs() {
|
||||||
@ -40,8 +59,7 @@ class JobQueue : JobDelegate {
|
|||||||
val allPendingJobs = MessagingConfiguration.shared.storage.getAllPendingJobs(type)
|
val allPendingJobs = MessagingConfiguration.shared.storage.getAllPendingJobs(type)
|
||||||
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
allPendingJobs.sortedBy { it.id }.forEach { job ->
|
||||||
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.")
|
||||||
job.delegate = this
|
queue.offer(job) // offer always called on unlimited capacity
|
||||||
job.execute()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,9 +78,9 @@ class JobQueue : JobDelegate {
|
|||||||
} else {
|
} else {
|
||||||
val retryInterval = getRetryInterval(job)
|
val retryInterval = getRetryInterval(job)
|
||||||
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).")
|
||||||
Timer().schedule(delay = retryInterval) {
|
timer.schedule(delay = retryInterval) {
|
||||||
Log.i("Jobs", "Retrying ${job::class.simpleName}.")
|
Log.i("Jobs", "Retrying ${job::class.simpleName}.")
|
||||||
job.execute()
|
queue.offer(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
val TAG = MessageReceiveJob::class.simpleName
|
val TAG = MessageReceiveJob::class.simpleName
|
||||||
val KEY: String = "MessageReceiveJob"
|
val KEY: String = "MessageReceiveJob"
|
||||||
|
|
||||||
|
private val RECEIVE_LOCK = Object()
|
||||||
|
|
||||||
//keys used for database storage purpose
|
//keys used for database storage purpose
|
||||||
private val KEY_DATA = "data"
|
private val KEY_DATA = "data"
|
||||||
private val KEY_IS_BACKGROUND_POLL = "is_background_poll"
|
private val KEY_IS_BACKGROUND_POLL = "is_background_poll"
|
||||||
@ -34,17 +36,19 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
|
|||||||
try {
|
try {
|
||||||
val isRetry: Boolean = failureCount != 0
|
val isRetry: Boolean = failureCount != 0
|
||||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
|
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry)
|
||||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
synchronized(RECEIVE_LOCK) {
|
||||||
|
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||||
|
}
|
||||||
this.handleSuccess()
|
this.handleSuccess()
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, "Couldn't receive message due to error: $e.")
|
Log.e(TAG, "Couldn't receive message due to error", e)
|
||||||
val error = e as? MessageReceiver.Error
|
val error = e as? MessageReceiver.Error
|
||||||
if (error != null && !error.isRetryable) {
|
if (error != null && !error.isRetryable) {
|
||||||
Log.d("Loki", "Message receive job permanently failed due to error: $error.")
|
Log.e("Loki", "Message receive job permanently failed due to error", e)
|
||||||
this.handlePermanentFailure(error)
|
this.handlePermanentFailure(error)
|
||||||
} else {
|
} else {
|
||||||
Log.d("Loki", "Couldn't receive message due to error: $e.")
|
Log.e("Loki", "Couldn't receive message due to error", e)
|
||||||
this.handleFailure(e)
|
this.handleFailure(e)
|
||||||
}
|
}
|
||||||
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||||
|
@ -8,12 +8,12 @@ import org.session.libsession.utilities.GroupUtil
|
|||||||
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.utilities.logging.Log
|
|
||||||
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.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.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class ClosedGroupControlMessage() : ControlMessage() {
|
class ClosedGroupControlMessage() : ControlMessage() {
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
class MemberLeft() : Kind()
|
class MemberLeft() : Kind()
|
||||||
class EncryptionKeyPairRequest(): Kind()
|
class EncryptionKeyPairRequest(): Kind()
|
||||||
|
|
||||||
val description: String = run {
|
val description: String =
|
||||||
when(this) {
|
when(this) {
|
||||||
is New -> "new"
|
is New -> "new"
|
||||||
is Update -> "update"
|
is Update -> "update"
|
||||||
@ -66,14 +66,14 @@ class ClosedGroupControlMessage() : ControlMessage() {
|
|||||||
is MemberLeft -> "memberLeft"
|
is MemberLeft -> "memberLeft"
|
||||||
is EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
is EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ClosedGroupControlMessage"
|
const val TAG = "ClosedGroupControlMessage"
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
|
||||||
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage ?: return null
|
if (!proto.hasDataMessage() || !proto.dataMessage.hasClosedGroupControlMessage()) return null
|
||||||
|
val closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage!!
|
||||||
val kind: Kind
|
val kind: Kind
|
||||||
when(closedGroupControlMessageProto.type) {
|
when(closedGroupControlMessageProto.type) {
|
||||||
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
DataMessage.ClosedGroupControlMessage.Type.NEW -> {
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
|
class DataExtractionNotification(): ControlMessage() {
|
||||||
|
var kind: Kind? = null
|
||||||
|
|
||||||
|
// Kind enum
|
||||||
|
sealed class Kind {
|
||||||
|
class Screenshot() : Kind()
|
||||||
|
class MediaSaved(val timestanp: Long) : Kind()
|
||||||
|
|
||||||
|
val description: String =
|
||||||
|
when(this) {
|
||||||
|
is Screenshot -> "screenshot"
|
||||||
|
is MediaSaved -> "mediaSaved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DataExtractionNotification"
|
||||||
|
|
||||||
|
fun fromProto(proto: SignalServiceProtos.Content): DataExtractionNotification? {
|
||||||
|
if (!proto.hasDataExtractionNotification()) return null
|
||||||
|
val dataExtractionNotification = proto.dataExtractionNotification!!
|
||||||
|
val kind: Kind = when(dataExtractionNotification.type) {
|
||||||
|
SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT -> Kind.Screenshot()
|
||||||
|
SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED -> {
|
||||||
|
val timestamp = if (dataExtractionNotification.hasTimestamp()) dataExtractionNotification.timestamp else return null
|
||||||
|
Kind.MediaSaved(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DataExtractionNotification(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//constructor
|
||||||
|
internal constructor(kind: Kind) : this() {
|
||||||
|
this.kind = kind
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Validation
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (!super.isValid()) return false
|
||||||
|
val kind = kind ?: return false
|
||||||
|
return when(kind) {
|
||||||
|
is Kind.Screenshot -> true
|
||||||
|
is Kind.MediaSaved -> kind.timestanp > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
|
val kind = kind
|
||||||
|
if (kind == null) {
|
||||||
|
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val dataExtractionNotification = SignalServiceProtos.DataExtractionNotification.newBuilder()
|
||||||
|
when(kind) {
|
||||||
|
is Kind.Screenshot -> dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.SCREENSHOT
|
||||||
|
is Kind.MediaSaved -> {
|
||||||
|
dataExtractionNotification.type = SignalServiceProtos.DataExtractionNotification.Type.MEDIA_SAVED
|
||||||
|
dataExtractionNotification.timestamp = kind.timestanp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||||
|
contentProto.dataExtractionNotification = dataExtractionNotification.build()
|
||||||
|
return contentProto.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Couldn't construct data extraction notification proto from: $this")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,23 +7,29 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
|
|||||||
|
|
||||||
class ExpirationTimerUpdate() : ControlMessage() {
|
class ExpirationTimerUpdate() : ControlMessage() {
|
||||||
|
|
||||||
|
/// In the case of a sync message, the public key of the person the message was targeted at.
|
||||||
|
/// - Note: `nil` if this isn't a sync message.
|
||||||
var syncTarget: String? = null
|
var syncTarget: String? = null
|
||||||
var duration: Int? = 0
|
var duration: Int? = 0
|
||||||
|
|
||||||
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ExpirationTimerUpdate"
|
const val TAG = "ExpirationTimerUpdate"
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? {
|
fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? {
|
||||||
val dataMessageProto = proto.dataMessage ?: return null
|
val dataMessageProto = if (proto.hasDataMessage()) proto.dataMessage else return null
|
||||||
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
|
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
|
||||||
if (!isExpirationTimerUpdate) return null
|
if (!isExpirationTimerUpdate) return null
|
||||||
|
val syncTarget = dataMessageProto.syncTarget
|
||||||
val duration = dataMessageProto.expireTimer
|
val duration = dataMessageProto.expireTimer
|
||||||
return ExpirationTimerUpdate(duration)
|
return ExpirationTimerUpdate(syncTarget, duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//constructor
|
//constructor
|
||||||
internal constructor(duration: Int) : this() {
|
internal constructor(syncTarget: String?, duration: Int) : this() {
|
||||||
|
this.syncTarget = syncTarget
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +48,10 @@ class ExpirationTimerUpdate() : ControlMessage() {
|
|||||||
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
|
||||||
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
|
||||||
dataMessageProto.expireTimer = duration
|
dataMessageProto.expireTimer = duration
|
||||||
syncTarget?.let { dataMessageProto.syncTarget = it }
|
// Sync target
|
||||||
|
if (syncTarget != null) {
|
||||||
|
dataMessageProto.syncTarget = syncTarget
|
||||||
|
}
|
||||||
// Group context
|
// Group context
|
||||||
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.session.libsession.messaging.messages.control
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class ReadReceipt() : ControlMessage() {
|
class ReadReceipt() : ControlMessage() {
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ class ReadReceipt() : ControlMessage() {
|
|||||||
const val TAG = "ReadReceipt"
|
const val TAG = "ReadReceipt"
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? {
|
fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? {
|
||||||
val receiptProto = proto.receiptMessage ?: return null
|
val receiptProto = if (proto.hasReceiptMessage()) proto.receiptMessage else return null
|
||||||
if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null
|
if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null
|
||||||
val timestamps = receiptProto.timestampList
|
val timestamps = receiptProto.timestampList
|
||||||
if (timestamps.isEmpty()) return null
|
if (timestamps.isEmpty()) return null
|
||||||
@ -35,7 +35,7 @@ class ReadReceipt() : ControlMessage() {
|
|||||||
override fun toProto(): SignalServiceProtos.Content? {
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
val timestamps = timestamps
|
val timestamps = timestamps
|
||||||
if (timestamps == null) {
|
if (timestamps == null) {
|
||||||
Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this")
|
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder()
|
val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder()
|
||||||
@ -46,7 +46,7 @@ class ReadReceipt() : ControlMessage() {
|
|||||||
contentProto.receiptMessage = receiptProto.build()
|
contentProto.receiptMessage = receiptProto.build()
|
||||||
return contentProto.build()
|
return contentProto.build()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this")
|
Log.w(TAG, "Couldn't construct read receipt proto from: $this")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package org.session.libsession.messaging.messages.control
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
class TypingIndicator() : ControlMessage() {
|
class TypingIndicator() : ControlMessage() {
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ class TypingIndicator() : ControlMessage() {
|
|||||||
const val TAG = "TypingIndicator"
|
const val TAG = "TypingIndicator"
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? {
|
fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? {
|
||||||
val typingIndicatorProto = proto.typingMessage ?: return null
|
val typingIndicatorProto = if (proto.hasTypingMessage()) proto.typingMessage else return null
|
||||||
val kind = Kind.fromProto(typingIndicatorProto.action)
|
val kind = Kind.fromProto(typingIndicatorProto.action)
|
||||||
return TypingIndicator(kind = kind)
|
return TypingIndicator(kind = kind)
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ package org.session.libsession.messaging.messages.signal;
|
|||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
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.PointerAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
||||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
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.utilities.GroupUtil;
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
|
||||||
@ -68,12 +68,12 @@ public class IncomingMediaMessage {
|
|||||||
Address from,
|
Address from,
|
||||||
long expiresIn,
|
long expiresIn,
|
||||||
Optional<SignalServiceGroup> group,
|
Optional<SignalServiceGroup> group,
|
||||||
Optional<List<SignalServiceAttachment>> attachments,
|
List<SignalServiceAttachment> attachments,
|
||||||
Optional<QuoteModel> quote,
|
Optional<QuoteModel> quote,
|
||||||
Optional<List<LinkPreview>> linkPreviews)
|
Optional<List<LinkPreview>> linkPreviews)
|
||||||
{
|
{
|
||||||
return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false,
|
return new IncomingMediaMessage(from, message.getSentTimestamp(), -1, expiresIn, false,
|
||||||
false, Optional.fromNullable(message.getText()), group, attachments, quote, Optional.absent(), linkPreviews);
|
false, Optional.fromNullable(message.getText()), group, Optional.fromNullable(attachments), quote, Optional.absent(), linkPreviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSubscriptionId() {
|
public int getSubscriptionId() {
|
||||||
|
@ -2,6 +2,7 @@ package org.session.libsession.messaging.messages.signal;
|
|||||||
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
@ -100,7 +101,7 @@ public class IncomingTextMessage implements Parcelable {
|
|||||||
Optional<SignalServiceGroup> group,
|
Optional<SignalServiceGroup> group,
|
||||||
long expiresInMillis)
|
long expiresInMillis)
|
||||||
{
|
{
|
||||||
return new IncomingTextMessage(sender, 1, message.getReceivedTimestamp(), message.getText(), group, expiresInMillis, false);
|
return new IncomingTextMessage(sender, 1, message.getSentTimestamp(), message.getText(), group, expiresInMillis, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSubscriptionId() {
|
public int getSubscriptionId() {
|
||||||
|
@ -11,8 +11,8 @@ import java.util.LinkedList;
|
|||||||
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
|
||||||
|
|
||||||
public OutgoingExpirationUpdateMessage(Recipient recipient, String body, long sentTimeMillis, long expiresIn) {
|
public OutgoingExpirationUpdateMessage(Recipient recipient, String body, long sentTimeMillis, long expiresIn) {
|
||||||
super(recipient, body, new LinkedList<Attachment>(), sentTimeMillis,
|
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||||
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
|
DistributionTypes.CONVERSATION, expiresIn, true, null, Collections.emptyList(),
|
||||||
Collections.emptyList());
|
Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
|
||||||
DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
|
DistributionTypes.CONVERSATION, expiresIn, false, quote, contacts, previews);
|
||||||
|
|
||||||
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews);
|
||||||
|
|
||||||
this.group = group;
|
this.group = group;
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||||
sentTime,
|
sentTime,
|
||||||
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
|
DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews);
|
||||||
|
|
||||||
this.group = group;
|
this.group = group;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ public class OutgoingMediaMessage {
|
|||||||
private final int distributionType;
|
private final int distributionType;
|
||||||
private final int subscriptionId;
|
private final int subscriptionId;
|
||||||
private final long expiresIn;
|
private final long expiresIn;
|
||||||
|
private final boolean expirationUpdate;
|
||||||
private final QuoteModel outgoingQuote;
|
private final QuoteModel outgoingQuote;
|
||||||
|
|
||||||
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
private final List<NetworkFailure> networkFailures = new LinkedList<>();
|
||||||
@ -36,6 +37,7 @@ public class OutgoingMediaMessage {
|
|||||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||||
List<Attachment> attachments, long sentTimeMillis,
|
List<Attachment> attachments, long sentTimeMillis,
|
||||||
int subscriptionId, long expiresIn,
|
int subscriptionId, long expiresIn,
|
||||||
|
boolean expirationUpdate,
|
||||||
int distributionType,
|
int distributionType,
|
||||||
@Nullable QuoteModel outgoingQuote,
|
@Nullable QuoteModel outgoingQuote,
|
||||||
@NonNull List<Contact> contacts,
|
@NonNull List<Contact> contacts,
|
||||||
@ -50,6 +52,7 @@ public class OutgoingMediaMessage {
|
|||||||
this.attachments = attachments;
|
this.attachments = attachments;
|
||||||
this.subscriptionId = subscriptionId;
|
this.subscriptionId = subscriptionId;
|
||||||
this.expiresIn = expiresIn;
|
this.expiresIn = expiresIn;
|
||||||
|
this.expirationUpdate = expirationUpdate;
|
||||||
this.outgoingQuote = outgoingQuote;
|
this.outgoingQuote = outgoingQuote;
|
||||||
|
|
||||||
this.contacts.addAll(contacts);
|
this.contacts.addAll(contacts);
|
||||||
@ -66,6 +69,7 @@ public class OutgoingMediaMessage {
|
|||||||
this.sentTimeMillis = that.sentTimeMillis;
|
this.sentTimeMillis = that.sentTimeMillis;
|
||||||
this.subscriptionId = that.subscriptionId;
|
this.subscriptionId = that.subscriptionId;
|
||||||
this.expiresIn = that.expiresIn;
|
this.expiresIn = that.expiresIn;
|
||||||
|
this.expirationUpdate = that.expirationUpdate;
|
||||||
this.outgoingQuote = that.outgoingQuote;
|
this.outgoingQuote = that.outgoingQuote;
|
||||||
|
|
||||||
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
|
||||||
@ -85,7 +89,7 @@ public class OutgoingMediaMessage {
|
|||||||
previews = Collections.singletonList(linkPreview);
|
previews = Collections.singletonList(linkPreview);
|
||||||
}
|
}
|
||||||
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
|
||||||
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
recipient.getExpireMessages() * 1000, false, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
|
||||||
previews, Collections.emptyList(), Collections.emptyList());
|
previews, Collections.emptyList(), Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,9 +113,7 @@ public class OutgoingMediaMessage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isExpirationUpdate() {
|
public boolean isExpirationUpdate() { return expirationUpdate; }
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getSentTimeMillis() {
|
public long getSentTimeMillis() {
|
||||||
return sentTimeMillis;
|
return sentTimeMillis;
|
||||||
|
@ -19,11 +19,12 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||||||
long sentTimeMillis,
|
long sentTimeMillis,
|
||||||
int distributionType,
|
int distributionType,
|
||||||
long expiresIn,
|
long expiresIn,
|
||||||
|
boolean expirationUpdate,
|
||||||
@Nullable QuoteModel quote,
|
@Nullable QuoteModel quote,
|
||||||
@NonNull List<Contact> contacts,
|
@NonNull List<Contact> contacts,
|
||||||
@NonNull List<LinkPreview> previews)
|
@NonNull List<LinkPreview> previews)
|
||||||
{
|
{
|
||||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, expirationUpdate, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||||
|
@ -9,16 +9,18 @@ public class OutgoingTextMessage {
|
|||||||
private final String message;
|
private final String message;
|
||||||
private final int subscriptionId;
|
private final int subscriptionId;
|
||||||
private final long expiresIn;
|
private final long expiresIn;
|
||||||
|
private final long sentTimestampMillis;
|
||||||
|
|
||||||
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId) {
|
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId, long sentTimestampMillis) {
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.expiresIn = expiresIn;
|
this.expiresIn = expiresIn;
|
||||||
this.subscriptionId = subscriptionId;
|
this.subscriptionId = subscriptionId;
|
||||||
|
this.sentTimestampMillis = sentTimestampMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
|
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
|
||||||
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1);
|
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1, message.getSentTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getExpiresIn() {
|
public long getExpiresIn() {
|
||||||
@ -37,6 +39,10 @@ public class OutgoingTextMessage {
|
|||||||
return recipient;
|
return recipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getSentTimestampMillis() {
|
||||||
|
return sentTimestampMillis;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSecureMessage() {
|
public boolean isSecureMessage() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ package org.session.libsession.messaging.messages.visible
|
|||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
|
||||||
|
import org.session.libsignal.libsignal.util.guava.Optional
|
||||||
|
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ class Attachment {
|
|||||||
var url: String? = null
|
var url: String? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromProto(proto: SignalServiceProtos.AttachmentPointer): Attachment? {
|
fun fromProto(proto: SignalServiceProtos.AttachmentPointer): Attachment {
|
||||||
val result = Attachment()
|
val result = Attachment()
|
||||||
result.fileName = proto.fileName
|
result.fileName = proto.fileName
|
||||||
fun inferContentType(): String {
|
fun inferContentType(): String {
|
||||||
@ -100,8 +100,14 @@ class Attachment {
|
|||||||
|
|
||||||
fun toSignalAttachment(): SignalAttachment? {
|
fun toSignalAttachment(): SignalAttachment? {
|
||||||
if (!isValid()) return null
|
if (!isValid()) return null
|
||||||
return DatabaseAttachment(null, 0, false, false, contentType, 0,
|
return PointerAttachment.forAttachment((this))
|
||||||
sizeInBytes?.toLong() ?: 0, fileName, null, key.toString(), null, digest, null, kind == Kind.VOICE_MESSAGE,
|
|
||||||
size?.width ?: 0, size?.height ?: 0, false, caption, url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toSignalPointer(): SignalServiceAttachmentPointer? {
|
||||||
|
if (!isValid()) return null
|
||||||
|
return SignalServiceAttachmentPointer(0, contentType, key, Optional.fromNullable(sizeInBytes), null,
|
||||||
|
size?.width ?: 0, size?.height ?: 0, Optional.fromNullable(digest), Optional.fromNullable(fileName),
|
||||||
|
kind == Kind.VOICE_MESSAGE, Optional.fromNullable(caption), url)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ class VisibleMessage : Message() {
|
|||||||
|
|
||||||
var syncTarget: String? = null
|
var syncTarget: String? = null
|
||||||
var text: String? = null
|
var text: String? = null
|
||||||
var attachmentIDs = ArrayList<Long>()
|
val attachmentIDs: MutableList<Long> = mutableListOf()
|
||||||
var quote: Quote? = null
|
var quote: Quote? = null
|
||||||
var linkPreview: LinkPreview? = null
|
var linkPreview: LinkPreview? = null
|
||||||
var contact: Contact? = null
|
var contact: Contact? = null
|
||||||
@ -27,17 +27,19 @@ class VisibleMessage : Message() {
|
|||||||
const val TAG = "VisibleMessage"
|
const val TAG = "VisibleMessage"
|
||||||
|
|
||||||
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? {
|
||||||
val dataMessage = proto.dataMessage ?: return null
|
val dataMessage = if (proto.hasDataMessage()) proto.dataMessage else return null
|
||||||
val result = VisibleMessage()
|
val result = VisibleMessage()
|
||||||
result.syncTarget = dataMessage.syncTarget
|
if (dataMessage.hasSyncTarget()) {
|
||||||
|
result.syncTarget = dataMessage.syncTarget
|
||||||
|
}
|
||||||
result.text = dataMessage.body
|
result.text = dataMessage.body
|
||||||
// Attachments are handled in MessageReceiver
|
// Attachments are handled in MessageReceiver
|
||||||
val quoteProto = dataMessage.quote
|
val quoteProto = if (dataMessage.hasQuote()) dataMessage.quote else null
|
||||||
quoteProto?.let {
|
quoteProto?.let {
|
||||||
val quote = Quote.fromProto(quoteProto)
|
val quote = Quote.fromProto(quoteProto)
|
||||||
quote?.let { result.quote = quote }
|
quote?.let { result.quote = quote }
|
||||||
}
|
}
|
||||||
val linkPreviewProto = dataMessage.previewList.first()
|
val linkPreviewProto = dataMessage.previewList.firstOrNull()
|
||||||
linkPreviewProto?.let {
|
linkPreviewProto?.let {
|
||||||
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
val linkPreview = LinkPreview.fromProto(linkPreviewProto)
|
||||||
linkPreview?.let { result.linkPreview = linkPreview }
|
linkPreview?.let { result.linkPreview = linkPreview }
|
||||||
@ -54,7 +56,7 @@ class VisibleMessage : Message() {
|
|||||||
val databaseAttachment = it as DatabaseAttachment
|
val databaseAttachment = it as DatabaseAttachment
|
||||||
databaseAttachment.attachmentId.rowId
|
databaseAttachment.attachmentId.rowId
|
||||||
}
|
}
|
||||||
this.attachmentIDs = attachmentIDs as ArrayList<Long>
|
this.attachmentIDs.addAll(attachmentIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMediaMessage(): Boolean {
|
fun isMediaMessage(): Boolean {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.session.libsession.messaging.opengroups
|
package org.session.libsession.messaging.opengroups
|
||||||
|
|
||||||
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
|
||||||
data class OpenGroup(
|
data class OpenGroup(
|
||||||
@ -13,6 +14,9 @@ data class OpenGroup(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic fun from(publicChat: PublicChat): OpenGroup =
|
||||||
|
OpenGroup(publicChat.channel, publicChat.server, publicChat.displayName, publicChat.isDeletable)
|
||||||
|
|
||||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
@JvmStatic fun getId(channel: Long, server: String): String {
|
||||||
return "$server.$channel"
|
return "$server.$channel"
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,13 @@ import nl.komponents.kovenant.deferred
|
|||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import nl.komponents.kovenant.then
|
import nl.komponents.kovenant.then
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
|
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
|
||||||
import org.session.libsession.messaging.fileserver.FileServerAPI
|
import org.session.libsession.messaging.fileserver.FileServerAPI
|
||||||
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -156,6 +154,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
||||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -188,6 +187,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
val deferred = deferred<OpenGroupMessage, Exception>()
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
@ -252,6 +252,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
||||||
try {
|
try {
|
||||||
@ -270,6 +271,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
val parameters = mapOf( "include_annotations" to 1 )
|
val parameters = mapOf( "include_annotations" to 1 )
|
||||||
@ -294,6 +296,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
storage.setUserCount(channel, server, info.memberCount)
|
storage.setUserCount(channel, server, info.memberCount)
|
||||||
@ -307,6 +310,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
||||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
||||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||||
@ -323,6 +327,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
||||||
@ -331,6 +336,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
||||||
return retryIfNeeded(maxRetryCount) {
|
return retryIfNeeded(maxRetryCount) {
|
||||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
||||||
@ -348,6 +354,7 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
||||||
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
||||||
val mapping = mutableMapOf<String, String>()
|
val mapping = mutableMapOf<String, String>()
|
||||||
@ -362,12 +369,14 @@ object OpenGroupAPI: DotNetAPI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
||||||
Log.d("Loki", "Updating display name on server: $server.")
|
Log.d("Loki", "Updating display name on server: $server.")
|
||||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
||||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@ package org.session.libsession.messaging.opengroups
|
|||||||
|
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.whispersystems.curve25519.Curve25519
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
data class OpenGroupMessage(
|
data class OpenGroupMessage(
|
||||||
@ -26,6 +26,7 @@ data class OpenGroupMessage(
|
|||||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||||
|
val attachmentIDs = message.attachmentIDs
|
||||||
// Validation
|
// Validation
|
||||||
if (!message.isValid()) { return null } // Should be valid at this point
|
if (!message.isValid()) { return null } // Should be valid at this point
|
||||||
// Quote
|
// Quote
|
||||||
@ -41,7 +42,8 @@ data class OpenGroupMessage(
|
|||||||
}()
|
}()
|
||||||
// Message
|
// Message
|
||||||
val displayname = storage.getUserDisplayName() ?: "Anonymous"
|
val displayname = storage.getUserDisplayName() ?: "Anonymous"
|
||||||
val body = message.text ?: message.sentTimestamp.toString() // The back-end doesn't accept messages without a body so we use this as a workaround
|
val text = message.text
|
||||||
|
val body = if (text.isNullOrEmpty()) message.sentTimestamp.toString() else text // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||||
val result = OpenGroupMessage(null, userPublicKey, displayname, body, message.sentTimestamp!!, OpenGroupAPI.openGroupMessageType, quote, mutableListOf(), null, null, 0)
|
val result = OpenGroupMessage(null, userPublicKey, displayname, body, message.sentTimestamp!!, OpenGroupAPI.openGroupMessageType, quote, mutableListOf(), null, null, 0)
|
||||||
// Link preview
|
// Link preview
|
||||||
val linkPreview = message.linkPreview
|
val linkPreview = message.linkPreview
|
||||||
|
@ -4,14 +4,16 @@ import org.session.libsession.messaging.MessagingConfiguration
|
|||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
import org.session.libsession.messaging.messages.control.*
|
import org.session.libsession.messaging.messages.control.*
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
|
||||||
object MessageReceiver {
|
object MessageReceiver {
|
||||||
|
|
||||||
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
|
||||||
|
|
||||||
internal sealed class Error(val description: String) : Exception() {
|
internal sealed class Error(val description: String) : Exception(description) {
|
||||||
object DuplicateMessage: Error("Duplicate message.")
|
object DuplicateMessage: Error("Duplicate message.")
|
||||||
object InvalidMessage: Error("Invalid message.")
|
object InvalidMessage: Error("Invalid message.")
|
||||||
object UnknownMessage: Error("Unknown message type.")
|
object UnknownMessage: Error("Unknown message type.")
|
||||||
@ -50,8 +52,7 @@ object MessageReceiver {
|
|||||||
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
|
// If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp
|
||||||
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
|
// will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround
|
||||||
// for this issue.
|
// for this issue.
|
||||||
if (storage.isMessageDuplicated(envelope.timestamp, envelope.source) && !isRetry) throw Error.DuplicateMessage
|
if (storage.isMessageDuplicated(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) throw Error.DuplicateMessage
|
||||||
storage.addReceivedMessageTimestamp(envelope.timestamp)
|
|
||||||
// Decrypt the contents
|
// Decrypt the contents
|
||||||
val ciphertext = envelope.content ?: throw Error.NoData
|
val ciphertext = envelope.content ?: throw Error.NoData
|
||||||
var plaintext: ByteArray? = null
|
var plaintext: ByteArray? = null
|
||||||
@ -70,7 +71,7 @@ object MessageReceiver {
|
|||||||
}
|
}
|
||||||
SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT -> {
|
SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT -> {
|
||||||
val hexEncodedGroupPublicKey = envelope.source
|
val hexEncodedGroupPublicKey = envelope.source
|
||||||
if (hexEncodedGroupPublicKey == null || MessagingConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey)) {
|
if (hexEncodedGroupPublicKey == null || !MessagingConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey)) {
|
||||||
throw Error.InvalidGroupPublicKey
|
throw Error.InvalidGroupPublicKey
|
||||||
}
|
}
|
||||||
val encryptionKeyPairs = MessagingConfiguration.shared.storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey)
|
val encryptionKeyPairs = MessagingConfiguration.shared.storage.getClosedGroupEncryptionKeyPairs(hexEncodedGroupPublicKey)
|
||||||
@ -94,32 +95,21 @@ object MessageReceiver {
|
|||||||
}
|
}
|
||||||
groupPublicKey = envelope.source
|
groupPublicKey = envelope.source
|
||||||
decrypt()
|
decrypt()
|
||||||
// try {
|
|
||||||
// decrypt()
|
|
||||||
// } catch(error: Exception) {
|
|
||||||
// val now = System.currentTimeMillis()
|
|
||||||
// var shouldRequestEncryptionKeyPair = true
|
|
||||||
// lastEncryptionKeyPairRequest[groupPublicKey!!]?.let {
|
|
||||||
// shouldRequestEncryptionKeyPair = now - it > 30 * 1000
|
|
||||||
// }
|
|
||||||
// if (shouldRequestEncryptionKeyPair) {
|
|
||||||
// MessageSender.requestEncryptionKeyPair(groupPublicKey)
|
|
||||||
// lastEncryptionKeyPairRequest[groupPublicKey] = now
|
|
||||||
// }
|
|
||||||
// throw error
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
else -> throw Error.UnknownEnvelopeType
|
else -> throw Error.UnknownEnvelopeType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Don't process the envelope any further if the message has been handled already
|
||||||
|
if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage
|
||||||
// Don't process the envelope any further if the sender is blocked
|
// Don't process the envelope any further if the sender is blocked
|
||||||
if (isBlock(sender!!)) throw Error.SenderBlocked
|
if (isBlock(sender!!)) throw Error.SenderBlocked
|
||||||
// Parse the proto
|
// Parse the proto
|
||||||
val proto = SignalServiceProtos.Content.parseFrom(plaintext)
|
val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext))
|
||||||
// Parse the message
|
// Parse the message
|
||||||
val message: Message = ReadReceipt.fromProto(proto) ?:
|
val message: Message = ReadReceipt.fromProto(proto) ?:
|
||||||
TypingIndicator.fromProto(proto) ?:
|
TypingIndicator.fromProto(proto) ?:
|
||||||
ClosedGroupControlMessage.fromProto(proto) ?:
|
ClosedGroupControlMessage.fromProto(proto) ?:
|
||||||
|
DataExtractionNotification.fromProto(proto) ?:
|
||||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||||
ConfigurationMessage.fromProto(proto) ?:
|
ConfigurationMessage.fromProto(proto) ?:
|
||||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||||
@ -131,12 +121,13 @@ object MessageReceiver {
|
|||||||
message.sender = sender
|
message.sender = sender
|
||||||
message.recipient = userPublicKey
|
message.recipient = userPublicKey
|
||||||
message.sentTimestamp = envelope.timestamp
|
message.sentTimestamp = envelope.timestamp
|
||||||
message.receivedTimestamp = System.currentTimeMillis()
|
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
||||||
|
Log.d("Loki", "time: ${envelope.timestamp}, sent: ${envelope.serverTimestamp}")
|
||||||
message.groupPublicKey = groupPublicKey
|
message.groupPublicKey = groupPublicKey
|
||||||
message.openGroupServerMessageID = openGroupServerID
|
message.openGroupServerMessageID = openGroupServerID
|
||||||
// Validate
|
// Validate
|
||||||
var isValid = message.isValid()
|
var isValid = message.isValid()
|
||||||
if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount == 0) { isValid = true }
|
if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount != 0) { isValid = true }
|
||||||
if (!isValid) { throw Error.InvalidMessage }
|
if (!isValid) { throw Error.InvalidMessage }
|
||||||
// Return
|
// Return
|
||||||
return Pair(message, proto)
|
return Pair(message, proto)
|
||||||
|
@ -18,15 +18,17 @@ 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.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
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.utilities.logging.Log
|
|
||||||
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
|
||||||
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.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@ -42,7 +44,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
|||||||
is ReadReceipt -> handleReadReceipt(message)
|
is ReadReceipt -> handleReadReceipt(message)
|
||||||
is TypingIndicator -> handleTypingIndicator(message)
|
is TypingIndicator -> handleTypingIndicator(message)
|
||||||
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
|
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
|
||||||
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto)
|
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message)
|
||||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
||||||
}
|
}
|
||||||
@ -81,32 +83,22 @@ fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
|||||||
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate, proto: SignalServiceProtos.Content) {
|
private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimerUpdate) {
|
||||||
if (message.duration!! > 0) {
|
if (message.duration!! > 0) {
|
||||||
setExpirationTimer(message, proto)
|
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(message)
|
||||||
} else {
|
} else {
|
||||||
disableExpirationTimer(message, proto)
|
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MessageReceiver.setExpirationTimer(message: ExpirationTimerUpdate, proto: SignalServiceProtos.Content) {
|
|
||||||
val id = message.id
|
|
||||||
val duration = message.duration!!
|
|
||||||
val senderPublicKey = message.sender!!
|
|
||||||
SSKEnvironment.shared.messageExpirationManager.setExpirationTimer(id, duration, senderPublicKey, proto)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, proto: SignalServiceProtos.Content) {
|
|
||||||
val id = message.id
|
|
||||||
val senderPublicKey = message.sender!!
|
|
||||||
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(id, senderPublicKey, proto)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) {
|
private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) {
|
||||||
val context = MessagingConfiguration.shared.context
|
val context = MessagingConfiguration.shared.context
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
|
if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return
|
||||||
if (message.sender != storage.getUserPublicKey()) return
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
|
if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return
|
||||||
|
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
||||||
|
TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!)
|
||||||
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
for (closeGroup in message.closedGroups) {
|
for (closeGroup in message.closedGroups) {
|
||||||
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
|
||||||
@ -117,25 +109,25 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes
|
|||||||
if (allOpenGroups.contains(openGroup)) continue
|
if (allOpenGroups.contains(openGroup)) continue
|
||||||
storage.addOpenGroup(openGroup, 1)
|
storage.addOpenGroup(openGroup, 1)
|
||||||
}
|
}
|
||||||
// TODO: in future handle the latest in config messages
|
if (message.displayName.isNotEmpty()) {
|
||||||
TextSecurePreferences.setConfigurationMessageSynced(context, true)
|
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||||
|
storage.setDisplayName(userPublicKey, message.displayName)
|
||||||
|
}
|
||||||
|
if (message.profileKey.isNotEmpty()) {
|
||||||
|
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||||
|
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||||
|
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||||
|
// handle profile photo
|
||||||
|
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||||
|
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.addContacts(message.contacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) {
|
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val context = MessagingConfiguration.shared.context
|
val context = MessagingConfiguration.shared.context
|
||||||
// Parse & persist attachments
|
|
||||||
val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
|
|
||||||
val attachment = Attachment.fromProto(proto)
|
|
||||||
if (attachment == null || !attachment.isValid()) {
|
|
||||||
return@mapNotNull null
|
|
||||||
} else {
|
|
||||||
return@mapNotNull attachment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val attachmentIDs = storage.persistAttachments(message.id ?: 0, attachments)
|
|
||||||
message.attachmentIDs = attachmentIDs as ArrayList<Long>
|
|
||||||
var attachmentsToDownload = attachmentIDs
|
|
||||||
// Update profile if needed
|
// Update profile if needed
|
||||||
val newProfile = message.profile
|
val newProfile = message.profile
|
||||||
if (newProfile != null) {
|
if (newProfile != null) {
|
||||||
@ -143,11 +135,13 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||||
val displayName = newProfile.displayName!!
|
val displayName = newProfile.displayName!!
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
if (userPublicKey == message.sender) {
|
if (openGroupID == null) {
|
||||||
// Update the user's local name if the message came from their master device
|
if (userPublicKey == message.sender) {
|
||||||
TextSecurePreferences.setProfileName(context, displayName)
|
// Update the user's local name if the message came from their master device
|
||||||
|
TextSecurePreferences.setProfileName(context, displayName)
|
||||||
|
}
|
||||||
|
profileManager.setDisplayName(context, recipient, displayName)
|
||||||
}
|
}
|
||||||
profileManager.setDisplayName(context, recipient, displayName)
|
|
||||||
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey)) {
|
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey)) {
|
||||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
||||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||||
@ -165,10 +159,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||||
val quote = proto.dataMessage.quote
|
val quote = proto.dataMessage.quote
|
||||||
val author = Address.fromSerialized(quote.author)
|
val author = Address.fromSerialized(quote.author)
|
||||||
val messageID = MessagingConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author)
|
val messageInfo = MessagingConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author)
|
||||||
if (messageID != null) {
|
if (messageInfo != null) {
|
||||||
val attachmentsWithLinkPreview = MessagingConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageID)
|
val attachments = if (messageInfo.second) MessagingConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList()
|
||||||
quoteModel = QuoteModel(quote.id, author, MessagingConfiguration.shared.messageDataProvider.getMessageBodyFor(messageID), false, attachmentsWithLinkPreview)
|
quoteModel = QuoteModel(quote.id, author, MessagingConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments)
|
||||||
} else {
|
} else {
|
||||||
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList))
|
||||||
}
|
}
|
||||||
@ -189,14 +183,25 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto ->
|
||||||
|
val attachment = Attachment.fromProto(proto)
|
||||||
|
if (!attachment.isValid()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
} else {
|
||||||
|
return@mapNotNull attachment
|
||||||
|
}
|
||||||
|
}
|
||||||
// Parse stickers if needed
|
// Parse stickers if needed
|
||||||
// Persist the message
|
// Persist the message
|
||||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID) ?: throw MessageReceiver.Error.NoThread
|
|
||||||
message.threadID = threadID
|
message.threadID = threadID
|
||||||
|
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.NoThread
|
||||||
|
// Parse & persist attachments
|
||||||
// Start attachment downloads if needed
|
// Start attachment downloads if needed
|
||||||
attachmentsToDownload.forEach { attachmentID ->
|
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
||||||
val downloadJob = AttachmentDownloadJob(attachmentID, messageID)
|
attachment.attachmentId?.let { id ->
|
||||||
JobQueue.shared.add(downloadJob)
|
val downloadJob = AttachmentDownloadJob(id.rowId, messageID)
|
||||||
|
JobQueue.shared.add(downloadJob)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Cancel any typing indicators if needed
|
// Cancel any typing indicators if needed
|
||||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||||
@ -266,6 +271,10 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
val oldMembers = group.members.map { it.serialize() }
|
val oldMembers = group.members.map { it.serialize() }
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||||
@ -314,12 +323,16 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!group.members.map { it.toString() }.contains(senderPublicKey)) {
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
|
||||||
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Find our wrapper and decrypt it if possible
|
// Find our wrapper and decrypt it if possible
|
||||||
val wrapper = kind.wrappers.firstOrNull { it.publicKey!!.toByteArray().toHexString() == userPublicKey } ?: return
|
val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return
|
||||||
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
|
||||||
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
|
||||||
// Parse it
|
// Parse it
|
||||||
@ -338,6 +351,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
|
|||||||
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
|
||||||
val context = MessagingConfiguration.shared.context
|
val context = MessagingConfiguration.shared.context
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
val senderPublicKey = message.sender ?: return
|
val senderPublicKey = message.sender ?: return
|
||||||
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return
|
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return
|
||||||
val groupPublicKey = message.groupPublicKey ?: return
|
val groupPublicKey = message.groupPublicKey ?: return
|
||||||
@ -347,6 +361,10 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
|
||||||
return
|
return
|
||||||
@ -356,7 +374,14 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
|
|||||||
val name = kind.name
|
val name = kind.name
|
||||||
storage.updateTitle(groupID, name)
|
storage.updateTitle(groupID, name)
|
||||||
|
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, message.sentTimestamp!!)
|
// Notify the user
|
||||||
|
if (userPublicKey == senderPublicKey) {
|
||||||
|
// sender is a linked device
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
|
} else {
|
||||||
|
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, message.sentTimestamp!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
|
||||||
@ -371,6 +396,10 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
@ -380,11 +409,13 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
|||||||
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
|
||||||
val newMembers = members + updateMembers
|
val newMembers = members + updateMembers
|
||||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||||
|
// Notify the user
|
||||||
if (userPublicKey == senderPublicKey) {
|
if (userPublicKey == senderPublicKey) {
|
||||||
|
// sender is a linked device
|
||||||
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
} else {
|
} else {
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
|
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.MEMBER_ADDED, name, members, admins, message.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
|
||||||
@ -398,7 +429,6 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.MEMBER_ADDED, name, members, admins, message.sentTimestamp!!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
|
||||||
@ -413,6 +443,10 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
val members = group.members.map { it.serialize() }
|
val members = group.members.map { it.serialize() }
|
||||||
@ -447,7 +481,14 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
|||||||
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
|
||||||
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.MEMBER_REMOVED
|
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.MEMBER_REMOVED
|
||||||
|
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
|
// Notify the user
|
||||||
|
if (userPublicKey == senderPublicKey) {
|
||||||
|
// sender is a linked device
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
|
storage.insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
|
} else {
|
||||||
|
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
|
||||||
@ -462,6 +503,10 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!group.isActive) {
|
||||||
|
Log.d("Loki", "Ignoring closed group info message for inactive group")
|
||||||
|
return
|
||||||
|
}
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Check common group update logic
|
// Check common group update logic
|
||||||
val members = group.members.map { it.serialize() }
|
val members = group.members.map { it.serialize() }
|
||||||
@ -472,8 +517,10 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
// If admin leaves the group is disbanded
|
// If admin leaves the group is disbanded
|
||||||
val didAdminLeave = admins.contains(senderPublicKey)
|
val didAdminLeave = admins.contains(senderPublicKey)
|
||||||
val updatedMemberList = members - senderPublicKey
|
val updatedMemberList = members - senderPublicKey
|
||||||
|
val userLeft = (userPublicKey == senderPublicKey)
|
||||||
|
|
||||||
if (didAdminLeave) {
|
if (didAdminLeave || userLeft) {
|
||||||
|
// admin left the group of linked device left the group
|
||||||
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||||
} else {
|
} else {
|
||||||
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
val isCurrentUserAdmin = admins.contains(userPublicKey)
|
||||||
@ -482,7 +529,14 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
// Notify the user
|
||||||
|
if (userLeft) {
|
||||||
|
//sender is a linked device
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
|
||||||
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!)
|
||||||
|
} else {
|
||||||
|
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
|
||||||
|
@ -35,7 +35,7 @@ import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as S
|
|||||||
object MessageSender {
|
object MessageSender {
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
sealed class Error(val description: String) : Exception() {
|
sealed class Error(val description: String) : Exception(description) {
|
||||||
object InvalidMessage : Error("Invalid message.")
|
object InvalidMessage : Error("Invalid message.")
|
||||||
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
|
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
|
||||||
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
|
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
|
||||||
@ -50,6 +50,9 @@ object MessageSender {
|
|||||||
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
|
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
|
||||||
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
object InvalidClosedGroupUpdate : Error("Invalid group update.")
|
||||||
|
|
||||||
|
// Precondition
|
||||||
|
class PreconditionFailure(val reason: String): Error(reason)
|
||||||
|
|
||||||
internal val isRetryable: Boolean = when (this) {
|
internal val isRetryable: Boolean = when (this) {
|
||||||
is InvalidMessage -> false
|
is InvalidMessage -> false
|
||||||
is ProtoConversionFailed -> false
|
is ProtoConversionFailed -> false
|
||||||
@ -73,7 +76,6 @@ object MessageSender {
|
|||||||
val promise = deferred.promise
|
val promise = deferred.promise
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
val preconditionFailure = Exception("Destination should not be open groups!")
|
|
||||||
// Set the timestamp, sender and recipient
|
// Set the timestamp, sender and recipient
|
||||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
|
||||||
message.sender = userPublicKey
|
message.sender = userPublicKey
|
||||||
@ -90,7 +92,7 @@ object MessageSender {
|
|||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> message.recipient = destination.publicKey
|
is Destination.Contact -> message.recipient = destination.publicKey
|
||||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||||
is Destination.OpenGroup -> throw preconditionFailure
|
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -128,7 +130,7 @@ object MessageSender {
|
|||||||
val encryptionKeyPair = MessagingConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
val encryptionKeyPair = MessagingConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw preconditionFailure
|
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -142,7 +144,7 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw preconditionFailure
|
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Calculate proof of work
|
// Calculate proof of work
|
||||||
@ -151,10 +153,9 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
val recipient = message.recipient!!
|
val recipient = message.recipient!!
|
||||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||||
val timestamp = System.currentTimeMillis()
|
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, message.sentTimestamp!!, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
|
||||||
val nonce = ProofOfWork.calculate(base64EncodedData, recipient, timestamp, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed
|
|
||||||
// Send the result
|
// Send the result
|
||||||
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce)
|
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, message.sentTimestamp!!, nonce)
|
||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
@ -177,8 +178,8 @@ object MessageSender {
|
|||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
||||||
JobQueue.shared.add(notifyPNServerJob)
|
JobQueue.shared.add(notifyPNServerJob)
|
||||||
deferred.resolve(Unit)
|
|
||||||
}
|
}
|
||||||
|
deferred.resolve(Unit)
|
||||||
}
|
}
|
||||||
promise.fail {
|
promise.fail {
|
||||||
errorCount += 1
|
errorCount += 1
|
||||||
@ -200,34 +201,31 @@ object MessageSender {
|
|||||||
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
|
|
||||||
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
|
||||||
message.sender = storage.getUserPublicKey()
|
message.sender = storage.getUserPublicKey()
|
||||||
|
// Set the failure handler (need it here already for precondition failure handling)
|
||||||
|
fun handleFailure(error: Exception) {
|
||||||
|
handleFailedMessageSend(message, error)
|
||||||
|
deferred.reject(error)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val server: String
|
val server: String
|
||||||
val channel: Long
|
val channel: Long
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> throw preconditionFailure
|
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
||||||
is Destination.ClosedGroup -> throw preconditionFailure
|
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
||||||
is Destination.OpenGroup -> {
|
is Destination.OpenGroup -> {
|
||||||
message.recipient = "${destination.server}.${destination.channel}"
|
message.recipient = "${destination.server}.${destination.channel}"
|
||||||
server = destination.server
|
server = destination.server
|
||||||
channel = destination.channel
|
channel = destination.channel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set the failure handler (need it here already for precondition failure handling)
|
|
||||||
fun handleFailure(error: Exception) {
|
|
||||||
handleFailedMessageSend(message, error)
|
|
||||||
deferred.reject(error)
|
|
||||||
}
|
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
handleFailure(Error.InvalidMessage)
|
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
}
|
}
|
||||||
// Convert the message to an open group message
|
// Convert the message to an open group message
|
||||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run {
|
val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run {
|
||||||
handleFailure(Error.InvalidMessage)
|
|
||||||
throw Error.InvalidMessage
|
throw Error.InvalidMessage
|
||||||
}
|
}
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -239,7 +237,7 @@ object MessageSender {
|
|||||||
handleFailure(it)
|
handleFailure(it)
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
deferred.reject(exception)
|
handleFailure(exception)
|
||||||
}
|
}
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
}
|
}
|
||||||
@ -247,7 +245,8 @@ object MessageSender {
|
|||||||
// Result Handling
|
// Result Handling
|
||||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
|
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
||||||
// Ignore future self-sends
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
@ -255,17 +254,16 @@ object MessageSender {
|
|||||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
|
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
|
||||||
}
|
}
|
||||||
// Mark the message as sent
|
// Mark the message as sent
|
||||||
storage.markAsSent(message.sentTimestamp!!, message.sender!!)
|
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
storage.markUnidentified(message.sentTimestamp!!, message.sender!!)
|
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
// Start the disappearing messages timer if needed
|
// Start the disappearing messages timer if needed
|
||||||
if (message is VisibleMessage && !isSyncMessage) {
|
if (message is VisibleMessage && !isSyncMessage) {
|
||||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender!!)
|
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
}
|
}
|
||||||
// Sync the message if:
|
// Sync the message if:
|
||||||
// • it's a visible message
|
// • it's a visible message
|
||||||
// • the destination was a contact
|
// • the destination was a contact
|
||||||
// • we didn't sync it already
|
// • we didn't sync it already
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
|
||||||
if (destination is Destination.Contact && !isSyncMessage) {
|
if (destination is Destination.Contact && !isSyncMessage) {
|
||||||
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
|
||||||
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
|
||||||
@ -275,7 +273,8 @@ object MessageSender {
|
|||||||
|
|
||||||
fun handleFailedMessageSend(message: Message, error: Exception) {
|
fun handleFailedMessageSend(message: Message, error: Exception) {
|
||||||
val storage = MessagingConfiguration.shared.storage
|
val storage = MessagingConfiguration.shared.storage
|
||||||
storage.setErrorMessage(message.sentTimestamp!!, message.sender!!, error)
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
|
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
@ -337,7 +336,7 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun explicitLeave(groupPublicKey: String): Promise<Unit, Exception> {
|
fun explicitLeave(groupPublicKey: String, notifyUser: Boolean): Promise<Unit, Exception> {
|
||||||
return leave(groupPublicKey)
|
return leave(groupPublicKey, notifyUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,22 +5,20 @@ package org.session.libsession.messaging.sending_receiving
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||||
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
|
|
||||||
import org.session.libsignal.libsignal.ecc.Curve
|
import org.session.libsignal.libsignal.ecc.Curve
|
||||||
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
import org.session.libsignal.libsignal.ecc.ECKeyPair
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional
|
import org.session.libsignal.libsignal.util.guava.Optional
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
|
||||||
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -211,6 +209,7 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
|||||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
|
||||||
val sentTime = System.currentTimeMillis()
|
val sentTime = System.currentTimeMillis()
|
||||||
closedGroupControlMessage.sentTimestamp = sentTime
|
closedGroupControlMessage.sentTimestamp = sentTime
|
||||||
|
storage.setActive(groupID, false)
|
||||||
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
|
||||||
// Notify the user
|
// Notify the user
|
||||||
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
|
||||||
@ -221,6 +220,8 @@ fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Pro
|
|||||||
// Remove the group private key and unsubscribe from PNs
|
// Remove the group private key and unsubscribe from PNs
|
||||||
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
|
}.fail {
|
||||||
|
storage.setActive(groupID, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
@ -291,4 +292,32 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
|
|||||||
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
|
||||||
closedGroupControlMessage.sentTimestamp = sentTime
|
closedGroupControlMessage.sentTimestamp = sentTime
|
||||||
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
send(closedGroupControlMessage, Address.fromSerialized(groupID))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) {
|
||||||
|
val storage = MessagingConfiguration.shared.storage
|
||||||
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
|
val group = storage.getGroup(groupID) ?: run {
|
||||||
|
Log.d("Loki", "Can't send encryption key pair for nonexistent closed group.")
|
||||||
|
throw Error.NoThread
|
||||||
|
}
|
||||||
|
val members = group.members.map { it.serialize() }
|
||||||
|
if (!members.contains(publicKey)) {
|
||||||
|
Log.d("Loki", "Refusing to send latest encryption key pair to non-member.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get the latest encryption key pair
|
||||||
|
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
|
||||||
|
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
|
||||||
|
// Send it
|
||||||
|
val proto = SignalServiceProtos.KeyPair.newBuilder()
|
||||||
|
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
|
||||||
|
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
|
||||||
|
val plaintext = proto.build().toByteArray()
|
||||||
|
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
|
||||||
|
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
|
||||||
|
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
|
||||||
|
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
|
||||||
|
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
|
||||||
|
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey))
|
||||||
}
|
}
|
@ -169,4 +169,25 @@ public class PointerAttachment extends Attachment {
|
|||||||
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
|
||||||
thumbnail != null ? thumbnail.asPointer().getUrl() : ""));
|
thumbnail != null ? thumbnail.asPointer().getUrl() : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Session Attachment to a Signal Attachment
|
||||||
|
* @param attachment Session Attachment
|
||||||
|
* @return Signal Attachment
|
||||||
|
*/
|
||||||
|
public static Attachment forAttachment(org.session.libsession.messaging.messages.visible.Attachment attachment) {
|
||||||
|
return new PointerAttachment(attachment.getContentType(),
|
||||||
|
AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING,
|
||||||
|
attachment.getSizeInBytes(),
|
||||||
|
attachment.getFileName(),
|
||||||
|
null, Base64.encodeBytes(attachment.getKey()),
|
||||||
|
null,
|
||||||
|
attachment.getDigest(),
|
||||||
|
null,
|
||||||
|
attachment.getKind() == org.session.libsession.messaging.messages.visible.Attachment.Kind.VOICE_MESSAGE,
|
||||||
|
attachment.getSize() != null ? attachment.getSize().getWidth() : 0,
|
||||||
|
attachment.getSize() != null ? attachment.getSize().getHeight() : 0,
|
||||||
|
attachment.getCaption(),
|
||||||
|
attachment.getUrl());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,15 @@ import android.os.Handler
|
|||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
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.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
|
import org.session.libsignal.service.loki.utilities.getRandomElementOrNull
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
|
|
||||||
class ClosedGroupPoller {
|
class ClosedGroupPoller {
|
||||||
private var isPolling = false
|
private var isPolling = false
|
||||||
@ -24,7 +22,7 @@ class ClosedGroupPoller {
|
|||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
poll()
|
poll()
|
||||||
handler.postDelayed(this, ClosedGroupPoller.pollInterval)
|
handler.postDelayed(this, pollInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +59,7 @@ class ClosedGroupPoller {
|
|||||||
// region Private API
|
// region Private API
|
||||||
private fun poll(): List<Promise<Unit, Exception>> {
|
private fun poll(): List<Promise<Unit, Exception>> {
|
||||||
if (!isPolling) { return listOf() }
|
if (!isPolling) { return listOf() }
|
||||||
val publicKeys = MessagingConfiguration.shared.storage.getAllClosedGroupPublicKeys()
|
val publicKeys = MessagingConfiguration.shared.storage.getAllActiveClosedGroupPublicKeys()
|
||||||
return publicKeys.map { publicKey ->
|
return publicKeys.map { publicKey ->
|
||||||
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
|
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
|
||||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
||||||
@ -69,6 +67,10 @@ class ClosedGroupPoller {
|
|||||||
SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) }
|
SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) }
|
||||||
}
|
}
|
||||||
promise.successBackground { messages ->
|
promise.successBackground { messages ->
|
||||||
|
if (!MessagingConfiguration.shared.storage.isGroupActive(publicKey)) {
|
||||||
|
// ignore inactive group's messages
|
||||||
|
return@successBackground
|
||||||
|
}
|
||||||
if (messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving.pollers
|
package org.session.libsession.messaging.sending_receiving.pollers
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
@ -11,61 +9,30 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|||||||
import org.session.libsession.messaging.opengroups.OpenGroup
|
import org.session.libsession.messaging.opengroups.OpenGroup
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroupMessage
|
import org.session.libsession.messaging.opengroups.OpenGroupMessage
|
||||||
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.*
|
import org.session.libsignal.service.internal.push.SignalServiceProtos.*
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class OpenGroupPoller(private val openGroup: OpenGroup, private val executorService: ScheduledExecutorService? = null) {
|
||||||
|
|
||||||
class OpenGroupPoller(private val openGroup: OpenGroup) {
|
|
||||||
private val handler by lazy { Handler() }
|
|
||||||
private var hasStarted = false
|
private var hasStarted = false
|
||||||
private var isPollOngoing = false
|
@Volatile private var isPollOngoing = false
|
||||||
public var isCaughtUp = false
|
var isCaughtUp = false
|
||||||
|
|
||||||
|
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||||
|
|
||||||
// region Convenience
|
// region Convenience
|
||||||
private val userHexEncodedPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
private val userHexEncodedPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||||
private var displayNameUpdatees = setOf<String>()
|
private var displayNameUpdates = setOf<String>()
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Tasks
|
|
||||||
private val pollForNewMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForNewMessages()
|
|
||||||
handler.postDelayed(this, pollForNewMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDeletedMessagesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDeletedMessages()
|
|
||||||
handler.postDelayed(this, pollForDeletedMessagesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForModeratorsTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForModerators()
|
|
||||||
handler.postDelayed(this, pollForModeratorsInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pollForDisplayNamesTask = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
pollForDisplayNames()
|
|
||||||
handler.postDelayed(this, pollForDisplayNamesInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Settings
|
// region Settings
|
||||||
companion object {
|
companion object {
|
||||||
private val pollForNewMessagesInterval: Long = 4 * 1000
|
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
||||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
||||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
||||||
@ -74,19 +41,21 @@ class OpenGroupPoller(private val openGroup: OpenGroup) {
|
|||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
fun startIfNeeded() {
|
fun startIfNeeded() {
|
||||||
if (hasStarted) return
|
if (hasStarted || executorService == null) return
|
||||||
pollForNewMessagesTask.run()
|
cancellableFutures += listOf(
|
||||||
pollForDeletedMessagesTask.run()
|
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
|
||||||
pollForModeratorsTask.run()
|
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
|
||||||
pollForDisplayNamesTask.run()
|
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
|
||||||
|
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
|
||||||
|
)
|
||||||
hasStarted = true
|
hasStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
handler.removeCallbacks(pollForNewMessagesTask)
|
cancellableFutures.forEach { future ->
|
||||||
handler.removeCallbacks(pollForDeletedMessagesTask)
|
future.cancel(false)
|
||||||
handler.removeCallbacks(pollForModeratorsTask)
|
}
|
||||||
handler.removeCallbacks(pollForDisplayNamesTask)
|
cancellableFutures.clear()
|
||||||
hasStarted = false
|
hasStarted = false
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -96,120 +65,129 @@ class OpenGroupPoller(private val openGroup: OpenGroup) {
|
|||||||
return pollForNewMessages(false)
|
return pollForNewMessages(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pollForNewMessages(isBackgroundPoll: Boolean): Promise<Unit, Exception> {
|
private fun pollForNewMessages(isBackgroundPoll: Boolean): Promise<Unit, Exception> {
|
||||||
if (isPollOngoing) { return Promise.of(Unit) }
|
if (isPollOngoing) { return Promise.of(Unit) }
|
||||||
isPollOngoing = true
|
isPollOngoing = true
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
||||||
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
||||||
// Process messages in the background
|
// Process messages in the background
|
||||||
|
Log.d("Loki", "received ${messages.size} messages")
|
||||||
messages.forEach { message ->
|
messages.forEach { message ->
|
||||||
val senderPublicKey = message.senderPublicKey
|
try {
|
||||||
val wasSentByCurrentUser = (senderPublicKey == userHexEncodedPublicKey)
|
val senderPublicKey = message.senderPublicKey
|
||||||
fun generateDisplayName(rawDisplayName: String): String {
|
fun generateDisplayName(rawDisplayName: String): String {
|
||||||
return "${rawDisplayName} (${senderPublicKey.takeLast(8)})"
|
return "$rawDisplayName (...${senderPublicKey.takeLast(8)})"
|
||||||
}
|
}
|
||||||
val senderDisplayName = MessagingConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName("Anonymous")
|
val senderDisplayName = MessagingConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName(message.displayName)
|
||||||
val id = openGroup.id.toByteArray()
|
val id = openGroup.id.toByteArray()
|
||||||
// Main message
|
// Main message
|
||||||
val dataMessageProto = DataMessage.newBuilder()
|
val dataMessageProto = DataMessage.newBuilder()
|
||||||
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
||||||
dataMessageProto.setBody(body)
|
dataMessageProto.setBody(body)
|
||||||
dataMessageProto.setTimestamp(message.timestamp)
|
dataMessageProto.setTimestamp(message.timestamp)
|
||||||
// Attachments
|
// Attachments
|
||||||
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
||||||
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
try {
|
||||||
val attachmentProto = AttachmentPointer.newBuilder()
|
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
||||||
attachmentProto.setId(attachment.serverID)
|
val attachmentProto = AttachmentPointer.newBuilder()
|
||||||
attachmentProto.setContentType(attachment.contentType)
|
attachmentProto.setId(attachment.serverID)
|
||||||
attachmentProto.setSize(attachment.size)
|
attachmentProto.setContentType(attachment.contentType)
|
||||||
attachmentProto.setFileName(attachment.fileName)
|
attachmentProto.setSize(attachment.size)
|
||||||
attachmentProto.setFlags(attachment.flags)
|
attachmentProto.setFileName(attachment.fileName)
|
||||||
attachmentProto.setWidth(attachment.width)
|
attachmentProto.setFlags(attachment.flags)
|
||||||
attachmentProto.setHeight(attachment.height)
|
attachmentProto.setWidth(attachment.width)
|
||||||
attachment.caption.let { attachmentProto.setCaption(it) }
|
attachmentProto.setHeight(attachment.height)
|
||||||
attachmentProto.setUrl(attachment.url)
|
attachment.caption?.let { attachmentProto.setCaption(it) }
|
||||||
attachmentProto.build()
|
attachmentProto.setUrl(attachment.url)
|
||||||
}
|
attachmentProto.build()
|
||||||
dataMessageProto.addAllAttachments(attachmentProtos)
|
} catch (e: Exception) {
|
||||||
// Link preview
|
Log.e("Loki","Failed to parse attachment as proto",e)
|
||||||
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
|
null
|
||||||
if (linkPreview != null) {
|
}
|
||||||
val linkPreviewProto = DataMessage.Preview.newBuilder()
|
}
|
||||||
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
|
dataMessageProto.addAllAttachments(attachmentProtos)
|
||||||
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
|
// Link preview
|
||||||
val attachmentProto = AttachmentPointer.newBuilder()
|
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
|
||||||
attachmentProto.setId(linkPreview.serverID)
|
if (linkPreview != null) {
|
||||||
attachmentProto.setContentType(linkPreview.contentType)
|
val linkPreviewProto = DataMessage.Preview.newBuilder()
|
||||||
attachmentProto.setSize(linkPreview.size)
|
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
|
||||||
attachmentProto.setFileName(linkPreview.fileName)
|
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
|
||||||
attachmentProto.setFlags(linkPreview.flags)
|
val attachmentProto = AttachmentPointer.newBuilder()
|
||||||
attachmentProto.setWidth(linkPreview.width)
|
attachmentProto.setId(linkPreview.serverID)
|
||||||
attachmentProto.setHeight(linkPreview.height)
|
attachmentProto.setContentType(linkPreview.contentType)
|
||||||
linkPreview.caption.let { attachmentProto.setCaption(it) }
|
attachmentProto.setSize(linkPreview.size)
|
||||||
attachmentProto.setUrl(linkPreview.url)
|
attachmentProto.setFileName(linkPreview.fileName)
|
||||||
linkPreviewProto.setImage(attachmentProto.build())
|
attachmentProto.setFlags(linkPreview.flags)
|
||||||
dataMessageProto.addPreview(linkPreviewProto.build())
|
attachmentProto.setWidth(linkPreview.width)
|
||||||
}
|
attachmentProto.setHeight(linkPreview.height)
|
||||||
// Quote
|
linkPreview.caption?.let { attachmentProto.setCaption(it) }
|
||||||
val quote = message.quote
|
attachmentProto.setUrl(linkPreview.url)
|
||||||
if (quote != null) {
|
linkPreviewProto.setImage(attachmentProto.build())
|
||||||
val quoteProto = DataMessage.Quote.newBuilder()
|
dataMessageProto.addPreview(linkPreviewProto.build())
|
||||||
quoteProto.setId(quote.quotedMessageTimestamp)
|
}
|
||||||
quoteProto.setAuthor(quote.quoteePublicKey)
|
// Quote
|
||||||
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
|
val quote = message.quote
|
||||||
dataMessageProto.setQuote(quoteProto.build())
|
if (quote != null) {
|
||||||
}
|
val quoteProto = DataMessage.Quote.newBuilder()
|
||||||
val messageServerID = message.serverID
|
quoteProto.setId(quote.quotedMessageTimestamp)
|
||||||
// Profile
|
quoteProto.setAuthor(quote.quoteePublicKey)
|
||||||
val profileProto = DataMessage.LokiProfile.newBuilder()
|
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
|
||||||
profileProto.setDisplayName(message.displayName)
|
dataMessageProto.setQuote(quoteProto.build())
|
||||||
val profilePicture = message.profilePicture
|
}
|
||||||
if (profilePicture != null) {
|
val messageServerID = message.serverID
|
||||||
profileProto.setProfilePicture(profilePicture.url)
|
// Profile
|
||||||
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
|
val profileProto = DataMessage.LokiProfile.newBuilder()
|
||||||
}
|
profileProto.setDisplayName(senderDisplayName)
|
||||||
dataMessageProto.setProfile(profileProto.build())
|
val profilePicture = message.profilePicture
|
||||||
/* TODO: the signal service proto needs to be synced with iOS
|
if (profilePicture != null) {
|
||||||
// Open group info
|
profileProto.setProfilePicture(profilePicture.url)
|
||||||
if (messageServerID != null) {
|
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
|
||||||
val openGroupProto = PublicChatInfo.newBuilder()
|
}
|
||||||
openGroupProto.setServerID(messageServerID)
|
dataMessageProto.setProfile(profileProto.build())
|
||||||
dataMessageProto.setPublicChatInfo(openGroupProto.build())
|
/* TODO: the signal service proto needs to be synced with iOS
|
||||||
}
|
// Open group info
|
||||||
*/
|
if (messageServerID != null) {
|
||||||
// Signal group context
|
val openGroupProto = PublicChatInfo.newBuilder()
|
||||||
val groupProto = GroupContext.newBuilder()
|
openGroupProto.setServerID(messageServerID)
|
||||||
groupProto.setId(ByteString.copyFrom(id))
|
dataMessageProto.setPublicChatInfo(openGroupProto.build())
|
||||||
groupProto.setType(GroupContext.Type.DELIVER)
|
}
|
||||||
groupProto.setName(openGroup.displayName)
|
*/
|
||||||
dataMessageProto.setGroup(groupProto.build())
|
// Signal group context
|
||||||
// Sync target
|
val groupProto = GroupContext.newBuilder()
|
||||||
if (wasSentByCurrentUser) {
|
groupProto.setId(ByteString.copyFrom(id))
|
||||||
dataMessageProto.setSyncTarget(openGroup.id)
|
groupProto.setType(GroupContext.Type.DELIVER)
|
||||||
}
|
groupProto.setName(openGroup.displayName)
|
||||||
// Content
|
dataMessageProto.setGroup(groupProto.build())
|
||||||
val content = Content.newBuilder()
|
// Content
|
||||||
content.setDataMessage(dataMessageProto.build())
|
val content = Content.newBuilder()
|
||||||
// Envelope
|
content.setDataMessage(dataMessageProto.build())
|
||||||
val builder = Envelope.newBuilder()
|
// Envelope
|
||||||
builder.type = Envelope.Type.UNIDENTIFIED_SENDER
|
val builder = Envelope.newBuilder()
|
||||||
builder.source = senderPublicKey
|
builder.type = Envelope.Type.UNIDENTIFIED_SENDER
|
||||||
builder.sourceDevice = 1
|
builder.source = senderPublicKey
|
||||||
builder.setContent(content.build().toByteString())
|
builder.sourceDevice = 1
|
||||||
builder.serverTimestamp = message.serverTimestamp
|
builder.setContent(content.build().toByteString())
|
||||||
val envelope = builder.build()
|
builder.timestamp = message.timestamp
|
||||||
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, messageServerID, openGroup.id)
|
builder.serverTimestamp = message.serverTimestamp
|
||||||
if (isBackgroundPoll) {
|
val envelope = builder.build()
|
||||||
job.executeAsync().success { deferred.resolve(Unit) }.fail { deferred.resolve(Unit) }
|
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, messageServerID, openGroup.id)
|
||||||
// The promise is just used to keep track of when we're done
|
Log.d("Loki", "Scheduling Job $job")
|
||||||
} else {
|
if (isBackgroundPoll) {
|
||||||
JobQueue.shared.add(job)
|
job.executeAsync().always { deferred.resolve(Unit) }
|
||||||
deferred.resolve(Unit)
|
// The promise is just used to keep track of when we're done
|
||||||
|
} else {
|
||||||
|
JobQueue.shared.add(job)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Exception parsing message", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey
|
||||||
|
executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS)
|
||||||
isCaughtUp = true
|
isCaughtUp = true
|
||||||
isPollOngoing = false
|
isPollOngoing = false
|
||||||
|
deferred.resolve(Unit)
|
||||||
}.fail {
|
}.fail {
|
||||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||||
isPollOngoing = false
|
isPollOngoing = false
|
||||||
@ -218,16 +196,17 @@ class OpenGroupPoller(private val openGroup: OpenGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pollForDisplayNames() {
|
private fun pollForDisplayNames() {
|
||||||
if (displayNameUpdatees.isEmpty()) { return }
|
if (displayNameUpdates.isEmpty()) { return }
|
||||||
val hexEncodedPublicKeys = displayNameUpdatees
|
val hexEncodedPublicKeys = displayNameUpdates
|
||||||
displayNameUpdatees = setOf()
|
displayNameUpdates = setOf()
|
||||||
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
||||||
for (pair in mapping.entries) {
|
for (pair in mapping.entries) {
|
||||||
val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})"
|
if (pair.key == userHexEncodedPublicKey) continue
|
||||||
|
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
|
||||||
MessagingConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
MessagingConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
||||||
}
|
}
|
||||||
}.fail {
|
}.fail {
|
||||||
displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys)
|
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,25 +2,22 @@ package org.session.libsession.messaging.sending_receiving.pollers
|
|||||||
|
|
||||||
import nl.komponents.kovenant.*
|
import nl.komponents.kovenant.*
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
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.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.snode.SnodeConfiguration
|
import org.session.libsession.snode.SnodeConfiguration
|
||||||
|
|
||||||
import org.session.libsignal.service.loki.api.Snode
|
import org.session.libsignal.service.loki.api.Snode
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private class PromiseCanceledException : Exception("Promise canceled.")
|
private class PromiseCanceledException : Exception("Promise canceled.")
|
||||||
|
|
||||||
class Poller {
|
class Poller {
|
||||||
private val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
var userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||||
private var hasStarted: Boolean = false
|
private var hasStarted: Boolean = false
|
||||||
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
||||||
public var isCaughtUp = false
|
public var isCaughtUp = false
|
||||||
|
@ -42,7 +42,7 @@ open class DotNetAPI {
|
|||||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
internal sealed class Error(val description: String) : Exception() {
|
internal sealed class Error(val description: String) : Exception(description) {
|
||||||
object Generic : Error("An error occurred.")
|
object Generic : Error("An error occurred.")
|
||||||
object InvalidURL : Error("Invalid URL.")
|
object InvalidURL : Error("Invalid URL.")
|
||||||
object ParsingFailed : Error("Invalid file server response.")
|
object ParsingFailed : Error("Invalid file server response.")
|
||||||
|
@ -10,7 +10,7 @@ import java.security.SecureRandom
|
|||||||
object MessageWrapper {
|
object MessageWrapper {
|
||||||
|
|
||||||
// region Types
|
// region Types
|
||||||
sealed class Error(val description: String) : Exception() {
|
sealed class Error(val description: String) : Exception(description) {
|
||||||
object FailedToWrapData : Error("Failed to wrap data.")
|
object FailedToWrapData : Error("Failed to wrap data.")
|
||||||
object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.")
|
object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.")
|
||||||
object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.")
|
object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.")
|
||||||
|
@ -2,11 +2,11 @@ package org.session.libsession.snode
|
|||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
import java.nio.Buffer
|
import java.nio.Buffer
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@ -62,7 +62,7 @@ object OnionRequestEncryption {
|
|||||||
*/
|
*/
|
||||||
internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
|
internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
|
||||||
val deferred = deferred<EncryptionResult, Exception>()
|
val deferred = deferred<EncryptionResult, Exception>()
|
||||||
Thread {
|
ThreadUtils.queue {
|
||||||
try {
|
try {
|
||||||
val payload: MutableMap<String, Any>
|
val payload: MutableMap<String, Any>
|
||||||
when (rhs) {
|
when (rhs) {
|
||||||
@ -89,7 +89,7 @@ object OnionRequestEncryption {
|
|||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
deferred.reject(exception)
|
deferred.reject(exception)
|
||||||
}
|
}
|
||||||
}.start()
|
}
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,19 @@
|
|||||||
|
|
||||||
package org.session.libsession.snode
|
package org.session.libsession.snode
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import nl.komponents.kovenant.*
|
import nl.komponents.kovenant.*
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
|
|
||||||
import org.session.libsession.snode.utilities.getRandomElement
|
import org.session.libsession.snode.utilities.getRandomElement
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
|
||||||
import org.session.libsignal.service.loki.api.Snode
|
import org.session.libsignal.service.loki.api.Snode
|
||||||
|
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.service.loki.utilities.Broadcaster
|
import org.session.libsignal.service.loki.utilities.Broadcaster
|
||||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
import org.session.libsignal.utilities.*
|
import org.session.libsignal.utilities.*
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
object SnodeAPI {
|
object SnodeAPI {
|
||||||
@ -36,7 +34,14 @@ object SnodeAPI {
|
|||||||
private val maxRetryCount = 6
|
private val maxRetryCount = 6
|
||||||
private val minimumSnodePoolCount = 64
|
private val minimumSnodePoolCount = 64
|
||||||
private val minimumSwarmSnodeCount = 2
|
private val minimumSwarmSnodeCount = 2
|
||||||
private val seedNodePool: Set<String> = setOf( "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" )
|
|
||||||
|
// use port 4433 if API level can handle network security config and enforce pinned certificates
|
||||||
|
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||||
|
private val seedNodePool: Set<String> = setOf(
|
||||||
|
"https://storage.seed1.loki.network:$seedPort",
|
||||||
|
"https://storage.seed3.loki.network:$seedPort",
|
||||||
|
"https://public.loki.foundation:$seedPort"
|
||||||
|
)
|
||||||
internal val snodeFailureThreshold = 4
|
internal val snodeFailureThreshold = 4
|
||||||
private val targetSwarmSnodeCount = 2
|
private val targetSwarmSnodeCount = 2
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ object SnodeAPI {
|
|||||||
internal var powDifficulty = 1
|
internal var powDifficulty = 1
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
internal sealed class Error(val description: String) : Exception() {
|
internal sealed class Error(val description: String) : Exception(description) {
|
||||||
object Generic : Error("An error occurred.")
|
object Generic : Error("An error occurred.")
|
||||||
object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.")
|
object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.")
|
||||||
object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.")
|
object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.")
|
||||||
|
@ -25,6 +25,11 @@ public class Debouncer {
|
|||||||
this.threshold = threshold;
|
this.threshold = threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Debouncer(Handler handler, long threshold) {
|
||||||
|
this.handler = handler;
|
||||||
|
this.threshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
public void publish(Runnable runnable) {
|
public void publish(Runnable runnable) {
|
||||||
handler.removeCallbacksAndMessages(null);
|
handler.removeCallbacksAndMessages(null);
|
||||||
handler.postDelayed(runnable, threshold);
|
handler.postDelayed(runnable, threshold);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier
|
||||||
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
|
||||||
@ -36,8 +37,8 @@ class SSKEnvironment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MessageExpirationManagerProtocol {
|
interface MessageExpirationManagerProtocol {
|
||||||
fun setExpirationTimer(messageID: Long?, duration: Int, senderPublicKey: String, content: SignalServiceProtos.Content)
|
fun setExpirationTimer(message: ExpirationTimerUpdate)
|
||||||
fun disableExpirationTimer(messageID: Long?, senderPublicKey: String, content: SignalServiceProtos.Content)
|
fun disableExpirationTimer(message: ExpirationTimerUpdate)
|
||||||
fun startAnyExpiration(timestamp: Long, author: String)
|
fun startAnyExpiration(timestamp: Long, author: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,10 +36,11 @@ message TypingMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message Content {
|
message Content {
|
||||||
optional DataMessage dataMessage = 1;
|
optional DataMessage dataMessage = 1;
|
||||||
optional ReceiptMessage receiptMessage = 5;
|
optional ReceiptMessage receiptMessage = 5;
|
||||||
optional TypingMessage typingMessage = 6;
|
optional TypingMessage typingMessage = 6;
|
||||||
optional ConfigurationMessage configurationMessage = 7;
|
optional ConfigurationMessage configurationMessage = 7;
|
||||||
|
optional DataExtractionNotification dataExtractionNotification = 82;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClosedGroupCiphertextMessageWrapper {
|
message ClosedGroupCiphertextMessageWrapper {
|
||||||
@ -56,6 +57,18 @@ message KeyPair {
|
|||||||
required bytes privateKey = 2;
|
required bytes privateKey = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DataExtractionNotification {
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
SCREENSHOT = 1;
|
||||||
|
MEDIA_SAVED = 2; // timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// @required
|
||||||
|
required Type type = 1;
|
||||||
|
optional uint64 timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message DataMessage {
|
message DataMessage {
|
||||||
|
|
||||||
enum Flags {
|
enum Flags {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,95 +0,0 @@
|
|||||||
package org.session.libsignal.service.loki.api
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.*
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private class PromiseCanceledException : Exception("Promise canceled.")
|
|
||||||
|
|
||||||
class Poller(public var userPublicKey: String, private val database: LokiAPIDatabaseProtocol, private val onMessagesReceived: (List<SignalServiceProtos.Envelope>) -> Unit) {
|
|
||||||
private var hasStarted: Boolean = false
|
|
||||||
private val usedSnodes: MutableSet<Snode> = mutableSetOf()
|
|
||||||
public var isCaughtUp = false
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val retryInterval: Long = 1 * 1000
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Public API
|
|
||||||
fun startIfNeeded() {
|
|
||||||
if (hasStarted) { return }
|
|
||||||
Log.d("Loki", "Started polling.")
|
|
||||||
hasStarted = true
|
|
||||||
setUpPolling()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopIfNeeded() {
|
|
||||||
Log.d("Loki", "Stopped polling.")
|
|
||||||
hasStarted = false
|
|
||||||
usedSnodes.clear()
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Private API
|
|
||||||
private fun setUpPolling() {
|
|
||||||
if (!hasStarted) { return; }
|
|
||||||
val thread = Thread.currentThread()
|
|
||||||
SwarmAPI.shared.getSwarm(userPublicKey).bind(SnodeAPI.messagePollingContext) {
|
|
||||||
usedSnodes.clear()
|
|
||||||
val deferred = deferred<Unit, Exception>(SnodeAPI.messagePollingContext)
|
|
||||||
pollNextSnode(deferred)
|
|
||||||
deferred.promise
|
|
||||||
}.always {
|
|
||||||
Timer().schedule(object : TimerTask() {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
thread.run { setUpPolling() }
|
|
||||||
}
|
|
||||||
}, retryInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollNextSnode(deferred: Deferred<Unit, Exception>) {
|
|
||||||
val swarm = database.getSwarm(userPublicKey) ?: setOf()
|
|
||||||
val unusedSnodes = swarm.subtract(usedSnodes)
|
|
||||||
if (unusedSnodes.isNotEmpty()) {
|
|
||||||
val index = SecureRandom().nextInt(unusedSnodes.size)
|
|
||||||
val nextSnode = unusedSnodes.elementAt(index)
|
|
||||||
usedSnodes.add(nextSnode)
|
|
||||||
Log.d("Loki", "Polling $nextSnode.")
|
|
||||||
poll(nextSnode, deferred).fail { exception ->
|
|
||||||
if (exception is PromiseCanceledException) {
|
|
||||||
Log.d("Loki", "Polling $nextSnode canceled.")
|
|
||||||
} else {
|
|
||||||
Log.d("Loki", "Polling $nextSnode failed; dropping it and switching to next snode.")
|
|
||||||
SwarmAPI.shared.dropSnodeFromSwarmIfNeeded(nextSnode, userPublicKey)
|
|
||||||
pollNextSnode(deferred)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isCaughtUp = true
|
|
||||||
deferred.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun poll(snode: Snode, deferred: Deferred<Unit, Exception>): Promise<Unit, Exception> {
|
|
||||||
if (!hasStarted) { return Promise.ofFail(PromiseCanceledException()) }
|
|
||||||
return SnodeAPI.shared.getRawMessages(snode, userPublicKey).bind(SnodeAPI.messagePollingContext) { rawResponse ->
|
|
||||||
isCaughtUp = true
|
|
||||||
if (deferred.promise.isDone()) {
|
|
||||||
task { Unit } // The long polling connection has been canceled; don't recurse
|
|
||||||
} else {
|
|
||||||
val messages = SnodeAPI.shared.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
|
||||||
onMessagesReceived(messages)
|
|
||||||
poll(snode, deferred)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -1,17 +1,18 @@
|
|||||||
package org.session.libsignal.service.loki.api
|
package org.session.libsignal.service.loki.api
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.deferred
|
import nl.komponents.kovenant.deferred
|
||||||
import nl.komponents.kovenant.functional.bind
|
import nl.komponents.kovenant.functional.bind
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import nl.komponents.kovenant.task
|
import nl.komponents.kovenant.task
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.service.loki.api.utilities.HTTP
|
import org.session.libsignal.service.loki.api.utilities.HTTP
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
|
||||||
import org.session.libsignal.service.loki.utilities.getRandomElement
|
import org.session.libsignal.service.loki.utilities.getRandomElement
|
||||||
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
import org.session.libsignal.service.loki.utilities.prettifiedDescription
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -23,7 +24,14 @@ class SwarmAPI private constructor(private val database: LokiAPIDatabaseProtocol
|
|||||||
set(newValue) { database.setSnodePool(newValue) }
|
set(newValue) { database.setSnodePool(newValue) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val seedNodePool: Set<String> = setOf( "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" )
|
|
||||||
|
// use port 4433 if API level can handle network security config and enforce pinned certificates
|
||||||
|
private val seedPort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4433
|
||||||
|
private val seedNodePool: Set<String> = setOf(
|
||||||
|
"https://storage.seed1.loki.network:$seedPort",
|
||||||
|
"https://storage.seed3.loki.network:$seedPort",
|
||||||
|
"https://public.loki.foundation:$seedPort"
|
||||||
|
)
|
||||||
|
|
||||||
// region Settings
|
// region Settings
|
||||||
private val minimumSnodePoolCount = 64
|
private val minimumSnodePoolCount = 64
|
||||||
|
@ -1,386 +0,0 @@
|
|||||||
package org.session.libsignal.service.loki.api.opengroups
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Kovenant
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.deferred
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import nl.komponents.kovenant.then
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsignal.service.loki.api.LokiDotNetAPI
|
|
||||||
import org.session.libsignal.service.loki.api.SnodeAPI
|
|
||||||
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
|
|
||||||
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.database.LokiOpenGroupDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.database.LokiUserDatabaseProtocol
|
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.service.loki.utilities.retryIfNeeded
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class PublicChatAPI(userPublicKey: String, private val userPrivateKey: ByteArray, private val apiDatabase: LokiAPIDatabaseProtocol,
|
|
||||||
private val userDatabase: LokiUserDatabaseProtocol, private val openGroupDatabase: LokiOpenGroupDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, apiDatabase) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
|
||||||
val sharedContext = Kovenant.createContext()
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
private val fallbackBatchCount = 64
|
|
||||||
private val maxRetryCount = 8
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private val channelInfoType = "net.patter-app.settings"
|
|
||||||
private val attachmentType = "net.app.core.oembed"
|
|
||||||
@JvmStatic
|
|
||||||
val publicChatMessageType = "network.loki.messenger.publicChat"
|
|
||||||
@JvmStatic
|
|
||||||
val profilePictureType = "network.loki.messenger.avatar"
|
|
||||||
|
|
||||||
fun getDefaultChats(): List<PublicChat> {
|
|
||||||
return listOf() // Don't auto-join any open groups right now
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
|
||||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
|
||||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Public API
|
|
||||||
fun getMessages(channel: Long, server: String): Promise<List<PublicChatMessage>, Exception> {
|
|
||||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
|
||||||
val lastMessageServerID = apiDatabase.getLastMessageServerID(channel, server)
|
|
||||||
if (lastMessageServerID != null) {
|
|
||||||
parameters["since_id"] = lastMessageServerID
|
|
||||||
} else {
|
|
||||||
parameters["count"] = fallbackBatchCount
|
|
||||||
parameters["include_deleted"] = 0
|
|
||||||
}
|
|
||||||
return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as List<Map<*, *>>
|
|
||||||
val messages = data.mapNotNull { message ->
|
|
||||||
try {
|
|
||||||
val isDeleted = message["is_deleted"] as? Boolean ?: false
|
|
||||||
if (isDeleted) { return@mapNotNull null }
|
|
||||||
// Ignore messages without annotations
|
|
||||||
if (message["annotations"] == null) { return@mapNotNull null }
|
|
||||||
val annotation = (message["annotations"] as List<Map<*, *>>).find {
|
|
||||||
((it["type"] as? String ?: "") == publicChatMessageType) && it["value"] != null
|
|
||||||
} ?: return@mapNotNull null
|
|
||||||
val value = annotation["value"] as Map<*, *>
|
|
||||||
val serverID = message["id"] as? Long ?: (message["id"] as? Int)?.toLong() ?: (message["id"] as String).toLong()
|
|
||||||
val user = message["user"] as Map<*, *>
|
|
||||||
val publicKey = user["username"] as String
|
|
||||||
val displayName = user["name"] as? String ?: "Anonymous"
|
|
||||||
var profilePicture: PublicChatMessage.ProfilePicture? = null
|
|
||||||
if (user["annotations"] != null) {
|
|
||||||
val profilePictureAnnotation = (user["annotations"] as List<Map< *, *>>).find {
|
|
||||||
((it["type"] as? String ?: "") == profilePictureType) && it["value"] != null
|
|
||||||
}
|
|
||||||
val profilePictureAnnotationValue = profilePictureAnnotation?.get("value") as? Map<*, *>
|
|
||||||
if (profilePictureAnnotationValue != null && profilePictureAnnotationValue["profileKey"] != null && profilePictureAnnotationValue["url"] != null) {
|
|
||||||
try {
|
|
||||||
val profileKey = Base64.decode(profilePictureAnnotationValue["profileKey"] as String)
|
|
||||||
val url = profilePictureAnnotationValue["url"] as String
|
|
||||||
profilePicture = PublicChatMessage.ProfilePicture(profileKey, url)
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Suppress("NAME_SHADOWING") val body = message["text"] as String
|
|
||||||
val timestamp = value["timestamp"] as? Long ?: (value["timestamp"] as? Int)?.toLong() ?: (value["timestamp"] as String).toLong()
|
|
||||||
var quote: PublicChatMessage.Quote? = null
|
|
||||||
if (value["quote"] != null) {
|
|
||||||
val replyTo = message["reply_to"] as? Long ?: (message["reply_to"] as? Int)?.toLong() ?: (message["reply_to"] as String).toLong()
|
|
||||||
val quoteAnnotation = value["quote"] as? Map<*, *>
|
|
||||||
val quoteTimestamp = quoteAnnotation?.get("id") as? Long ?: (quoteAnnotation?.get("id") as? Int)?.toLong() ?: (quoteAnnotation?.get("id") as? String)?.toLong() ?: 0L
|
|
||||||
val author = quoteAnnotation?.get("author") as? String
|
|
||||||
val text = quoteAnnotation?.get("text") as? String
|
|
||||||
quote = if (quoteTimestamp > 0L && author != null && text != null) PublicChatMessage.Quote(quoteTimestamp, author, text, replyTo) else null
|
|
||||||
}
|
|
||||||
val attachmentsAsJSON = (message["annotations"] as List<Map<*, *>>).filter {
|
|
||||||
((it["type"] as? String ?: "") == attachmentType) && it["value"] != null
|
|
||||||
}
|
|
||||||
val attachments = attachmentsAsJSON.mapNotNull { it["value"] as? Map<*, *> }.mapNotNull { attachmentAsJSON ->
|
|
||||||
try {
|
|
||||||
val kindAsString = attachmentAsJSON["lokiType"] as String
|
|
||||||
val kind = PublicChatMessage.Attachment.Kind.values().first { it.rawValue == kindAsString }
|
|
||||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
|
||||||
val contentType = attachmentAsJSON["contentType"] as String
|
|
||||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
|
||||||
val fileName = attachmentAsJSON["fileName"] as String
|
|
||||||
val flags = 0
|
|
||||||
val url = attachmentAsJSON["url"] as String
|
|
||||||
val caption = attachmentAsJSON["caption"] as? String
|
|
||||||
val linkPreviewURL = attachmentAsJSON["linkPreviewUrl"] as? String
|
|
||||||
val linkPreviewTitle = attachmentAsJSON["linkPreviewTitle"] as? String
|
|
||||||
if (kind == PublicChatMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
PublicChatMessage.Attachment(kind, server, id, contentType, size, fileName, flags, 0, 0, caption, url, linkPreviewURL, linkPreviewTitle)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki","Couldn't parse attachment due to error: $e.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over
|
|
||||||
@Suppress("NAME_SHADOWING") val lastMessageServerID = apiDatabase.getLastMessageServerID(channel, server)
|
|
||||||
if (serverID > lastMessageServerID ?: 0) { apiDatabase.setLastMessageServerID(channel, server, serverID) }
|
|
||||||
val hexEncodedSignature = value["sig"] as String
|
|
||||||
val signatureVersion = value["sigver"] as? Long ?: (value["sigver"] as? Int)?.toLong() ?: (value["sigver"] as String).toLong()
|
|
||||||
val signature = PublicChatMessage.Signature(Hex.fromStringCondensed(hexEncodedSignature), signatureVersion)
|
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
||||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
|
||||||
val dateAsString = message["created_at"] as String
|
|
||||||
val serverTimestamp = format.parse(dateAsString).time
|
|
||||||
// Verify the message
|
|
||||||
val groupMessage = PublicChatMessage(serverID, publicKey, displayName, body, timestamp, publicChatMessageType, quote, attachments, profilePicture, signature, serverTimestamp)
|
|
||||||
if (groupMessage.hasValidSignature()) groupMessage else null
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)}. Exception: ${exception.message}")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
}.sortedBy { it.serverTimestamp }
|
|
||||||
messages
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse messages for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
|
||||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = mutableMapOf<String, Any>()
|
|
||||||
val lastDeletionServerID = apiDatabase.getLastDeletionServerID(channel, server)
|
|
||||||
if (lastDeletionServerID != null) {
|
|
||||||
parameters["since_id"] = lastDeletionServerID
|
|
||||||
} else {
|
|
||||||
parameters["count"] = fallbackBatchCount
|
|
||||||
}
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion ->
|
|
||||||
try {
|
|
||||||
val serverID = deletion["id"] as? Long ?: (deletion["id"] as? Int)?.toLong() ?: (deletion["id"] as String).toLong()
|
|
||||||
val messageServerID = deletion["message_id"] as? Long ?: (deletion["message_id"] as? Int)?.toLong() ?: (deletion["message_id"] as String).toLong()
|
|
||||||
@Suppress("NAME_SHADOWING") val lastDeletionServerID = apiDatabase.getLastDeletionServerID(channel, server)
|
|
||||||
if (serverID > (lastDeletionServerID ?: 0)) { apiDatabase.setLastDeletionServerID(channel, server, serverID) }
|
|
||||||
messageServerID
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse deleted message for open group with ID: $channel on server: $server. Exception: ${exception.message}")
|
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deletedMessageServerIDs
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse deleted messages for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(message: PublicChatMessage, channel: Long, server: String): Promise<PublicChatMessage, Exception> {
|
|
||||||
val deferred = deferred<PublicChatMessage, Exception>()
|
|
||||||
ThreadUtils.queue {
|
|
||||||
val signedMessage = message.sign(userPrivateKey)
|
|
||||||
if (signedMessage == null) {
|
|
||||||
deferred.reject(SnodeAPI.Error.MessageSigningFailed)
|
|
||||||
} else {
|
|
||||||
retryIfNeeded(maxRetryCount) {
|
|
||||||
Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.")
|
|
||||||
val parameters = signedMessage.toJSON()
|
|
||||||
execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as Map<*, *>
|
|
||||||
val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong()
|
|
||||||
val displayName = userDatabase.getDisplayName(userPublicKey) ?: "Anonymous"
|
|
||||||
val text = data["text"] as String
|
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
|
||||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
|
||||||
val dateAsString = data["created_at"] as String
|
|
||||||
val timestamp = format.parse(dateAsString).time
|
|
||||||
@Suppress("NAME_SHADOWING") val message = PublicChatMessage(serverID, userPublicKey, displayName, text, timestamp, publicChatMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
|
||||||
message
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.success {
|
|
||||||
deferred.resolve(it)
|
|
||||||
}.fail {
|
|
||||||
deferred.reject(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deferred.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMessage(messageServerID: Long, channel: Long, server: String, isSentByUser: Boolean): Promise<Long, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val isModerationRequest = !isSentByUser
|
|
||||||
Log.d("Loki", "Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
|
||||||
val endpoint = if (isSentByUser) "channels/$channel/messages/$messageServerID" else "loki/v1/moderation/message/$messageServerID"
|
|
||||||
execute(HTTPVerb.DELETE, server, endpoint, isJSONRequired = false).then {
|
|
||||||
Log.d("Loki", "Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server.")
|
|
||||||
messageServerID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val isModerationRequest = !isSentByUser
|
|
||||||
val parameters = mapOf( "ids" to messageServerIDs.joinToString(",") )
|
|
||||||
Log.d("Loki", "Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
|
||||||
val endpoint = if (isSentByUser) "loki/v1/messages" else "loki/v1/moderation/messages"
|
|
||||||
execute(HTTPVerb.DELETE, server, endpoint, parameters = parameters, isJSONRequired = false).then { json ->
|
|
||||||
Log.d("Loki", "Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server.")
|
|
||||||
messageServerIDs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
|
|
||||||
val moderatorsAsSet = moderators.orEmpty().toSet()
|
|
||||||
if (Companion.moderators[server] != null) {
|
|
||||||
Companion.moderators[server]!![channel] = moderatorsAsSet
|
|
||||||
} else {
|
|
||||||
Companion.moderators[server] = hashMapOf( channel to moderatorsAsSet )
|
|
||||||
}
|
|
||||||
moderatorsAsSet
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse moderators for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChannelInfo(channel: Long, server: String): Promise<PublicChatInfo, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val parameters = mapOf( "include_annotations" to 1 )
|
|
||||||
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then(sharedContext) { json ->
|
|
||||||
try {
|
|
||||||
val data = json["data"] as Map<*, *>
|
|
||||||
val annotations = data["annotations"] as List<Map<*, *>>
|
|
||||||
val annotation = annotations.find { (it["type"] as? String ?: "") == channelInfoType } ?: throw SnodeAPI.Error.ParsingFailed
|
|
||||||
val info = annotation["value"] as Map<*, *>
|
|
||||||
val displayName = info["name"] as String
|
|
||||||
val countInfo = data["counts"] as Map<*, *>
|
|
||||||
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
|
||||||
val profilePictureURL = info["avatar"] as String
|
|
||||||
val publicChatInfo = PublicChatInfo(displayName, profilePictureURL, memberCount)
|
|
||||||
apiDatabase.setUserCount(channel, server, memberCount)
|
|
||||||
publicChatInfo
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: PublicChatInfo, isForcedUpdate: Boolean) {
|
|
||||||
apiDatabase.setUserCount(channel, server, info.memberCount)
|
|
||||||
openGroupDatabase.updateTitle(groupID, info.displayName)
|
|
||||||
// Download and update profile picture if needed
|
|
||||||
val oldProfilePictureURL = apiDatabase.getOpenGroupProfilePictureURL(channel, server)
|
|
||||||
if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) {
|
|
||||||
val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return
|
|
||||||
openGroupDatabase.updateProfilePicture(groupID, profilePictureAsByteArray)
|
|
||||||
apiDatabase.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
|
||||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
|
||||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
try {
|
|
||||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
|
||||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
|
||||||
return outputStream.toByteArray()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Failed to download open group profile picture from \"$url\" due to error: $e.")
|
|
||||||
return null
|
|
||||||
} finally {
|
|
||||||
outputStream.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
|
||||||
Log.d("Loki", "Joined channel with ID: $channel on server: $server.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
|
||||||
Log.d("Loki", "Left channel with ID: $channel on server: $server.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
|
||||||
Log.d("Loki", "Banned user with ID: $publicKey from $server")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
|
||||||
return getUserProfiles(publicKeys, server, false).map(sharedContext) { json ->
|
|
||||||
val mapping = mutableMapOf<String, String>()
|
|
||||||
for (user in json) {
|
|
||||||
if (user["username"] != null) {
|
|
||||||
val publicKey = user["username"] as String
|
|
||||||
val displayName = user["name"] as? String ?: "Anonymous"
|
|
||||||
mapping[publicKey] = displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapping
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
|
||||||
Log.d("Loki", "Updating display name on server: $server.")
|
|
||||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
|
||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
|
||||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setProfilePicture(server: String, profileKey: String, url: String?): Promise<Unit, Exception> {
|
|
||||||
Log.d("Loki", "Updating profile picture on server: $server.")
|
|
||||||
val value = when (url) {
|
|
||||||
null -> null
|
|
||||||
else -> mapOf( "profileKey" to profileKey, "url" to url )
|
|
||||||
}
|
|
||||||
// TODO: This may actually completely replace the annotations, have to double check it
|
|
||||||
return setSelfAnnotation(server, profilePictureType, value).map { Unit }.fail {
|
|
||||||
Log.d("Loki", "Failed to update profile picture due to error: $it.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -50,7 +50,7 @@ class MnemonicCodec(private val loadFileContents: (String) -> String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class DecodingError(val description: String) : Exception() {
|
sealed class DecodingError(val description: String) : Exception(description) {
|
||||||
object Generic : DecodingError("Something went wrong. Please check your mnemonic and try again.")
|
object Generic : DecodingError("Something went wrong. Please check your mnemonic and try again.")
|
||||||
object InputTooShort : DecodingError("Looks like you didn't enter enough words. Please check your mnemonic and try again.")
|
object InputTooShort : DecodingError("Looks like you didn't enter enough words. Please check your mnemonic and try again.")
|
||||||
object MissingLastWord : DecodingError("You seem to be missing the last word of your mnemonic. Please check what you entered and try again.")
|
object MissingLastWord : DecodingError("You seem to be missing the last word of your mnemonic. Please check what you entered and try again.")
|
||||||
|
Loading…
Reference in New Issue
Block a user