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 polling-limit-after-inactivity
This commit is contained in:
commit
becc3c7278
@ -143,8 +143,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 170
|
def canonicalVersionCode = 171
|
||||||
def canonicalVersionName = "1.10.7"
|
def canonicalVersionName = "1.10.8"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -22,19 +22,15 @@ 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 android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
import androidx.multidex.MultiDexApplication;
|
import androidx.multidex.MultiDexApplication;
|
||||||
|
|
||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.session.libsession.avatars.AvatarHelper;
|
import org.session.libsession.avatars.AvatarHelper;
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI;
|
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager;
|
import org.session.libsession.messaging.mentions.MentionsManager;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||||
@ -174,9 +170,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
|
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
|
||||||
}
|
}
|
||||||
setUpStorageAPIIfNeeded();
|
|
||||||
resubmitProfilePictureIfNeeded();
|
resubmitProfilePictureIfNeeded();
|
||||||
updateOpenGroupProfilePicturesIfNeeded();
|
|
||||||
if (userPublicKey != null) {
|
if (userPublicKey != null) {
|
||||||
registerForFCMIfNeeded(false);
|
registerForFCMIfNeeded(false);
|
||||||
}
|
}
|
||||||
@ -401,20 +395,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProviderInitializationException extends RuntimeException {
|
private static class ProviderInitializationException extends RuntimeException { }
|
||||||
}
|
|
||||||
|
|
||||||
// region Loki
|
|
||||||
public boolean setUpStorageAPIIfNeeded() {
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null || !IdentityKeyUtil.hasIdentityKey(this)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
|
||||||
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
|
||||||
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void registerForFCMIfNeeded(final Boolean force) {
|
public void registerForFCMIfNeeded(final Boolean force) {
|
||||||
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
|
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
|
||||||
@ -490,19 +471,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
|
||||||
AsyncTask.execute(() -> {
|
|
||||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
|
||||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
|
||||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
|
||||||
for (String server : servers) {
|
|
||||||
if (profileKey != null) {
|
|
||||||
OpenGroupAPI.setProfilePicture(server, profileKey, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||||
String token = TextSecurePreferences.getFCMToken(this);
|
String token = TextSecurePreferences.getFCMToken(this);
|
||||||
if (token != null && !token.isEmpty()) {
|
if (token != null && !token.isEmpty()) {
|
||||||
|
@ -30,18 +30,15 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.messages.visible.LinkPreview;
|
import org.session.libsession.messaging.messages.visible.LinkPreview;
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
||||||
import org.session.libsession.messaging.messages.visible.Quote;
|
import org.session.libsession.messaging.messages.visible.Quote;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
import org.session.libsession.messaging.utilities.UpdateMessageData;
|
||||||
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
|
||||||
@ -264,7 +261,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
toFrom.setText(toFromRes);
|
toFrom.setText(toFromRes);
|
||||||
long threadID = messageRecord.getThreadId();
|
long threadID = messageRecord.getThreadId();
|
||||||
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID);
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID);
|
||||||
if (openGroup != null && messageRecord.isOutgoing()) {
|
if (openGroup != null && messageRecord.isOutgoing()) {
|
||||||
toFrom.setVisibility(View.GONE);
|
toFrom.setVisibility(View.GONE);
|
||||||
separator.setVisibility(View.GONE);
|
separator.setVisibility(View.GONE);
|
||||||
|
@ -5,10 +5,9 @@ import android.text.TextUtils
|
|||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.database.MessageDataProvider
|
import org.session.libsession.database.MessageDataProvider
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.utilities.UploadResult
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.messages.SignalServiceAttachment
|
import org.session.libsignal.messages.SignalServiceAttachment
|
||||||
@ -104,11 +103,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
|
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOpenGroup(threadID: Long): OpenGroup? {
|
override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||||
return null // TODO: Implement
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
|
||||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||||
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
|
||||||
|
@ -13,24 +13,18 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient;
|
import org.session.libsession.utilities.recipients.Recipient;
|
||||||
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
import org.session.libsession.utilities.recipients.RecipientModifiedListener;
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
@ -197,15 +191,11 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
|
|||||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||||
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress().serialize());
|
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress().serialize());
|
||||||
|
|
||||||
String quoteeDisplayName = author.toShortString();
|
String quoteeDisplayName;
|
||||||
|
|
||||||
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient);
|
|
||||||
String senderHexEncodedPublicKey = author.getAddress().serialize();
|
String senderHexEncodedPublicKey = author.getAddress().serialize();
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
|
|
||||||
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
|
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
|
||||||
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
|
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
|
||||||
} else if (publicChat != null) {
|
|
||||||
quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), senderHexEncodedPublicKey);
|
|
||||||
} else {
|
} else {
|
||||||
quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(senderHexEncodedPublicKey);
|
quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(senderHexEncodedPublicKey);
|
||||||
}
|
}
|
||||||
|
@ -77,9 +77,7 @@ import androidx.core.view.MenuItemCompat;
|
|||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
@ -90,7 +88,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessa
|
|||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
@ -193,7 +190,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
|
|||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -204,7 +200,6 @@ import java.util.Locale;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@ -377,12 +372,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
|
||||||
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (openGroupV2 != null) {
|
||||||
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
|
||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
|
||||||
} else if (openGroupV2 != null) {
|
|
||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
||||||
if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen")
|
if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen")
|
||||||
|| openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) {
|
|| openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) {
|
||||||
@ -1419,13 +1410,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
|
||||||
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null &&
|
|
||||||
publicChat.getChannel() == event.getChannel() &&
|
|
||||||
publicChat.getServer().equals(event.getUrl())) {
|
|
||||||
this.updateSubtitleTextView();
|
|
||||||
}
|
|
||||||
if (openGroup != null &&
|
if (openGroup != null &&
|
||||||
openGroup.getRoom().equals(event.getRoom()) &&
|
openGroup.getRoom().equals(event.getRoom()) &&
|
||||||
openGroup.getServer().equals(event.getUrl())) {
|
openGroup.getServer().equals(event.getUrl())) {
|
||||||
@ -2380,13 +2365,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
muteIndicatorImageView.setVisibility(View.VISIBLE);
|
muteIndicatorImageView.setVisibility(View.VISIBLE);
|
||||||
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
||||||
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
|
||||||
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (openGroup != null) {
|
||||||
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
|
|
||||||
if (userCount == null) { userCount = 0; }
|
|
||||||
subtitleTextView.setText(userCount + " members");
|
|
||||||
} else if (openGroup != null) {
|
|
||||||
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
|
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
|
||||||
if (userCount == null) { userCount = 0; }
|
if (userCount == null) { userCount = 0; }
|
||||||
subtitleTextView.setText(userCount + " members");
|
subtitleTextView.setText(userCount + " members");
|
||||||
|
@ -54,17 +54,13 @@ import androidx.loader.content.Loader;
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.Quote;
|
import org.session.libsession.messaging.messages.visible.Quote;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
||||||
@ -398,9 +394,8 @@ public class ConversationFragment extends Fragment
|
|||||||
boolean isGroupChat = recipient.isGroupRecipient();
|
boolean isGroupChat = recipient.isGroupRecipient();
|
||||||
|
|
||||||
if (isGroupChat) {
|
if (isGroupChat) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
|
||||||
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
boolean isPublicChat = (publicChat != null || openGroupChat != null);
|
boolean isPublicChat = (openGroupChat != null);
|
||||||
int selectedMessageCount = messageRecords.size();
|
int selectedMessageCount = messageRecords.size();
|
||||||
boolean areAllSentByUser = true;
|
boolean areAllSentByUser = true;
|
||||||
Set<String> uniqueUserSet = new HashSet<>();
|
Set<String> uniqueUserSet = new HashSet<>();
|
||||||
@ -412,10 +407,7 @@ public class ConversationFragment extends Fragment
|
|||||||
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
||||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext());
|
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext());
|
||||||
boolean userCanModerate =
|
boolean userCanModerate =
|
||||||
(isPublicChat &&
|
(isPublicChat && (OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())));
|
||||||
((publicChat != null && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()))
|
|
||||||
|| (openGroupChat != null && OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())))
|
|
||||||
);
|
|
||||||
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
||||||
// allow banning if moderating a public chat and only one user's messages are selected
|
// allow banning if moderating a public chat and only one user's messages are selected
|
||||||
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
||||||
@ -515,7 +507,6 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
|
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
|
||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
|
||||||
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
||||||
@ -527,7 +518,7 @@ public class ConversationFragment extends Fragment
|
|||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(MessageRecord... messageRecords) {
|
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||||
if (publicChat != null || openGroupChat != null) {
|
if (openGroupChat != null) {
|
||||||
ArrayList<Long> serverIDs = new ArrayList<>();
|
ArrayList<Long> serverIDs = new ArrayList<>();
|
||||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||||
ArrayList<Long> failedMessages = new ArrayList<>();
|
ArrayList<Long> failedMessages = new ArrayList<>();
|
||||||
@ -541,29 +532,7 @@ public class ConversationFragment extends Fragment
|
|||||||
ignoredMessages.add(messageRecord.getId());
|
ignoredMessages.add(messageRecord.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (publicChat != null) {
|
if (openGroupChat != null) {
|
||||||
OpenGroupAPI
|
|
||||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
|
||||||
.success(l -> {
|
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
|
||||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
|
||||||
if (l.contains(serverID)) {
|
|
||||||
if (messageRecord.isMms()) {
|
|
||||||
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
|
||||||
} else {
|
|
||||||
DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
|
|
||||||
}
|
|
||||||
} else if (!ignoredMessages.contains(serverID)) {
|
|
||||||
failedMessages.add(messageRecord.getId());
|
|
||||||
Log.w("Loki", "Failed to delete message: " + messageRecord.getId() + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}). fail(e -> {
|
|
||||||
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} else if (openGroupChat != null) {
|
|
||||||
for (Long serverId : serverIDs) {
|
for (Long serverId : serverIDs) {
|
||||||
OpenGroupAPIV2
|
OpenGroupAPIV2
|
||||||
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
@ -617,7 +586,6 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
final OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
|
||||||
final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
||||||
@ -630,17 +598,7 @@ public class ConversationFragment extends Fragment
|
|||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(String... userPublicKeyParam) {
|
protected Void doInBackground(String... userPublicKeyParam) {
|
||||||
String userPublicKey = userPublicKeyParam[0];
|
String userPublicKey = userPublicKeyParam[0];
|
||||||
if (publicChat != null) {
|
if (openGroupChat != null) {
|
||||||
OpenGroupAPI
|
|
||||||
.ban(userPublicKey, publicChat.getServer())
|
|
||||||
.success(l -> {
|
|
||||||
Log.d("Loki", "User banned");
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}).fail(e -> {
|
|
||||||
Log.e("Loki", "Couldn't ban user due to error",e);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} else if (openGroupChat != null) {
|
|
||||||
OpenGroupAPIV2
|
OpenGroupAPIV2
|
||||||
.ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())
|
.ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
.success(l -> {
|
.success(l -> {
|
||||||
|
@ -45,17 +45,12 @@ import android.widget.ImageView;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.DimenRes;
|
import androidx.annotation.DimenRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
|
||||||
import org.session.libsession.messaging.jobs.JobQueue;
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
import org.session.libsession.messaging.open_groups.OpenGroupV2;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
@ -760,10 +755,6 @@ public class ConversationItem extends LinearLayout
|
|||||||
String publicKey = recipient.getAddress().toString();
|
String publicKey = recipient.getAddress().toString();
|
||||||
profilePictureView.setPublicKey(publicKey);
|
profilePictureView.setPublicKey(publicKey);
|
||||||
String displayName = recipient.getName();
|
String displayName = recipient.getName();
|
||||||
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
|
||||||
if (displayName == null && openGroup != null) {
|
|
||||||
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(openGroup.getId(), publicKey);
|
|
||||||
}
|
|
||||||
profilePictureView.setDisplayName(displayName);
|
profilePictureView.setDisplayName(displayName);
|
||||||
profilePictureView.setAdditionalPublicKey(null);
|
profilePictureView.setAdditionalPublicKey(null);
|
||||||
profilePictureView.setRSSFeed(false);
|
profilePictureView.setRSSFeed(false);
|
||||||
@ -898,20 +889,7 @@ public class ConversationItem extends LinearLayout
|
|||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
|
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
|
||||||
if (groupThread && !messageRecord.isOutgoing()) {
|
if (groupThread && !messageRecord.isOutgoing()) {
|
||||||
// Show custom display names for group chats
|
|
||||||
String displayName = recipient.toShortString();
|
String displayName = recipient.toShortString();
|
||||||
try {
|
|
||||||
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
|
|
||||||
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
|
|
||||||
if (senderDisplayName != null) {
|
|
||||||
displayName = senderDisplayName;
|
|
||||||
} else {
|
|
||||||
// opengroupv2 format
|
|
||||||
displayName = OpenGroupUtilities.getDisplayName(recipient);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
this.groupSender.setText(displayName);
|
this.groupSender.setText(displayName);
|
||||||
|
|
||||||
@ -952,12 +930,8 @@ public class ConversationItem extends LinearLayout
|
|||||||
profilePictureView.setVisibility(VISIBLE);
|
profilePictureView.setVisibility(VISIBLE);
|
||||||
int visibility = View.GONE;
|
int visibility = View.GONE;
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
|
||||||
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId());
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId());
|
||||||
if (publicChat != null) {
|
if (openGroupV2 != null) {
|
||||||
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
|
|
||||||
visibility = isModerator ? View.VISIBLE : View.GONE;
|
|
||||||
} else if (openGroupV2 != null) {
|
|
||||||
boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer());
|
boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer());
|
||||||
visibility = isModerator ? View.VISIBLE : View.GONE;
|
visibility = isModerator ? View.VISIBLE : View.GONE;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||||
@ -14,7 +13,6 @@ import org.session.libsession.messaging.messages.signal.*
|
|||||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
@ -41,12 +39,12 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
|||||||
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
import org.thoughtcrime.securesms.loki.api.OpenGroupManager
|
||||||
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.get
|
import org.thoughtcrime.securesms.loki.utilities.get
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
import org.thoughtcrime.securesms.loki.utilities.getString
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
|
|
||||||
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? {
|
||||||
return TextSecurePreferences.getLocalNumber(context)
|
return TextSecurePreferences.getLocalNumber(context)
|
||||||
}
|
}
|
||||||
@ -73,13 +71,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return TextSecurePreferences.getProfilePictureURL(context)
|
return TextSecurePreferences.getProfilePictureURL(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserProfilePictureUrl(newProfilePicture: String) {
|
override fun setUserProfilePictureURL(newValue: String) {
|
||||||
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
|
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
|
||||||
Recipient.from(context, it, false)
|
Recipient.from(context, it, false)
|
||||||
}
|
}
|
||||||
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture)
|
TextSecurePreferences.setProfilePictureURL(context, newValue)
|
||||||
RetrieveProfileAvatarJob(ourRecipient, newProfilePicture)
|
RetrieveProfileAvatarJob(ourRecipient, newValue)
|
||||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture))
|
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? {
|
override fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? {
|
||||||
@ -108,15 +106,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return registrationID
|
return registrationID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> {
|
override fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long> {
|
||||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
|
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
|
||||||
return database.insertAttachments(messageId, databaseAttachments)
|
return database.insertAttachments(messageID, databaseAttachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment> {
|
override fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment> {
|
||||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||||
return database.getAttachmentsForMessage(messageId)
|
return database.getAttachmentsForMessage(messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
|
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
|
||||||
@ -186,7 +184,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return messageID
|
return messageID
|
||||||
}
|
}
|
||||||
|
|
||||||
// JOBS
|
|
||||||
override fun persistJob(job: Job) {
|
override fun persistJob(job: Job) {
|
||||||
DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
|
DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
|
||||||
}
|
}
|
||||||
@ -220,20 +217,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return DatabaseFactory.getSessionJobDatabase(context).isJobCanceled(job)
|
return DatabaseFactory.getSessionJobDatabase(context).isJobCanceled(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization
|
|
||||||
|
|
||||||
override fun getAuthToken(server: String): String? {
|
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setAuthToken(server: String, newValue: String?) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeAuthToken(server: String) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAuthToken(room: String, server: String): String? {
|
override fun getAuthToken(room: String, server: String): String? {
|
||||||
val id = "$server.$room"
|
val id = "$server.$room"
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
|
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
|
||||||
@ -249,30 +232,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOpenGroup(threadID: String): OpenGroup? {
|
override fun getV2OpenGroup(threadId: Long): OpenGroupV2? {
|
||||||
if (threadID.toInt() < 0) { return null }
|
|
||||||
val database = databaseHelper.readableDatabase
|
|
||||||
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadID)) { cursor ->
|
|
||||||
val publicChatAsJSON = cursor.getString(LokiThreadDatabase.publicChat)
|
|
||||||
OpenGroup.fromJSON(publicChatAsJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getV2OpenGroup(threadId: String): OpenGroupV2? {
|
|
||||||
if (threadId.toInt() < 0) { return null }
|
if (threadId.toInt() < 0) { return null }
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
|
||||||
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
||||||
OpenGroupV2.fromJSON(publicChatAsJson)
|
OpenGroupV2.fromJSON(publicChatAsJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThreadID(openGroupID: String): String {
|
|
||||||
val address = Address.fromSerialized(openGroupID)
|
|
||||||
val recipient = Recipient.from(context, address, false)
|
|
||||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOpenGroupPublicKey(server: String): String? {
|
override fun getOpenGroupPublicKey(server: String): String? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupPublicKey(server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupPublicKey(server)
|
||||||
}
|
}
|
||||||
@ -281,63 +249,31 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupPublicKey(server, newValue)
|
DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupPublicKey(server, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) {
|
override fun getLastMessageServerID(room: String, server: String): Long? {
|
||||||
val groupID = "$server.$channel"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) {
|
|
||||||
val groupID = "$server.$room"
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
|
|
||||||
val groupID = "$server.$channel"
|
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? {
|
|
||||||
val groupID = "$server.$room"
|
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLastMessageServerId(room: String, server: String): Long? {
|
|
||||||
// return null if limit is set on open groups polling
|
// return null if limit is set on open groups polling
|
||||||
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLastMessageServerId(room: String, server: String, newValue: Long) {
|
override fun setLastMessageServerID(room: String, server: String, newValue: Long) {
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
|
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeLastMessageServerId(room: String, server: String) {
|
override fun removeLastMessageServerID(room: String, server: String) {
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
override fun getLastDeletionServerID(room: String, server: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(group, server, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeLastMessageServerID(group: Long, server: String) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLastDeletionServerId(room: String, server: String): Long? {
|
|
||||||
// return null if limit is set on open groups polling
|
// return null if limit is set on open groups polling
|
||||||
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) {
|
override fun setLastDeletionServerID(room: String, server: String, newValue: Long) {
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
|
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeLastDeletionServerId(room: String, server: String) {
|
override fun removeLastDeletionServerID(room: String, server: String) {
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,34 +281,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
|
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
|
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
|
||||||
}
|
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
|
||||||
|
|
||||||
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(group, server, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeLastDeletionServerID(group: Long, server: String) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isDuplicateMessage(timestamp: Long): Boolean {
|
override fun isDuplicateMessage(timestamp: Long): Boolean {
|
||||||
return getReceivedMessageTimestamps().contains(timestamp)
|
return getReceivedMessageTimestamps().contains(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(group, server, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupProfilePictureURL(group, server, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOpenGroupProfilePictureURL(group: Long, server: String): String? {
|
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupProfilePictureURL(group, server)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateTitle(groupID: String, newValue: String) {
|
override fun updateTitle(groupID: String, newValue: String) {
|
||||||
DatabaseFactory.getGroupDatabase(context).updateTitle(groupID, newValue)
|
DatabaseFactory.getGroupDatabase(context).updateTitle(groupID, newValue)
|
||||||
}
|
}
|
||||||
@ -399,15 +316,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.getMessageFor(timestamp, address)?.getId()
|
return database.getMessageFor(timestamp, address)?.getId()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
|
|
||||||
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
|
|
||||||
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
|
||||||
return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun markAsSent(timestamp: Long, author: String) {
|
override fun markAsSent(timestamp: Long, author: String) {
|
||||||
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
val messageRecord = database.getMessageFor(timestamp, author) ?: return
|
||||||
@ -466,7 +374,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
|
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getZombieMember(groupID: String): Set<String> {
|
override fun getZombieMembers(groupID: String): Set<String> {
|
||||||
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
|
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,7 +386,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
|
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateZombieMembers(groupID: String, members: List<Address>) {
|
override fun setZombieMembers(groupID: String, members: List<Address>) {
|
||||||
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
|
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -544,39 +452,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
|
|
||||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().mapValues { (_,chat)->
|
|
||||||
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
|
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOpenGroup(serverUrl: String, channel: Long) {
|
|
||||||
val httpUrl = HttpUrl.parse(serverUrl) ?: return
|
|
||||||
if (httpUrl.queryParameterNames().contains("public_key")) {
|
|
||||||
// open group v2
|
|
||||||
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
|
||||||
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
|
||||||
// non-standard port, add to server
|
|
||||||
this.port(httpUrl.port())
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
val room = httpUrl.pathSegments().firstOrNull() ?: return
|
|
||||||
val publicKey = httpUrl.queryParameter("public_key") ?: return
|
|
||||||
|
|
||||||
OpenGroupManager.add(server.toString().removeSuffix("/"), room, publicKey, context)
|
|
||||||
} else {
|
|
||||||
// TODO: No longer supported so let's remove this code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAllGroups(): List<GroupRecord> {
|
override fun getAllGroups(): List<GroupRecord> {
|
||||||
return DatabaseFactory.getGroupDatabase(context).allGroups
|
return DatabaseFactory.getGroupDatabase(context).allGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addOpenGroup(urlAsString: String) {
|
||||||
|
OpenGroupManager.addOpenGroup(urlAsString, context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setProfileSharing(address: Address, value: Boolean) {
|
override fun setProfileSharing(address: Address, value: Boolean) {
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
|
||||||
@ -601,15 +488,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThreadIdFor(address: Address): Long? {
|
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
|
||||||
val recipient = Recipient.from(context, address, false)
|
val address = Address.fromSerialized(publicKeyOrOpenGroupID)
|
||||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
return getThreadId(address)
|
||||||
return if (threadID < 0) null else threadID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun foo() {
|
override fun getThreadId(address: Address): Long? {
|
||||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
val recipient = Recipient.from(context, address, false)
|
||||||
|
return getThreadId(recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getThreadId(recipient: Recipient): Long? {
|
||||||
|
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
||||||
|
return if (threadID < 0) null else threadID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThreadIdForMms(mmsId: Long): Long {
|
override fun getThreadIdForMms(mmsId: Long): Long {
|
||||||
@ -621,22 +512,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return threadId
|
return threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestSentTimestamp(publicKey, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? {
|
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestProcessedTimestamp(publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) {
|
|
||||||
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDisplayName(publicKey: String): String? {
|
override fun getDisplayName(publicKey: String): String? {
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
}
|
}
|
||||||
@ -645,10 +520,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName)
|
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
|
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverID, publicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProfilePictureURL(publicKey: String): String? {
|
override fun getProfilePictureURL(publicKey: String): String? {
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getProfilePictureURL(publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getProfilePictureURL(publicKey)
|
||||||
}
|
}
|
||||||
@ -696,7 +567,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return PartAuthority.getAttachmentThumbnailUri(attachmentId)
|
return PartAuthority.getAttachmentThumbnailUri(attachmentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data Extraction Notification
|
|
||||||
override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) {
|
override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) {
|
||||||
val database = DatabaseFactory.getMmsDatabase(context)
|
val database = DatabaseFactory.getMmsDatabase(context)
|
||||||
val address = fromSerialized(senderPublicKey)
|
val address = fromSerialized(senderPublicKey)
|
||||||
|
@ -92,7 +92,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
|||||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
|
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
|
||||||
|
|
||||||
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
||||||
DownloadUtilities.downloadFile(attachment, pointer.getUrl(), MAX_AVATAR_SIZE, null);
|
DownloadUtilities.downloadFile(attachment, pointer.getUrl());
|
||||||
|
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||||
InputStream inputStream;
|
InputStream inputStream;
|
||||||
|
@ -100,7 +100,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
|||||||
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DownloadUtilities.downloadFile(downloadDestination, profileAvatar, MAX_PROFILE_SIZE_BYTES, null);
|
DownloadUtilities.downloadFile(downloadDestination, profileAvatar);
|
||||||
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
|
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
|
||||||
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityBackupRestoreBinding
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
import org.thoughtcrime.securesms.backup.FullBackupImporter
|
||||||
@ -61,31 +60,31 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setUpActionBarSessionLogo()
|
setUpActionBarSessionLogo()
|
||||||
|
|
||||||
val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
|
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
|
||||||
viewBinding.lifecycleOwner = this
|
// viewBinding.lifecycleOwner = this
|
||||||
viewBinding.viewModel = viewModel
|
// viewBinding.viewModel = viewModel
|
||||||
|
|
||||||
viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
|
// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() }
|
||||||
|
|
||||||
viewBinding.buttonSelectFile.setOnClickListener {
|
// viewBinding.buttonSelectFile.setOnClickListener {
|
||||||
fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
//FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
|
// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly
|
||||||
// and the backup files are unavailable for selection.
|
// // and the backup files are unavailable for selection.
|
||||||
// type = BackupUtil.BACKUP_FILE_MIME_TYPE
|
//// type = BackupUtil.BACKUP_FILE_MIME_TYPE
|
||||||
type = "*/*"
|
// type = "*/*"
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
|
// viewBinding.backupCode.addTextChangedListener { text -> viewModel.backupPassphrase.value = text.toString() }
|
||||||
|
|
||||||
// Focus passphrase text edit when backup file is selected.
|
// Focus passphrase text edit when backup file is selected.
|
||||||
viewModel.backupFile.observe(this, { backupFile ->
|
// viewModel.backupFile.observe(this, { backupFile ->
|
||||||
if (backupFile != null) viewBinding.backupCode.post {
|
// if (backupFile != null) viewBinding.backupCode.post {
|
||||||
viewBinding.backupCode.requestFocus()
|
// viewBinding.backupCode.requestFocus()
|
||||||
(getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
|
// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||||
.showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
|
// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
// React to backup import result.
|
// React to backup import result.
|
||||||
viewModel.backupImportResult.observe(this) { result ->
|
viewModel.backupImportResult.observe(this) { result ->
|
||||||
@ -116,8 +115,8 @@ class BackupRestoreActivity : BaseActionBarActivity() {
|
|||||||
openURL("https://getsession.org/privacy-policy/")
|
openURL("https://getsession.org/privacy-policy/")
|
||||||
}
|
}
|
||||||
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
}, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
viewBinding.termsTextView.text = termsExplanation
|
// viewBinding.termsTextView.text = termsExplanation
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +189,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica
|
|||||||
TextSecurePreferences.setHasViewedSeed(context, true)
|
TextSecurePreferences.setHasViewedSeed(context, true)
|
||||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
TextSecurePreferences.setHasSeenWelcomeScreen(context, true)
|
||||||
val application = ApplicationContext.getInstance(context)
|
val application = ApplicationContext.getInstance(context)
|
||||||
application.setUpStorageAPIIfNeeded()
|
|
||||||
|
|
||||||
BackupRestoreResult.SUCCESS
|
BackupRestoreResult.SUCCESS
|
||||||
} catch (e: DatabaseDowngradeException) {
|
} catch (e: DatabaseDowngradeException) {
|
||||||
|
@ -30,7 +30,6 @@ import org.greenrobot.eventbus.Subscribe
|
|||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
||||||
import org.session.libsession.utilities.*
|
import org.session.libsession.utilities.*
|
||||||
@ -344,16 +343,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete the conversation
|
// Delete the conversation
|
||||||
val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
|
||||||
val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
if (v1OpenGroup != null) {
|
if (v2OpenGroup != null) {
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
|
||||||
apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
|
||||||
apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server)
|
|
||||||
apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server)
|
|
||||||
OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server)
|
|
||||||
// FIXME: No longer supported so let's remove this code
|
|
||||||
} else if (v2OpenGroup != null) {
|
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
|
apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||||
apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)
|
apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)
|
||||||
|
@ -122,7 +122,6 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel
|
|||||||
}
|
}
|
||||||
// start polling and wait for updated message
|
// start polling and wait for updated message
|
||||||
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
|
ApplicationContext.getInstance(this@LinkDeviceActivity).apply {
|
||||||
setUpStorageAPIIfNeeded()
|
|
||||||
startPollingIfNeeded()
|
startPollingIfNeeded()
|
||||||
}
|
}
|
||||||
TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
|
TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect {
|
||||||
|
@ -153,7 +153,6 @@ class PNModeActivity : BaseActionBarActivity() {
|
|||||||
}
|
}
|
||||||
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView))
|
||||||
val application = ApplicationContext.getInstance(this)
|
val application = ApplicationContext.getInstance(this)
|
||||||
application.setUpStorageAPIIfNeeded()
|
|
||||||
application.startPollingIfNeeded()
|
application.startPollingIfNeeded()
|
||||||
application.registerForFCMIfNeeded(true)
|
application.registerForFCMIfNeeded(true)
|
||||||
val intent = Intent(this, HomeActivity::class.java)
|
val intent = Intent(this, HomeActivity::class.java)
|
||||||
|
@ -26,7 +26,6 @@ import nl.komponents.kovenant.all
|
|||||||
import nl.komponents.kovenant.ui.alwaysUi
|
import nl.komponents.kovenant.ui.alwaysUi
|
||||||
import nl.komponents.kovenant.ui.successUi
|
import nl.komponents.kovenant.ui.successUi
|
||||||
import org.session.libsession.avatars.AvatarHelper
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.ProfilePictureUtilities
|
import org.session.libsession.utilities.ProfilePictureUtilities
|
||||||
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
@ -179,8 +178,6 @@ 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 servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
|
||||||
promises.addAll(servers.map { OpenGroupAPI.setDisplayName(displayName, it) })
|
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
}
|
}
|
||||||
val profilePicture = profilePictureToBeUploaded
|
val profilePicture = profilePictureToBeUploaded
|
||||||
@ -195,7 +192,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
|
TextSecurePreferences.setLastProfilePictureUpload(this, Date().time)
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||||
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
|
||||||
}
|
}
|
||||||
if (profilePicture != null || displayName != null) {
|
if (profilePicture != null || displayName != null) {
|
||||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.api
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
@ -49,8 +50,8 @@ object OpenGroupManager {
|
|||||||
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
|
||||||
if (existingOpenGroup != null) { return }
|
if (existingOpenGroup != null) { return }
|
||||||
// Clear any existing data if needed
|
// Clear any existing data if needed
|
||||||
storage.removeLastDeletionServerId(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
storage.removeLastMessageServerId(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
// Store the public key
|
// Store the public key
|
||||||
storage.setOpenGroupPublicKey(server,publicKey)
|
storage.setOpenGroupPublicKey(server,publicKey)
|
||||||
// Get an auth token
|
// Get an auth token
|
||||||
@ -95,9 +96,22 @@ object OpenGroupManager {
|
|||||||
}
|
}
|
||||||
// Delete
|
// Delete
|
||||||
ThreadUtils.queue {
|
ThreadUtils.queue {
|
||||||
storage.removeLastDeletionServerId(room, server)
|
storage.removeLastDeletionServerID(room, server)
|
||||||
storage.removeLastMessageServerId(room, server)
|
storage.removeLastMessageServerID(room, server)
|
||||||
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
|
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addOpenGroup(urlAsString: String, context: Context) {
|
||||||
|
val url = HttpUrl.parse(urlAsString) ?: return
|
||||||
|
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
|
||||||
|
if (url.port() != 80 || url.port() != 443) {
|
||||||
|
// Non-standard port; add to server
|
||||||
|
builder.port(url.port())
|
||||||
|
}
|
||||||
|
val server = builder.build()
|
||||||
|
val room = url.pathSegments().firstOrNull() ?: return
|
||||||
|
val publicKey = url.queryParameter("public_key") ?: return
|
||||||
|
add(server.toString().removeSuffix("/"), room, publicKey, context)
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.loki.api
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||||
|
|
||||||
@ -57,36 +56,16 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
|||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
||||||
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
|
||||||
val room = inputData.getString(DATA_KEY_ROOM)
|
val room = inputData.getString(DATA_KEY_ROOM)
|
||||||
|
val openGroupId = "$serverUrl.$room"
|
||||||
val isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
|
return try {
|
||||||
|
Log.v(TAG, "Updating open group info for $openGroupId.")
|
||||||
if (!isOpenGroupV2) {
|
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
|
||||||
val publicChatId = OpenGroup.getId(channel, serverUrl)
|
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
|
||||||
|
Result.success()
|
||||||
return try {
|
} catch (e: Exception) {
|
||||||
Log.v(TAG, "Updating open group info for $publicChatId.")
|
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
||||||
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
|
Result.failure()
|
||||||
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
|
|
||||||
Result.success()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
|
|
||||||
Result.failure()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val openGroupId = "$serverUrl.$room"
|
|
||||||
|
|
||||||
return try {
|
|
||||||
Log.v(TAG, "Updating open group info for $openGroupId.")
|
|
||||||
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
|
|
||||||
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
|
|
||||||
Result.success()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
|
||||||
Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,13 @@ package org.thoughtcrime.securesms.loki.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
|
||||||
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
@ -22,7 +18,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
private val sessionResetTable = "loki_thread_session_reset_database"
|
private val sessionResetTable = "loki_thread_session_reset_database"
|
||||||
val publicChatTable = "loki_public_chat_database"
|
val publicChatTable = "loki_public_chat_database"
|
||||||
val threadID = "thread_id"
|
val threadID = "thread_id"
|
||||||
private val friendRequestStatus = "friend_request_status"
|
|
||||||
private val sessionResetStatus = "session_reset_status"
|
private val sessionResetStatus = "session_reset_status"
|
||||||
val publicChat = "public_chat"
|
val publicChat = "public_chat"
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@ -37,28 +32,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllPublicChats(): Map<Long, OpenGroup> {
|
|
||||||
val database = databaseHelper.readableDatabase
|
|
||||||
var cursor: Cursor? = null
|
|
||||||
val result = mutableMapOf<Long, OpenGroup>()
|
|
||||||
try {
|
|
||||||
cursor = database.rawQuery("select * from $publicChatTable", null)
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
|
||||||
val threadID = cursor.getLong(threadID)
|
|
||||||
val string = cursor.getString(publicChat)
|
|
||||||
val publicChat = OpenGroup.fromJSON(string)
|
|
||||||
if (publicChat != null) {
|
|
||||||
result[threadID] = publicChat
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Do nothing
|
|
||||||
} finally {
|
|
||||||
cursor?.close()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
var cursor: Cursor? = null
|
var cursor: Cursor? = null
|
||||||
@ -79,20 +52,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllPublicChatServers(): Set<String> {
|
|
||||||
return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPublicChat(threadID: Long): OpenGroup? {
|
|
||||||
if (threadID < 0) { return null }
|
|
||||||
|
|
||||||
val database = databaseHelper.readableDatabase
|
|
||||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
|
||||||
val publicChatAsJSON = cursor.getString(publicChat)
|
|
||||||
OpenGroup.fromJSON(publicChatAsJSON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
||||||
if (threadID < 0) {
|
if (threadID < 0) {
|
||||||
return null
|
return null
|
||||||
@ -114,19 +73,4 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
|
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
|
||||||
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
|
|
||||||
if (threadID < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val database = databaseHelper.writableDatabase
|
|
||||||
val contentValues = ContentValues(2)
|
|
||||||
contentValues.put(Companion.threadID, threadID)
|
|
||||||
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
|
|
||||||
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removePublicChat(threadID: Long) {
|
|
||||||
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -54,27 +54,6 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
|
|||||||
Recipient.from(context, Address.fromSerialized(publicKey), false).notifyListeners()
|
Recipient.from(context, Address.fromSerialized(publicKey), false).notifyListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
|
|
||||||
val database = databaseHelper.readableDatabase
|
|
||||||
return database.get(serverDisplayNameTable, "${Companion.publicKey} = ? AND ${Companion.serverID} = ?", arrayOf( publicKey, serverID )) { cursor ->
|
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(displayName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setServerDisplayName(serverID: String, publicKey: String, displayName: String) {
|
|
||||||
val database = databaseHelper.writableDatabase
|
|
||||||
val values = ContentValues(3)
|
|
||||||
values.put(Companion.serverID, serverID)
|
|
||||||
values.put(Companion.publicKey, publicKey)
|
|
||||||
values.put(Companion.displayName, displayName)
|
|
||||||
try {
|
|
||||||
database.insertWithOnConflict(serverDisplayNameTable, null, values, SQLiteDatabase.CONFLICT_REPLACE)
|
|
||||||
Recipient.from(context, Address.fromSerialized(publicKey), false).notifyListeners()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't save server display name due to exception: $e.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getProfilePictureURL(publicKey: String): String? {
|
override fun getProfilePictureURL(publicKey: String): String? {
|
||||||
return if (publicKey == TextSecurePreferences.getLocalNumber(context)) {
|
return if (publicKey == TextSecurePreferences.getLocalNumber(context)) {
|
||||||
TextSecurePreferences.getProfilePictureURL(context)
|
TextSecurePreferences.getProfilePictureURL(context)
|
||||||
|
@ -39,12 +39,6 @@ object SessionMetaProtocol {
|
|||||||
return shouldIgnoreMessage
|
return shouldIgnoreMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun shouldIgnoreDecryptionException(context: Context, timestamp: Long): Boolean {
|
|
||||||
val restorationTimestamp = TextSecurePreferences.getRestorationTime(context)
|
|
||||||
return timestamp <= restorationTimestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) {
|
fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) {
|
||||||
val displayName = content.senderDisplayName.orNull() ?: return
|
val displayName = content.senderDisplayName.orNull() ?: return
|
||||||
@ -58,24 +52,6 @@ object SessionMetaProtocol {
|
|||||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(sender, displayName)
|
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(sender, displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun handleProfileKeyUpdate(context: Context, content: SignalServiceContent) {
|
|
||||||
val message = content.dataMessage.get()
|
|
||||||
if (!message.profileKey.isPresent) { return }
|
|
||||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(content.sender), false)
|
|
||||||
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, message.profileKey.get())) {
|
|
||||||
database.setProfileKey(recipient, message.profileKey.get())
|
|
||||||
database.setUnidentifiedAccessMode(recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
|
||||||
val url = content.senderProfilePictureURL.or("")
|
|
||||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(recipient, url))
|
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
|
||||||
if (userPublicKey == content.sender) {
|
|
||||||
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun canUserReplyToNotification(recipient: Recipient): Boolean {
|
fun canUserReplyToNotification(recipient: Recipient): Boolean {
|
||||||
// TODO return !recipient.address.isRSSFeed
|
// TODO return !recipient.address.isRSSFeed
|
||||||
|
@ -27,15 +27,12 @@ object MentionUtilities {
|
|||||||
var matcher = pattern.matcher(text)
|
var matcher = pattern.matcher(text)
|
||||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
if (matcher.find(startIndex)) {
|
if (matcher.find(startIndex)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
||||||
val userDisplayName: String? = if (publicKey.toLowerCase() == userPublicKey.toLowerCase()) {
|
val userDisplayName: String? = if (publicKey.toLowerCase() == userPublicKey.toLowerCase()) {
|
||||||
TextSecurePreferences.getProfileName(context)
|
TextSecurePreferences.getProfileName(context)
|
||||||
} else if (publicChat != null) {
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
|
|
||||||
} else {
|
} else {
|
||||||
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
|||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
||||||
fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, context: Context): String {
|
fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, context: Context): String {
|
||||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(threadRecipient)
|
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
|
||||||
val publicKey = recipient.address.toString()
|
val publicKey = recipient.address.toString()
|
||||||
val displayName = if (publicChat != null) {
|
val displayName = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
|
// FIXME: Add short ID here?
|
||||||
} else {
|
|
||||||
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
|
||||||
}
|
|
||||||
return displayName ?: publicKey
|
return displayName ?: publicKey
|
||||||
}
|
}
|
@ -3,18 +3,10 @@ package org.thoughtcrime.securesms.loki.utilities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.ProfileKeyUtil
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
||||||
@ -28,23 +20,6 @@ object OpenGroupUtilities {
|
|||||||
*
|
*
|
||||||
* Consider using [org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker] for lazy approach.
|
* Consider using [org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker] for lazy approach.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
|
||||||
@WorkerThread
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
|
||||||
// Check if open group has a related DB record.
|
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID(OpenGroup.getId(channel, url).toByteArray())
|
|
||||||
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
|
||||||
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
|
||||||
}
|
|
||||||
|
|
||||||
val info = OpenGroupAPI.getChannelInfo(channel, url).get()
|
|
||||||
|
|
||||||
OpenGroupAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
|
||||||
|
|
||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -17,10 +17,10 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
|
||||||
var glide: GlideRequests? = null
|
var glide: GlideRequests? = null
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
|
||||||
var publicChatServer: String? = null
|
var openGroupServer: String? = null
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatServer = publicChatServer }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer }
|
||||||
var publicChatChannel: Long? = null
|
var openGroupRoom: String? = null
|
||||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatChannel = publicChatChannel }
|
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupRoom = openGroupRoom }
|
||||||
var onMentionCandidateSelected: ((Mention) -> Unit)? = null
|
var onMentionCandidateSelected: ((Mention) -> Unit)? = null
|
||||||
|
|
||||||
private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) }
|
private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) }
|
||||||
@ -29,8 +29,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
var mentionCandidates = listOf<Mention>()
|
var mentionCandidates = listOf<Mention>()
|
||||||
set(newValue) { field = newValue; notifyDataSetChanged() }
|
set(newValue) { field = newValue; notifyDataSetChanged() }
|
||||||
var glide: GlideRequests? = null
|
var glide: GlideRequests? = null
|
||||||
var publicChatServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var publicChatChannel: Long? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
override fun getCount(): Int {
|
override fun getCount(): Int {
|
||||||
return mentionCandidates.count()
|
return mentionCandidates.count()
|
||||||
@ -49,8 +49,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
val mentionCandidate = getItem(position)
|
val mentionCandidate = getItem(position)
|
||||||
cell.glide = glide
|
cell.glide = glide
|
||||||
cell.mentionCandidate = mentionCandidate
|
cell.mentionCandidate = mentionCandidate
|
||||||
cell.publicChatServer = publicChatServer
|
cell.openGroupServer = openGroupServer
|
||||||
cell.publicChatChannel = publicChatChannel
|
cell.openGroupRoom = openGroupRoom
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,10 +68,10 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun show(mentionCandidates: List<Mention>, threadID: Long) {
|
fun show(mentionCandidates: List<Mention>, threadID: Long) {
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
if (publicChat != null) {
|
if (openGroup != null) {
|
||||||
publicChatServer = publicChat.server
|
openGroupServer = openGroup.server
|
||||||
publicChatChannel = publicChat.channel
|
openGroupRoom = openGroup.room
|
||||||
}
|
}
|
||||||
this.mentionCandidates = mentionCandidates
|
this.mentionCandidates = mentionCandidates
|
||||||
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
||||||
|
@ -8,16 +8,16 @@ import android.view.ViewGroup
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
var mentionCandidate = Mention("", "")
|
var mentionCandidate = Mention("", "")
|
||||||
set(newValue) { field = newValue; update() }
|
set(newValue) { field = newValue; update() }
|
||||||
var glide: GlideRequests? = null
|
var glide: GlideRequests? = null
|
||||||
var publicChatServer: String? = null
|
var openGroupServer: String? = null
|
||||||
var publicChatChannel: Long? = null
|
var openGroupRoom: String? = null
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||||
constructor(context: Context) : this(context, null)
|
constructor(context: Context) : this(context, null)
|
||||||
@ -37,8 +37,8 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
|
|||||||
profilePictureView.isRSSFeed = false
|
profilePictureView.isRSSFeed = false
|
||||||
profilePictureView.glide = glide!!
|
profilePictureView.glide = glide!!
|
||||||
profilePictureView.update()
|
profilePictureView.update()
|
||||||
if (publicChatServer != null && publicChatChannel != null) {
|
if (openGroupServer != null && openGroupRoom != null) {
|
||||||
val isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
|
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
|
||||||
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
|
||||||
|
@ -29,7 +29,7 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
var additionalDisplayName: String? = null
|
var additionalDisplayName: String? = null
|
||||||
var isRSSFeed = false
|
var isRSSFeed = false
|
||||||
var isLarge = false
|
var isLarge = false
|
||||||
private val imagesCached = mutableSetOf<String>()
|
private val profilePicturesCached = mutableMapOf<String,String?>()
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) {
|
||||||
@ -61,11 +61,7 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
if (publicKey == null || publicKey.isBlank()) {
|
if (publicKey == null || publicKey.isBlank()) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
var result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
val result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
|
||||||
if (result == null && publicChat != null) {
|
|
||||||
result = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
|
|
||||||
}
|
|
||||||
return result ?: publicKey
|
return result ?: publicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,13 +142,13 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
|
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
|
||||||
if (publicKey.isNotEmpty()) {
|
if (publicKey.isNotEmpty()) {
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||||
if (imagesCached.contains(publicKey)) return
|
if (profilePicturesCached.containsKey(publicKey) && profilePicturesCached[publicKey] == recipient.profileAvatar) return
|
||||||
val signalProfilePicture = recipient.contactPhoto
|
val signalProfilePicture = recipient.contactPhoto
|
||||||
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
|
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
|
||||||
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
|
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
|
||||||
imagesCached.add(publicKey)
|
profilePicturesCached[publicKey] = recipient.profileAvatar
|
||||||
} else {
|
} else {
|
||||||
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
|
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
@ -162,7 +158,7 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
publicKey,
|
publicKey,
|
||||||
displayName
|
displayName
|
||||||
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||||
imagesCached.add(publicKey)
|
profilePicturesCached[publicKey] = recipient.profileAvatar
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
imageView.setImageDrawable(null)
|
imageView.setImageDrawable(null)
|
||||||
@ -170,7 +166,7 @@ class ProfilePictureView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
imagesCached.clear()
|
profilePicturesCached.clear()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,7 @@ class UserView : LinearLayout {
|
|||||||
if (publicKey == null || publicKey.isBlank()) {
|
if (publicKey == null || publicKey.isBlank()) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
var result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
val result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(openGroupThreadID)
|
|
||||||
if (result == null && publicChat != null) {
|
|
||||||
result = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
|
|
||||||
}
|
|
||||||
return result ?: publicKey
|
return result ?: publicKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI;
|
import org.session.libsession.messaging.file_server.FileServerAPIV2;
|
||||||
|
|
||||||
public class PushMediaConstraints extends MediaConstraints {
|
public class PushMediaConstraints extends MediaConstraints {
|
||||||
|
|
||||||
@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getImageMaxSize(Context context) {
|
public int getImageMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier());
|
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getGifMaxSize(Context context) {
|
public int getGifMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier());
|
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getVideoMaxSize(Context context) {
|
public int getVideoMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier());
|
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAudioMaxSize(Context context) {
|
public int getAudioMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier());
|
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDocumentMaxSize(Context context) {
|
public int getDocumentMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier());
|
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,4 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
|||||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||||
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateOpenGroupProfilePicturesIfNeeded(context: Context) {
|
|
||||||
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
package org.session.libsession.database
|
package org.session.libsession.database
|
||||||
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.*
|
import org.session.libsession.messaging.sending_receiving.attachments.*
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.utilities.UploadResult
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -13,31 +12,20 @@ interface MessageDataProvider {
|
|||||||
fun getMessageID(serverID: Long): Long?
|
fun getMessageID(serverID: Long): Long?
|
||||||
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||||
|
|
||||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||||
|
|
||||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||||
|
|
||||||
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||||
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
|
||||||
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
|
||||||
|
|
||||||
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
|
||||||
|
|
||||||
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream)
|
||||||
|
|
||||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||||
|
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
|
|
||||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||||
|
|
||||||
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
fun getMessageForQuote(timestamp: Long, author: Address): Pair<Long, Boolean>?
|
||||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||||
fun getMessageBodyFor(timestamp: Long, author: String): String
|
fun getMessageBodyFor(timestamp: Long, author: String): String
|
||||||
|
|
||||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||||
|
|
||||||
fun getOpenGroup(threadID: Long): OpenGroup?
|
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package org.session.libsession.database
|
package org.session.libsession.database
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||||
@ -9,7 +8,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
|||||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
@ -18,6 +16,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
|||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
||||||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||||
@ -32,14 +31,12 @@ 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 setUserProfilePictureURL(newValue: 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)
|
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
|
||||||
|
|
||||||
// Signal Protocol
|
// Signal
|
||||||
|
|
||||||
fun getOrGenerateRegistrationID(): Int
|
fun getOrGenerateRegistrationID(): Int
|
||||||
|
|
||||||
// Jobs
|
// Jobs
|
||||||
@ -59,48 +56,40 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
||||||
fun getV2OpenGroup(threadId: String): OpenGroupV2?
|
fun getV2OpenGroup(threadId: Long): OpenGroupV2?
|
||||||
|
fun addOpenGroup(urlAsString: String)
|
||||||
// Open Groups
|
|
||||||
fun getThreadID(openGroupID: String): String?
|
|
||||||
fun addOpenGroup(serverUrl: String, channel: Long)
|
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
|
||||||
|
|
||||||
// Open Group Public Keys
|
// Open Group Public Keys
|
||||||
fun getOpenGroupPublicKey(server: String): String?
|
fun getOpenGroupPublicKey(server: String): String?
|
||||||
fun setOpenGroupPublicKey(server: String, newValue: String)
|
fun setOpenGroupPublicKey(server: String, newValue: String)
|
||||||
|
|
||||||
// Open Group User Info
|
|
||||||
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
|
|
||||||
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
|
|
||||||
|
|
||||||
// Open Group Metadata
|
// Open Group Metadata
|
||||||
|
|
||||||
fun updateTitle(groupID: String, newValue: String)
|
fun updateTitle(groupID: String, newValue: String)
|
||||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||||
fun setUserCount(room: String, server: String, newValue: Int)
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
|
|
||||||
// Last Message Server ID
|
// Last Message Server ID
|
||||||
fun getLastMessageServerId(room: String, server: String): Long?
|
fun getLastMessageServerID(room: String, server: String): Long?
|
||||||
fun setLastMessageServerId(room: String, server: String, newValue: Long)
|
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
||||||
fun removeLastMessageServerId(room: String, server: String)
|
fun removeLastMessageServerID(room: String, server: String)
|
||||||
|
|
||||||
// Last Deletion Server ID
|
// Last Deletion Server ID
|
||||||
fun getLastDeletionServerId(room: String, server: String): Long?
|
fun getLastDeletionServerID(room: String, server: String): Long?
|
||||||
fun setLastDeletionServerId(room: String, server: String, newValue: Long)
|
fun setLastDeletionServerID(room: String, server: String, newValue: Long)
|
||||||
fun removeLastDeletionServerId(room: String, server: String)
|
fun removeLastDeletionServerID(room: String, server: String)
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
fun isDuplicateMessage(timestamp: Long): Boolean
|
fun isDuplicateMessage(timestamp: Long): Boolean
|
||||||
fun getReceivedMessageTimestamps(): Set<Long>
|
fun getReceivedMessageTimestamps(): Set<Long>
|
||||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||||
// Returns the IDs of the saved attachments.
|
/**
|
||||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
* Returns the IDs of the saved attachments.
|
||||||
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
*/
|
||||||
|
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||||
|
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||||
fun markAsSent(timestamp: Long, author: String)
|
fun markAsSent(timestamp: Long, author: String)
|
||||||
fun markUnidentified(timestamp: Long, author: String)
|
fun markUnidentified(timestamp: Long, author: String)
|
||||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||||
@ -110,11 +99,10 @@ interface StorageProtocol {
|
|||||||
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
||||||
fun isGroupActive(groupPublicKey: String): Boolean
|
fun isGroupActive(groupPublicKey: String): Boolean
|
||||||
fun setActive(groupID: String, value: Boolean)
|
fun setActive(groupID: String, value: Boolean)
|
||||||
fun getZombieMember(groupID: String): Set<String>
|
fun getZombieMembers(groupID: String): Set<String>
|
||||||
fun removeMember(groupID: String, member: Address)
|
fun removeMember(groupID: String, member: Address)
|
||||||
fun updateMembers(groupID: String, members: List<Address>)
|
fun updateMembers(groupID: String, members: List<Address>)
|
||||||
fun updateZombieMembers(groupID: String, members: List<Address>)
|
fun setZombieMembers(groupID: String, members: List<Address>)
|
||||||
// Closed Group
|
|
||||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||||
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
||||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||||
@ -122,9 +110,9 @@ interface StorageProtocol {
|
|||||||
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
|
||||||
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
|
||||||
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
|
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
|
||||||
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
|
||||||
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
|
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String,
|
||||||
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
|
||||||
fun isClosedGroup(publicKey: String): Boolean
|
fun isClosedGroup(publicKey: String): Boolean
|
||||||
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
|
||||||
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
|
||||||
@ -138,58 +126,27 @@ interface StorageProtocol {
|
|||||||
// Thread
|
// Thread
|
||||||
fun getOrCreateThreadIdFor(address: Address): Long
|
fun getOrCreateThreadIdFor(address: Address): Long
|
||||||
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
||||||
fun getThreadIdFor(address: Address): Long?
|
fun getThreadId(publicKeyOrOpenGroupID: String): Long?
|
||||||
|
fun getThreadId(address: Address): Long?
|
||||||
|
fun getThreadId(recipient: Recipient): Long?
|
||||||
fun getThreadIdForMms(mmsId: Long): Long
|
fun getThreadIdForMms(mmsId: Long): Long
|
||||||
fun getLastUpdated(threadID: Long): Long
|
fun getLastUpdated(threadID: Long): Long
|
||||||
|
|
||||||
// Session Request
|
// Contacts
|
||||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
|
||||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
|
||||||
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
|
||||||
fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long)
|
|
||||||
|
|
||||||
// Loki User
|
|
||||||
fun getDisplayName(publicKey: String): String?
|
fun getDisplayName(publicKey: String): String?
|
||||||
fun setDisplayName(publicKey: String, newName: String)
|
fun setDisplayName(publicKey: String, newName: String)
|
||||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
|
||||||
fun getProfilePictureURL(publicKey: String): String?
|
fun getProfilePictureURL(publicKey: String): String?
|
||||||
|
|
||||||
// Recipient
|
|
||||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||||
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||||
|
|
||||||
// PartAuthority
|
// Attachments
|
||||||
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
|
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
|
||||||
fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri
|
fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
/// Returns the ID of the `TSIncomingMessage` that was constructed.
|
/**
|
||||||
|
* Returns the ID of the `TSIncomingMessage` that was constructed.
|
||||||
|
*/
|
||||||
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
|
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
|
||||||
|
|
||||||
// Data Extraction Notification
|
|
||||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||||
|
|
||||||
// DEPRECATED
|
|
||||||
fun getAuthToken(server: String): String?
|
|
||||||
fun setAuthToken(server: String, newValue: String?)
|
|
||||||
fun removeAuthToken(server: String)
|
|
||||||
|
|
||||||
fun getLastMessageServerID(group: Long, server: String): Long?
|
|
||||||
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
|
||||||
fun removeLastMessageServerID(group: Long, server: String)
|
|
||||||
|
|
||||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
|
||||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
|
||||||
fun removeLastDeletionServerID(group: Long, server: String)
|
|
||||||
|
|
||||||
fun getOpenGroup(threadID: String): OpenGroup?
|
|
||||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
|
||||||
|
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
|
||||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
|
||||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
|
||||||
|
|
||||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
|
||||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package org.session.libsession.messaging.file_server
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
|
||||||
internal val maxRetryCount = 4
|
|
||||||
|
|
||||||
public val maxFileSize = 10_000_000 // 10 MB
|
|
||||||
/**
|
|
||||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
|
||||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
|
||||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
|
||||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
|
||||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
|
||||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
|
||||||
*/
|
|
||||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
|
||||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Initialization
|
|
||||||
lateinit var shared: FileServerAPI
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Must be called before `LokiAPI` is used.
|
|
||||||
*/
|
|
||||||
fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) {
|
|
||||||
if (Companion::shared.isInitialized) { return }
|
|
||||||
val server = "https://file.getsession.org"
|
|
||||||
shared = FileServerAPI(server, userPublicKey, userPrivateKey, database)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Open Group Server Public Key
|
|
||||||
fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise<String, Exception> {
|
|
||||||
val publicKey = database.getOpenGroupPublicKey(openGroupServer)
|
|
||||||
if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) {
|
|
||||||
return Promise.of(publicKey)
|
|
||||||
} else {
|
|
||||||
val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}"
|
|
||||||
val request = Request.Builder().url(url)
|
|
||||||
request.addHeader("Content-Type", "application/json")
|
|
||||||
request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token
|
|
||||||
return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json ->
|
|
||||||
try {
|
|
||||||
val bodyAsString = json["data"] as String
|
|
||||||
val body = JsonUtil.fromJson(bodyAsString)
|
|
||||||
val base64EncodedPublicKey = body.get("data").asText()
|
|
||||||
val prefixedPublicKey = Base64.decode(base64EncodedPublicKey)
|
|
||||||
val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
|
||||||
val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
|
||||||
database.setOpenGroupPublicKey(openGroupServer, result)
|
|
||||||
result
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse open group public key from: $json.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,10 +15,18 @@ import org.session.libsignal.utilities.Log
|
|||||||
|
|
||||||
object FileServerAPIV2 {
|
object FileServerAPIV2 {
|
||||||
|
|
||||||
private const val OLD_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||||
const val OLD_SERVER = "http://88.99.175.227"
|
const val server = "http://filev2.getsession.org"
|
||||||
private const val SERVER_PUBLIC_KEY = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
const val maxFileSize = 10_000_000 // 10 MB
|
||||||
const val SERVER = "http://filev2.getsession.org"
|
/**
|
||||||
|
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||||
|
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||||
|
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||||
|
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||||
|
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||||
|
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||||
|
*/
|
||||||
|
const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||||
|
|
||||||
sealed class Error(message: String) : Exception(message) {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object ParsingFailed : Error("Invalid response.")
|
object ParsingFailed : Error("Invalid response.")
|
||||||
@ -44,9 +52,7 @@ object FileServerAPIV2 {
|
|||||||
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(request: Request, useOldServer: Boolean): Promise<Map<*, *>, Exception> {
|
private fun send(request: Request): Promise<Map<*, *>, Exception> {
|
||||||
val server = if (useOldServer) OLD_SERVER else SERVER
|
|
||||||
val serverPublicKey = if (useOldServer) OLD_SERVER_PUBLIC_KEY else SERVER_PUBLIC_KEY
|
|
||||||
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL)
|
||||||
val urlBuilder = HttpUrl.Builder()
|
val urlBuilder = HttpUrl.Builder()
|
||||||
.scheme(url.scheme())
|
.scheme(url.scheme())
|
||||||
@ -80,14 +86,14 @@ object FileServerAPIV2 {
|
|||||||
val base64EncodedFile = Base64.encodeBytes(file)
|
val base64EncodedFile = Base64.encodeBytes(file)
|
||||||
val parameters = mapOf( "file" to base64EncodedFile )
|
val parameters = mapOf( "file" to base64EncodedFile )
|
||||||
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
|
||||||
return send(request, false).map { json ->
|
return send(request).map { json ->
|
||||||
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(file: Long, useOldServer: Boolean): Promise<ByteArray, Exception> {
|
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||||
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
val request = Request(verb = HTTP.Verb.GET, endpoint = "files/$file")
|
||||||
return send(request, useOldServer).map { json ->
|
return send(request).map { json ->
|
||||||
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
|
||||||
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,9 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
|
||||||
import org.session.libsession.utilities.DownloadUtilities
|
import org.session.libsession.utilities.DownloadUtilities
|
||||||
import org.session.libsignal.streams.AttachmentCipherInputStream
|
import org.session.libsignal.streams.AttachmentCipherInputStream
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
@ -42,11 +40,6 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
if (exception == Error.NoAttachment) {
|
if (exception == Error.NoAttachment) {
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
||||||
this.handlePermanentFailure(exception)
|
this.handlePermanentFailure(exception)
|
||||||
} else if (exception == DotNetAPI.Error.ParsingFailed) {
|
|
||||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
|
||||||
// got a "Cannot GET ..." error from the file server.
|
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID)
|
|
||||||
this.handlePermanentFailure(exception)
|
|
||||||
} else {
|
} else {
|
||||||
this.handleFailure(exception)
|
this.handleFailure(exception)
|
||||||
}
|
}
|
||||||
@ -57,9 +50,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
val tempFile = createTempFile()
|
val tempFile = createTempFile()
|
||||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
||||||
val openGroupV2 = storage.getV2OpenGroup(threadID.toString())
|
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||||
val inputStream = if (openGroupV2 == null) {
|
val inputStream = if (openGroupV2 == null) {
|
||||||
DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null)
|
DownloadUtilities.downloadFile(tempFile, attachment.url)
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
||||||
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) {
|
||||||
FileInputStream(tempFile)
|
FileInputStream(tempFile)
|
||||||
|
@ -6,13 +6,12 @@ import com.esotericsoftware.kryo.io.Output
|
|||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.utilities.UploadResult
|
||||||
import org.session.libsignal.streams.AttachmentCipherOutputStream
|
import org.session.libsignal.streams.AttachmentCipherOutputStream
|
||||||
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
import org.session.libsignal.messages.SignalServiceAttachmentStream
|
||||||
import org.session.libsignal.streams.PaddingInputStream
|
import org.session.libsignal.streams.PaddingInputStream
|
||||||
@ -53,37 +52,28 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
val v2OpenGroup = storage.getV2OpenGroup(threadID)
|
val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong())
|
||||||
val v1OpenGroup = storage.getOpenGroup(threadID)
|
|
||||||
if (v2OpenGroup != null) {
|
if (v2OpenGroup != null) {
|
||||||
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
|
||||||
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
} else if (v1OpenGroup == null) {
|
} else {
|
||||||
val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) {
|
val keyAndResult = upload(attachment, FileServerAPIV2.server, true) {
|
||||||
FileServerAPIV2.upload(it)
|
FileServerAPIV2.upload(it)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
||||||
} else { // V1 open group
|
|
||||||
val server = v1OpenGroup.server
|
|
||||||
val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream,
|
|
||||||
attachment.length, PlaintextOutputStreamFactory(), attachment.listener)
|
|
||||||
val result = FileServerAPI.shared.uploadAttachment(server, pushData)
|
|
||||||
handleSuccess(attachment, ByteArray(0), result)
|
|
||||||
}
|
}
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
if (e == Error.NoAttachment) {
|
if (e == Error.NoAttachment) {
|
||||||
this.handlePermanentFailure(e)
|
this.handlePermanentFailure(e)
|
||||||
} else if (e is DotNetAPI.Error && !e.isRetryable) {
|
|
||||||
this.handlePermanentFailure(e)
|
|
||||||
} else {
|
} else {
|
||||||
this.handleFailure(e)
|
this.handleFailure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
|
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, UploadResult> {
|
||||||
// Key
|
// Key
|
||||||
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
|
val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0)
|
||||||
// Length
|
// Length
|
||||||
@ -112,10 +102,10 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val id = upload(data).get()
|
val id = upload(data).get()
|
||||||
val digest = drb.transmittedDigest
|
val digest = drb.transmittedDigest
|
||||||
// Return
|
// Return
|
||||||
return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest))
|
return Pair(key, UploadResult(id, "${server}/files/$id", digest))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
|
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
|
||||||
Log.d(TAG, "Attachment uploaded successfully.")
|
Log.d(TAG, "Attachment uploaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this)
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||||
|
@ -30,14 +30,8 @@ class MentionsManager(private val userPublicKey: String, private val userDatabas
|
|||||||
// Prepare
|
// Prepare
|
||||||
val cache = userPublicKeyCache[threadID] ?: return listOf()
|
val cache = userPublicKeyCache[threadID] ?: return listOf()
|
||||||
// Gather candidates
|
// Gather candidates
|
||||||
val publicChat = MessagingModuleConfiguration.shared.messageDataProvider.getOpenGroup(threadID)
|
|
||||||
var candidates: List<Mention> = cache.mapNotNull { publicKey ->
|
var candidates: List<Mention> = cache.mapNotNull { publicKey ->
|
||||||
val displayName: String?
|
val displayName = userDatabase.getDisplayName(publicKey)
|
||||||
if (publicChat != null) {
|
|
||||||
displayName = userDatabase.getServerDisplayName(publicChat.id, publicKey)
|
|
||||||
} else {
|
|
||||||
displayName = userDatabase.getDisplayName(publicKey)
|
|
||||||
}
|
|
||||||
if (displayName == null) { return@mapNotNull null }
|
if (displayName == null) { return@mapNotNull null }
|
||||||
if (displayName.startsWith("Anonymous")) { return@mapNotNull null }
|
if (displayName.startsWith("Anonymous")) { return@mapNotNull null }
|
||||||
Mention(publicKey, displayName)
|
Mention(publicKey, displayName)
|
||||||
|
@ -2,7 +2,6 @@ package org.session.libsession.messaging.messages
|
|||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
@ -15,9 +14,6 @@ sealed class Destination {
|
|||||||
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
class ClosedGroup(var groupPublicKey: String) : Destination() {
|
||||||
internal constructor(): this("")
|
internal constructor(): this("")
|
||||||
}
|
}
|
||||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
|
||||||
internal constructor(): this(0, "")
|
|
||||||
}
|
|
||||||
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
class OpenGroupV2(var room: String, var server: String) : Destination() {
|
||||||
internal constructor(): this("", "")
|
internal constructor(): this("", "")
|
||||||
}
|
}
|
||||||
@ -36,10 +32,8 @@ sealed class Destination {
|
|||||||
}
|
}
|
||||||
address.isOpenGroup -> {
|
address.isOpenGroup -> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
val threadID = storage.getThreadId(address)!!
|
||||||
when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) {
|
when (val openGroup = storage.getV2OpenGroup(threadID)) {
|
||||||
is org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
-> Destination.OpenGroup(openGroup.channel, openGroup.server)
|
|
||||||
is org.session.libsession.messaging.open_groups.OpenGroupV2
|
is org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
|
-> Destination.OpenGroupV2(openGroup.room, openGroup.server)
|
||||||
else -> throw Exception("Missing open group for thread with ID: $threadID.")
|
else -> throw Exception("Missing open group for thread with ID: $threadID.")
|
||||||
|
@ -114,10 +114,9 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups:
|
|||||||
closedGroups.add(closedGroup)
|
closedGroups.add(closedGroup)
|
||||||
}
|
}
|
||||||
if (group.isOpenGroup) {
|
if (group.isOpenGroup) {
|
||||||
val threadID = storage.getThreadID(group.encodedId) ?: continue
|
val threadID = storage.getThreadId(group.encodedId) ?: continue
|
||||||
val openGroup = storage.getOpenGroup(threadID)
|
|
||||||
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
val openGroupV2 = storage.getV2OpenGroup(threadID)
|
||||||
val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue
|
val shareUrl = openGroupV2?.joinURL ?: continue
|
||||||
openGroups.add(shareUrl)
|
openGroups.add(shareUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
|
|
||||||
data class OpenGroup(
|
|
||||||
val channel: Long,
|
|
||||||
private val serverURL: String,
|
|
||||||
val displayName: String,
|
|
||||||
val isDeletable: Boolean
|
|
||||||
) {
|
|
||||||
val server get() = serverURL.toLowerCase()
|
|
||||||
val id get() = getId(channel, server)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
|
||||||
return "$server.$channel"
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic fun fromJSON(jsonAsString: String): OpenGroup? {
|
|
||||||
try {
|
|
||||||
val json = JsonUtil.fromJson(jsonAsString)
|
|
||||||
val channel = json.get("channel").asLong()
|
|
||||||
val server = json.get("server").asText().toLowerCase()
|
|
||||||
val displayName = json.get("displayName").asText()
|
|
||||||
val isDeletable = json.get("isDeletable").asBoolean()
|
|
||||||
return OpenGroup(channel, server, displayName, isDeletable)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toJSON(): Map<String, Any> {
|
|
||||||
return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable )
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,394 +0,0 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.deferred
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import nl.komponents.kovenant.then
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
|
||||||
import org.session.libsession.utilities.DownloadUtilities
|
|
||||||
import org.session.libsignal.utilities.retryIfNeeded
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object OpenGroupAPI: DotNetAPI() {
|
|
||||||
|
|
||||||
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
|
||||||
|
|
||||||
// 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 openGroupMessageType = "network.loki.messenger.publicChat"
|
|
||||||
@JvmStatic
|
|
||||||
val profilePictureType = "network.loki.messenger.avatar"
|
|
||||||
|
|
||||||
fun getDefaultChats(): List<OpenGroup> {
|
|
||||||
return listOf() // Don't auto-join any open groups right now
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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<OpenGroupMessage>, Exception> {
|
|
||||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
|
||||||
val lastMessageServerID = storage.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 { 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 ?: "") == openGroupMessageType) && 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: OpenGroupMessage.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 = OpenGroupMessage.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: OpenGroupMessage.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) OpenGroupMessage.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 = OpenGroupMessage.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 == OpenGroupMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
OpenGroupMessage.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 = storage.getLastMessageServerID(channel, server)
|
|
||||||
if (serverID > lastMessageServerID ?: 0) { storage.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 = OpenGroupMessage.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 = OpenGroupMessage(serverID, publicKey, displayName, body, timestamp, openGroupMessageType, quote, attachments.toMutableList(), 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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 storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val parameters = mutableMapOf<String, Any>()
|
|
||||||
val lastDeletionServerID = storage.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 { 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 = storage.getLastDeletionServerID(channel, server)
|
|
||||||
if (serverID > (lastDeletionServerID ?: 0)) { storage.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
|
||||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
|
|
||||||
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
|
||||||
ThreadUtils.queue {
|
|
||||||
val signedMessage = message.sign(userKeyPair.second)
|
|
||||||
if (signedMessage == null) {
|
|
||||||
deferred.reject(Error.SigningFailed)
|
|
||||||
} 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 { 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 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
|
|
||||||
OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then { json ->
|
|
||||||
try {
|
|
||||||
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
|
|
||||||
val moderatorsAsSet = moderators.orEmpty().toSet()
|
|
||||||
if (this.moderators[server] != null) {
|
|
||||||
this.moderators[server]!![channel] = moderatorsAsSet
|
|
||||||
} else {
|
|
||||||
this.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
|
||||||
return retryIfNeeded(maxRetryCount) {
|
|
||||||
val parameters = mapOf( "include_annotations" to 1 )
|
|
||||||
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then { 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 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 = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
|
||||||
MessagingModuleConfiguration.shared.storage.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
storage.setUserCount(channel, server, info.memberCount)
|
|
||||||
storage.updateTitle(groupID, info.displayName)
|
|
||||||
// Download and update profile picture if needed
|
|
||||||
val oldProfilePictureURL = storage.getOpenGroupProfilePictureURL(channel, server)
|
|
||||||
if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) {
|
|
||||||
val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return
|
|
||||||
storage.updateProfilePicture(groupID, profilePictureAsByteArray)
|
|
||||||
storage.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
|
||||||
return getUserProfiles(publicKeys, server, false).map { 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
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
|
|
||||||
}
|
|
@ -31,8 +31,8 @@ object OpenGroupAPIV2 {
|
|||||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
|
|
||||||
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
private const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
const val DEFAULT_SERVER = "http://116.203.70.33"
|
const val defaultServer = "http://116.203.70.33"
|
||||||
|
|
||||||
sealed class Error(message: String) : Exception(message) {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object Generic : Error("An error occurred.")
|
object Generic : Error("An error occurred.")
|
||||||
@ -45,7 +45,7 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
|
||||||
|
|
||||||
val joinURL: String get() = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Info(val id: String, val name: String, val imageID: String?)
|
data class Info(val id: String, val name: String, val imageID: String?)
|
||||||
@ -60,7 +60,7 @@ object OpenGroupAPIV2 {
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = MessageDeletion()
|
val empty = MessageDeletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,9 +125,7 @@ object OpenGroupAPIV2 {
|
|||||||
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
if (request.room != null) {
|
if (request.room != null) {
|
||||||
storage.removeAuthToken("${request.server}.${request.room}")
|
storage.removeAuthToken(request.room, request.server)
|
||||||
} else {
|
|
||||||
storage.removeAuthToken(request.server)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,7 +235,7 @@ object OpenGroupAPIV2 {
|
|||||||
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val queryParameters = mutableMapOf<String, String>()
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
storage.getLastMessageServerId(room, server)?.let { lastId ->
|
storage.getLastMessageServerID(room, server)?.let { lastId ->
|
||||||
queryParameters += "from_server_id" to lastId.toString()
|
queryParameters += "from_server_id" to lastId.toString()
|
||||||
}
|
}
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||||
@ -250,7 +248,7 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
private fun parseMessages(room: String, server: String, rawMessages: List<Map<*, *>>): List<OpenGroupMessageV2> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0
|
val lastMessageServerID = storage.getLastMessageServerID(room, server) ?: 0
|
||||||
var currentLastMessageServerID = lastMessageServerID
|
var currentLastMessageServerID = lastMessageServerID
|
||||||
val messages = rawMessages.mapNotNull { json ->
|
val messages = rawMessages.mapNotNull { json ->
|
||||||
json as Map<String, Any>
|
json as Map<String, Any>
|
||||||
@ -274,7 +272,7 @@ object OpenGroupAPIV2 {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage.setLastMessageServerId(room, server, currentLastMessageServerID)
|
storage.setLastMessageServerID(room, server, currentLastMessageServerID)
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
@ -291,7 +289,7 @@ object OpenGroupAPIV2 {
|
|||||||
fun getDeletedMessages(room: String, server: String): Promise<List<MessageDeletion>, Exception> {
|
fun getDeletedMessages(room: String, server: String): Promise<List<MessageDeletion>, Exception> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val queryParameters = mutableMapOf<String, String>()
|
val queryParameters = mutableMapOf<String, String>()
|
||||||
storage.getLastDeletionServerId(room, server)?.let { last ->
|
storage.getLastDeletionServerID(room, server)?.let { last ->
|
||||||
queryParameters["from_server_id"] = last.toString()
|
queryParameters["from_server_id"] = last.toString()
|
||||||
}
|
}
|
||||||
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
||||||
@ -299,10 +297,10 @@ object OpenGroupAPIV2 {
|
|||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
val idsAsString = JsonUtil.toJson(json["ids"])
|
val idsAsString = JsonUtil.toJson(json["ids"])
|
||||||
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||||
val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0
|
val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0
|
||||||
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY
|
val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.empty
|
||||||
if (serverID.id > lastMessageServerId) {
|
if (serverID.id > lastMessageServerId) {
|
||||||
storage.setLastDeletionServerId(room, server, serverID.id)
|
storage.setLastDeletionServerID(room, server, serverID.id)
|
||||||
}
|
}
|
||||||
serverIDs
|
serverIDs
|
||||||
}
|
}
|
||||||
@ -361,8 +359,8 @@ object OpenGroupAPIV2 {
|
|||||||
CompactPollRequest(
|
CompactPollRequest(
|
||||||
roomID = room,
|
roomID = room,
|
||||||
authToken = authToken,
|
authToken = authToken,
|
||||||
fromDeletionServerID = storage.getLastDeletionServerId(room, server),
|
fromDeletionServerID = storage.getLastDeletionServerID(room, server),
|
||||||
fromMessageServerID = storage.getLastMessageServerId(room, server)
|
fromMessageServerID = storage.getLastMessageServerID(room, server)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
val request = Request(verb = POST, room = null, server = server, endpoint = "compact_poll", isAuthRequired = false, parameters = mapOf( "requests" to requests ))
|
||||||
@ -386,10 +384,10 @@ object OpenGroupAPIV2 {
|
|||||||
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
val idsAsString = JsonUtil.toJson(json["deletions"])
|
val idsAsString = JsonUtil.toJson(json["deletions"])
|
||||||
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.ParsingFailed
|
||||||
val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0
|
val lastDeletionServerID = storage.getLastDeletionServerID(roomID, server) ?: 0
|
||||||
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY
|
val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.empty
|
||||||
if (serverID.id > lastDeletionServerID) {
|
if (serverID.id > lastDeletionServerID) {
|
||||||
storage.setLastDeletionServerId(roomID, server, serverID.id)
|
storage.setLastDeletionServerID(roomID, server, serverID.id)
|
||||||
}
|
}
|
||||||
// Messages
|
// Messages
|
||||||
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
val rawMessages = json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
|
||||||
@ -405,8 +403,8 @@ object OpenGroupAPIV2 {
|
|||||||
|
|
||||||
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY)
|
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
|
||||||
return getAllRooms(DEFAULT_SERVER).map { groups ->
|
return getAllRooms(defaultServer).map { groups ->
|
||||||
val earlyGroups = groups.map { group ->
|
val earlyGroups = groups.map { group ->
|
||||||
DefaultGroup(group.id, group.name, null)
|
DefaultGroup(group.id, group.name, null)
|
||||||
}
|
}
|
||||||
@ -417,7 +415,7 @@ object OpenGroupAPIV2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val images = groups.map { group ->
|
val images = groups.map { group ->
|
||||||
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
group.id to downloadOpenGroupProfilePicture(group.id, defaultServer)
|
||||||
}.toMap()
|
}.toMap()
|
||||||
groups.map { group ->
|
groups.map { group ->
|
||||||
val image = try {
|
val image = try {
|
||||||
|
@ -1,247 +0,0 @@
|
|||||||
package org.session.libsession.messaging.open_groups
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
|
||||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.whispersystems.curve25519.Curve25519
|
|
||||||
|
|
||||||
data class OpenGroupMessage(
|
|
||||||
val serverID: Long?,
|
|
||||||
val senderPublicKey: String,
|
|
||||||
val displayName: String,
|
|
||||||
val body: String,
|
|
||||||
val timestamp: Long,
|
|
||||||
val type: String,
|
|
||||||
val quote: Quote?,
|
|
||||||
val attachments: MutableList<Attachment>,
|
|
||||||
val profilePicture: ProfilePicture?,
|
|
||||||
val signature: Signature?,
|
|
||||||
val serverTimestamp: Long,
|
|
||||||
) {
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
|
||||||
val attachmentIDs = message.attachmentIDs
|
|
||||||
// Validation
|
|
||||||
if (!message.isValid()) { return null } // Should be valid at this point
|
|
||||||
// Quote
|
|
||||||
val quote: Quote? = {
|
|
||||||
val quote = message.quote
|
|
||||||
if (quote != null && quote.isValid()) {
|
|
||||||
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
|
|
||||||
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
|
|
||||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Message
|
|
||||||
val displayname = storage.getUserDisplayName() ?: "Anonymous"
|
|
||||||
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)
|
|
||||||
// Link preview
|
|
||||||
val linkPreview = message.linkPreview
|
|
||||||
linkPreview?.let {
|
|
||||||
if (!linkPreview.isValid()) { return@let }
|
|
||||||
val attachmentID = linkPreview.attachmentID ?: return@let
|
|
||||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
|
|
||||||
val openGroupLinkPreview = Attachment(
|
|
||||||
Attachment.Kind.LinkPreview,
|
|
||||||
server,
|
|
||||||
attachment.id,
|
|
||||||
attachment.contentType!!,
|
|
||||||
attachment.size.get(),
|
|
||||||
attachment.fileName.orNull(),
|
|
||||||
0,
|
|
||||||
attachment.width,
|
|
||||||
attachment.height,
|
|
||||||
attachment.caption.orNull(),
|
|
||||||
attachment.url,
|
|
||||||
linkPreview.url,
|
|
||||||
linkPreview.title)
|
|
||||||
result.attachments.add(openGroupLinkPreview)
|
|
||||||
}
|
|
||||||
// Attachments
|
|
||||||
val attachments = message.attachmentIDs.mapNotNull {
|
|
||||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null
|
|
||||||
return@mapNotNull Attachment(
|
|
||||||
Attachment.Kind.Attachment,
|
|
||||||
server,
|
|
||||||
attachment.id,
|
|
||||||
attachment.contentType!!,
|
|
||||||
attachment.size.orNull(),
|
|
||||||
attachment.fileName.orNull() ?: "",
|
|
||||||
0,
|
|
||||||
attachment.width,
|
|
||||||
attachment.height,
|
|
||||||
attachment.caption.orNull(),
|
|
||||||
attachment.url,
|
|
||||||
null,
|
|
||||||
null)
|
|
||||||
}
|
|
||||||
result.attachments.addAll(attachments)
|
|
||||||
// Return
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
|
||||||
private val signatureVersion: Long = 1
|
|
||||||
private val attachmentType = "net.app.core.oembed"
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Types
|
|
||||||
data class ProfilePicture(
|
|
||||||
val profileKey: ByteArray,
|
|
||||||
val url: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Quote(
|
|
||||||
val quotedMessageTimestamp: Long,
|
|
||||||
val quoteePublicKey: String,
|
|
||||||
val quotedMessageBody: String,
|
|
||||||
val quotedMessageServerID: Long? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Signature(
|
|
||||||
val data: ByteArray,
|
|
||||||
val version: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Attachment(
|
|
||||||
val kind: Kind,
|
|
||||||
val server: String,
|
|
||||||
val serverID: Long,
|
|
||||||
val contentType: String,
|
|
||||||
val size: Int,
|
|
||||||
val fileName: String?,
|
|
||||||
val flags: Int,
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
val caption: String?,
|
|
||||||
val url: String,
|
|
||||||
/**
|
|
||||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
|
||||||
*/
|
|
||||||
val linkPreviewURL: String?,
|
|
||||||
/**
|
|
||||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
|
||||||
*/
|
|
||||||
val linkPreviewTitle: String?,
|
|
||||||
) {
|
|
||||||
val dotNetAPIType = when {
|
|
||||||
contentType.startsWith("image") -> "photo"
|
|
||||||
contentType.startsWith("video") -> "video"
|
|
||||||
contentType.startsWith("audio") -> "audio"
|
|
||||||
else -> "other"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Kind(val rawValue: String) {
|
|
||||||
Attachment("attachment"), LinkPreview("preview")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Initialization
|
|
||||||
constructor(hexEncodedPublicKey: String, displayName: String, body: String, timestamp: Long, type: String, quote: Quote?, attachments: List<Attachment>)
|
|
||||||
: this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments as MutableList<Attachment>, null, null, 0)
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Crypto
|
|
||||||
internal fun sign(privateKey: ByteArray): OpenGroupMessage? {
|
|
||||||
val data = getValidationData(signatureVersion)
|
|
||||||
if (data == null) {
|
|
||||||
Log.d("Loki", "Failed to sign public chat message.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val signatureData = curve.calculateSignature(privateKey, data)
|
|
||||||
val signature = Signature(signatureData, signatureVersion)
|
|
||||||
return copy(signature = signature)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Failed to sign public chat message due to error: ${e.message}.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun hasValidSignature(): Boolean {
|
|
||||||
if (signature == null) { return false }
|
|
||||||
val data = getValidationData(signature.version) ?: return false
|
|
||||||
val publicKey = Hex.fromStringCondensed(senderPublicKey.removing05PrefixIfNeeded())
|
|
||||||
try {
|
|
||||||
return curve.verifySignature(publicKey, data, signature.data)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Loki", "Failed to verify public chat message due to error: ${e.message}.")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Parsing
|
|
||||||
internal fun toJSON(): Map<String, Any> {
|
|
||||||
val value = mutableMapOf<String, Any>("timestamp" to timestamp)
|
|
||||||
if (quote != null) {
|
|
||||||
value["quote"] = mapOf("id" to quote.quotedMessageTimestamp, "author" to quote.quoteePublicKey, "text" to quote.quotedMessageBody)
|
|
||||||
}
|
|
||||||
if (signature != null) {
|
|
||||||
value["sig"] = Hex.toStringCondensed(signature.data)
|
|
||||||
value["sigver"] = signature.version
|
|
||||||
}
|
|
||||||
val annotation = mapOf("type" to type, "value" to value)
|
|
||||||
val annotations = mutableListOf(annotation)
|
|
||||||
attachments.forEach { attachment ->
|
|
||||||
val attachmentValue = mutableMapOf(
|
|
||||||
// Fields required by the .NET API
|
|
||||||
"version" to 1,
|
|
||||||
"type" to attachment.dotNetAPIType,
|
|
||||||
// Custom fields
|
|
||||||
"lokiType" to attachment.kind.rawValue,
|
|
||||||
"server" to attachment.server,
|
|
||||||
"id" to attachment.serverID,
|
|
||||||
"contentType" to attachment.contentType,
|
|
||||||
"size" to attachment.size,
|
|
||||||
"fileName" to attachment.fileName,
|
|
||||||
"flags" to attachment.flags,
|
|
||||||
"width" to attachment.width,
|
|
||||||
"height" to attachment.height,
|
|
||||||
"url" to attachment.url
|
|
||||||
)
|
|
||||||
if (attachment.caption != null) { attachmentValue["caption"] = attachment.caption }
|
|
||||||
if (attachment.linkPreviewURL != null) { attachmentValue["linkPreviewUrl"] = attachment.linkPreviewURL }
|
|
||||||
if (attachment.linkPreviewTitle != null) { attachmentValue["linkPreviewTitle"] = attachment.linkPreviewTitle }
|
|
||||||
val attachmentAnnotation = mapOf("type" to attachmentType, "value" to attachmentValue)
|
|
||||||
annotations.add(attachmentAnnotation)
|
|
||||||
}
|
|
||||||
val result = mutableMapOf("text" to body, "annotations" to annotations)
|
|
||||||
if (quote?.quotedMessageServerID != null) {
|
|
||||||
result["reply_to"] = quote.quotedMessageServerID
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private fun getValidationData(signatureVersion: Long): ByteArray? {
|
|
||||||
var string = "${body.trim()}$timestamp"
|
|
||||||
if (quote != null) {
|
|
||||||
string += "${quote.quotedMessageTimestamp}${quote.quoteePublicKey}${quote.quotedMessageBody.trim()}"
|
|
||||||
if (quote.quotedMessageServerID != null) {
|
|
||||||
string += "${quote.quotedMessageServerID}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
string += attachments.sortedBy { it.serverID }.map { it.serverID }.joinToString("")
|
|
||||||
string += "$signatureVersion"
|
|
||||||
try {
|
|
||||||
return string.toByteArray(Charsets.UTF_8)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -54,10 +54,11 @@ object MessageSender {
|
|||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||||
if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) {
|
if (destination is Destination.OpenGroupV2) {
|
||||||
return sendToOpenGroupDestination(destination, message)
|
return sendToOpenGroupDestination(destination, message)
|
||||||
|
} else {
|
||||||
|
return sendToSnodeDestination(destination, message)
|
||||||
}
|
}
|
||||||
return sendToSnodeDestination(destination, message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-on-One Chats & Closed Groups
|
// One-on-One Chats & Closed Groups
|
||||||
@ -84,7 +85,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, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -122,7 +123,7 @@ object MessageSender {
|
|||||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||||
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -136,7 +137,7 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -162,9 +163,11 @@ object MessageSender {
|
|||||||
}
|
}
|
||||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||||
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
||||||
|
/*
|
||||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||||
shouldNotify = true
|
shouldNotify = true
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
||||||
JobQueue.shared.add(notifyPNServerJob)
|
JobQueue.shared.add(notifyPNServerJob)
|
||||||
@ -203,27 +206,6 @@ object MessageSender {
|
|||||||
try {
|
try {
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
|
||||||
is Destination.OpenGroup -> {
|
|
||||||
message.recipient = "${destination.server}.${destination.channel}"
|
|
||||||
val server = destination.server
|
|
||||||
val channel = destination.channel
|
|
||||||
// Validate the message
|
|
||||||
if (message !is VisibleMessage || !message.isValid()) {
|
|
||||||
throw Error.InvalidMessage
|
|
||||||
}
|
|
||||||
// Convert the message to an open group message
|
|
||||||
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
|
|
||||||
throw Error.InvalidMessage
|
|
||||||
}
|
|
||||||
// Send the result
|
|
||||||
OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success {
|
|
||||||
message.openGroupServerMessageID = it.serverID
|
|
||||||
handleSuccessfulMessageSend(message, destination)
|
|
||||||
deferred.resolve(Unit)
|
|
||||||
}.fail {
|
|
||||||
handleFailure(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is Destination.OpenGroupV2 -> {
|
is Destination.OpenGroupV2 -> {
|
||||||
message.recipient = "${destination.server}.${destination.room}"
|
message.recipient = "${destination.server}.${destination.room}"
|
||||||
val server = destination.server
|
val server = destination.server
|
||||||
@ -275,7 +257,7 @@ object MessageSender {
|
|||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
||||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
||||||
if (threadID != null && threadID >= 0) {
|
if (threadID != null && threadID >= 0) {
|
||||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
}
|
}
|
||||||
|
@ -192,8 +192,8 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
|||||||
// Save the new group members
|
// Save the new group members
|
||||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||||
// Update the zombie list
|
// Update the zombie list
|
||||||
val oldZombies = storage.getZombieMember(groupID)
|
val oldZombies = storage.getZombieMembers(groupID)
|
||||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
storage.setZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||||
val name = group.title
|
val name = group.title
|
||||||
// Send the update to the group
|
// Send the update to the group
|
||||||
|
@ -69,21 +69,21 @@ private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
|||||||
fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) {
|
fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val address = Address.fromSerialized(senderPublicKey)
|
val address = Address.fromSerialized(senderPublicKey)
|
||||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1)
|
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) {
|
fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val address = Address.fromSerialized(senderPublicKey)
|
val address = Address.fromSerialized(senderPublicKey)
|
||||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false)
|
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val address = Address.fromSerialized(senderPublicKey)
|
val address = Address.fromSerialized(senderPublicKey)
|
||||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||||
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +123,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
|
||||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups) {
|
||||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
if (allV2OpenGroups.contains(openGroup)) continue
|
||||||
storage.addOpenGroup(openGroup, 1)
|
storage.addOpenGroup(openGroup)
|
||||||
}
|
}
|
||||||
if (message.displayName.isNotEmpty()) {
|
if (message.displayName.isNotEmpty()) {
|
||||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||||
@ -138,7 +137,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||||
}
|
}
|
||||||
storage.addContacts(message.contacts)
|
storage.addContacts(message.contacts)
|
||||||
}
|
}
|
||||||
@ -158,12 +157,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||||
throw MessageReceiver.Error.NoThread
|
throw MessageReceiver.Error.NoThread
|
||||||
}
|
}
|
||||||
val openGroup = threadID.let {
|
|
||||||
storage.getOpenGroup(it.toString())
|
|
||||||
}
|
|
||||||
// Update profile if needed
|
// Update profile if needed
|
||||||
val profile = message.profile
|
val profile = message.profile
|
||||||
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
if (profile != null && userPublicKey != message.sender) {
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||||
val displayName = profile.displayName!!
|
val displayName = profile.displayName!!
|
||||||
@ -475,8 +471,8 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
|||||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||||
}
|
}
|
||||||
// Update zombie members
|
// Update zombie members
|
||||||
val zombies = storage.getZombieMember(groupID)
|
val zombies = storage.getZombieMembers(groupID)
|
||||||
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||||
// Notify the user
|
// Notify the user
|
||||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||||
@ -528,8 +524,8 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
|||||||
} else {
|
} else {
|
||||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||||
// Update zombie members
|
// Update zombie members
|
||||||
val zombies = storage.getZombieMember(groupID)
|
val zombies = storage.getZombieMembers(groupID)
|
||||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||||
}
|
}
|
||||||
// Notify the user
|
// Notify the user
|
||||||
if (userLeft) {
|
if (userLeft) {
|
||||||
|
@ -28,7 +28,7 @@ class ClosedGroupPollerV2 {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val minPollInterval = 4 * 1000
|
private val minPollInterval = 4 * 1000
|
||||||
private val maxPollInterval = 2 * 60 * 1000
|
private val maxPollInterval = 4 * 60 * 1000
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val shared = ClosedGroupPollerV2()
|
val shared = ClosedGroupPollerV2()
|
||||||
@ -75,7 +75,7 @@ class ClosedGroupPollerV2 {
|
|||||||
// reasonable fake time interval to use instead.
|
// reasonable fake time interval to use instead.
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
val threadID = storage.getThreadID(groupID)?.toLongOrNull() ?: return
|
val threadID = storage.getThreadId(groupID) ?: return
|
||||||
val lastUpdated = storage.getLastUpdated(threadID)
|
val lastUpdated = storage.getLastUpdated(threadID)
|
||||||
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
||||||
val minPollInterval = Companion.minPollInterval
|
val minPollInterval = Companion.minPollInterval
|
||||||
|
@ -1,232 +0,0 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving.pollers
|
|
||||||
|
|
||||||
import com.google.protobuf.ByteString
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.deferred
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.GroupUtil
|
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.*
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
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) {
|
|
||||||
|
|
||||||
private var hasStarted = false
|
|
||||||
@Volatile private var isPollOngoing = false
|
|
||||||
var isCaughtUp = false
|
|
||||||
|
|
||||||
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
|
||||||
|
|
||||||
// region Convenience
|
|
||||||
private val userHexEncodedPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
|
|
||||||
private var displayNameUpdates = setOf<String>()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val pollForNewMessagesInterval: Long = 10 * 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 || executorService == null) return
|
|
||||||
cancellableFutures += listOf(
|
|
||||||
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
|
|
||||||
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
|
|
||||||
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
|
|
||||||
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
|
|
||||||
)
|
|
||||||
hasStarted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
cancellableFutures.forEach { future ->
|
|
||||||
future.cancel(false)
|
|
||||||
}
|
|
||||||
cancellableFutures.clear()
|
|
||||||
hasStarted = false
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Polling
|
|
||||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
|
||||||
return pollForNewMessagesInternal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForNewMessagesInternal(isBackgroundPoll: Boolean): Promise<Unit, Exception> {
|
|
||||||
if (isPollOngoing) { return Promise.of(Unit) }
|
|
||||||
isPollOngoing = true
|
|
||||||
val deferred = deferred<Unit, Exception>()
|
|
||||||
// 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 ->
|
|
||||||
// Process messages in the background
|
|
||||||
messages.forEach { message ->
|
|
||||||
try {
|
|
||||||
val senderPublicKey = message.senderPublicKey
|
|
||||||
fun generateDisplayName(rawDisplayName: String): String {
|
|
||||||
return "$rawDisplayName (...${senderPublicKey.takeLast(8)})"
|
|
||||||
}
|
|
||||||
val senderDisplayName = MessagingModuleConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName(message.displayName)
|
|
||||||
val id = openGroup.id.toByteArray()
|
|
||||||
// Main message
|
|
||||||
val dataMessageProto = DataMessage.newBuilder()
|
|
||||||
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
|
||||||
dataMessageProto.setBody(body)
|
|
||||||
dataMessageProto.setTimestamp(message.timestamp)
|
|
||||||
// Attachments
|
|
||||||
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
|
||||||
try {
|
|
||||||
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
|
||||||
val attachmentProto = AttachmentPointer.newBuilder()
|
|
||||||
attachmentProto.setId(attachment.serverID)
|
|
||||||
attachmentProto.setContentType(attachment.contentType)
|
|
||||||
attachmentProto.setSize(attachment.size)
|
|
||||||
attachmentProto.setFileName(attachment.fileName)
|
|
||||||
attachmentProto.setFlags(attachment.flags)
|
|
||||||
attachmentProto.setWidth(attachment.width)
|
|
||||||
attachmentProto.setHeight(attachment.height)
|
|
||||||
attachment.caption?.let { attachmentProto.setCaption(it) }
|
|
||||||
attachmentProto.setUrl(attachment.url)
|
|
||||||
attachmentProto.build()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki","Failed to parse attachment as proto",e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataMessageProto.addAllAttachments(attachmentProtos)
|
|
||||||
// Link preview
|
|
||||||
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
|
|
||||||
if (linkPreview != null) {
|
|
||||||
val linkPreviewProto = DataMessage.Preview.newBuilder()
|
|
||||||
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
|
|
||||||
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
|
|
||||||
val attachmentProto = AttachmentPointer.newBuilder()
|
|
||||||
attachmentProto.setId(linkPreview.serverID)
|
|
||||||
attachmentProto.setContentType(linkPreview.contentType)
|
|
||||||
attachmentProto.setSize(linkPreview.size)
|
|
||||||
attachmentProto.setFileName(linkPreview.fileName)
|
|
||||||
attachmentProto.setFlags(linkPreview.flags)
|
|
||||||
attachmentProto.setWidth(linkPreview.width)
|
|
||||||
attachmentProto.setHeight(linkPreview.height)
|
|
||||||
linkPreview.caption?.let { attachmentProto.setCaption(it) }
|
|
||||||
attachmentProto.setUrl(linkPreview.url)
|
|
||||||
linkPreviewProto.setImage(attachmentProto.build())
|
|
||||||
dataMessageProto.addPreview(linkPreviewProto.build())
|
|
||||||
}
|
|
||||||
// Quote
|
|
||||||
val quote = message.quote
|
|
||||||
if (quote != null) {
|
|
||||||
val quoteProto = DataMessage.Quote.newBuilder()
|
|
||||||
quoteProto.setId(quote.quotedMessageTimestamp)
|
|
||||||
quoteProto.setAuthor(quote.quoteePublicKey)
|
|
||||||
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
|
|
||||||
dataMessageProto.setQuote(quoteProto.build())
|
|
||||||
}
|
|
||||||
val messageServerID = message.serverID
|
|
||||||
// Profile
|
|
||||||
val profileProto = DataMessage.LokiProfile.newBuilder()
|
|
||||||
profileProto.setDisplayName(senderDisplayName)
|
|
||||||
val profilePicture = message.profilePicture
|
|
||||||
if (profilePicture != null) {
|
|
||||||
profileProto.setProfilePicture(profilePicture.url)
|
|
||||||
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
|
|
||||||
}
|
|
||||||
dataMessageProto.setProfile(profileProto.build())
|
|
||||||
/* TODO: the signal service proto needs to be synced with iOS
|
|
||||||
// Open group info
|
|
||||||
if (messageServerID != null) {
|
|
||||||
val openGroupProto = PublicChatInfo.newBuilder()
|
|
||||||
openGroupProto.setServerID(messageServerID)
|
|
||||||
dataMessageProto.setPublicChatInfo(openGroupProto.build())
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// Signal group context
|
|
||||||
val groupProto = GroupContext.newBuilder()
|
|
||||||
groupProto.setId(ByteString.copyFrom(id))
|
|
||||||
groupProto.setType(GroupContext.Type.DELIVER)
|
|
||||||
groupProto.setName(openGroup.displayName)
|
|
||||||
dataMessageProto.setGroup(groupProto.build())
|
|
||||||
// Content
|
|
||||||
val content = Content.newBuilder()
|
|
||||||
content.setDataMessage(dataMessageProto.build())
|
|
||||||
// Envelope
|
|
||||||
val builder = Envelope.newBuilder()
|
|
||||||
builder.type = Envelope.Type.SESSION_MESSAGE
|
|
||||||
builder.source = senderPublicKey
|
|
||||||
builder.sourceDevice = 1
|
|
||||||
builder.setContent(content.build().toByteString())
|
|
||||||
builder.timestamp = message.timestamp
|
|
||||||
builder.serverTimestamp = message.serverTimestamp
|
|
||||||
val envelope = builder.build()
|
|
||||||
val job = MessageReceiveJob(envelope.toByteArray(), messageServerID, openGroup.id)
|
|
||||||
Log.d("Loki", "Scheduling Job $job")
|
|
||||||
if (isBackgroundPoll) {
|
|
||||||
job.executeAsync().always { 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
|
|
||||||
isPollOngoing = false
|
|
||||||
deferred.resolve(Unit)
|
|
||||||
}.fail {
|
|
||||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
|
||||||
isPollOngoing = false
|
|
||||||
}
|
|
||||||
return deferred.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDisplayNames() {
|
|
||||||
if (displayNameUpdates.isEmpty()) { return }
|
|
||||||
val hexEncodedPublicKeys = displayNameUpdates
|
|
||||||
displayNameUpdates = setOf()
|
|
||||||
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
|
||||||
for (pair in mapping.entries) {
|
|
||||||
if (pair.key == userHexEncodedPublicKey) continue
|
|
||||||
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
|
|
||||||
MessagingModuleConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForDeletedMessages() {
|
|
||||||
val messagingModule = MessagingModuleConfiguration.shared
|
|
||||||
val address = GroupUtil.getEncodedOpenGroupID(openGroup.id.toByteArray())
|
|
||||||
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
|
||||||
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { messagingModule.messageDataProvider.getMessageID(it, threadId) }
|
|
||||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
|
||||||
}
|
|
||||||
}.fail {
|
|
||||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollForModerators() {
|
|
||||||
OpenGroupAPI.getModerators(openGroup.channel, openGroup.server)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -78,7 +78,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService:
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||||
val threadID = storage.getThreadIdFor(Address.fromSerialized(groupID)) ?: return
|
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID ->
|
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID ->
|
||||||
val messageID = dataProvider.getMessageID(serverID, threadID)
|
val messageID = dataProvider.getMessageID(serverID, threadID)
|
||||||
if (messageID == null) {
|
if (messageID == null) {
|
||||||
|
@ -1,269 +0,0 @@
|
|||||||
package org.session.libsession.messaging.utilities
|
|
||||||
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import nl.komponents.kovenant.then
|
|
||||||
import okhttp3.*
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
|
||||||
|
|
||||||
import org.session.libsignal.crypto.DiffieHellman
|
|
||||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
|
||||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
|
||||||
import org.session.libsignal.exceptions.PushNetworkException
|
|
||||||
import org.session.libsignal.streams.StreamDetails
|
|
||||||
import org.session.libsignal.utilities.ProfileAvatarData
|
|
||||||
import org.session.libsignal.utilities.PushAttachmentData
|
|
||||||
import org.session.libsignal.streams.DigestingRequestBody
|
|
||||||
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
|
||||||
import org.session.libsignal.utilities.Hex
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
|
||||||
import org.session.libsignal.utilities.HTTP
|
|
||||||
import org.session.libsignal.utilities.*
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class that provides utilities for .NET based APIs.
|
|
||||||
*/
|
|
||||||
open class DotNetAPI {
|
|
||||||
|
|
||||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
|
||||||
|
|
||||||
// Error
|
|
||||||
internal sealed class Error(val description: String) : Exception(description) {
|
|
||||||
object Generic : Error("An error occurred.")
|
|
||||||
object InvalidURL : Error("Invalid URL.")
|
|
||||||
object ParsingFailed : Error("Invalid file server response.")
|
|
||||||
object SigningFailed : Error("Couldn't sign message.")
|
|
||||||
object EncryptionFailed : Error("Couldn't encrypt file.")
|
|
||||||
object DecryptionFailed : Error("Couldn't decrypt file.")
|
|
||||||
object MaxFileSizeExceeded : Error("Maximum file size exceeded.")
|
|
||||||
object TokenExpired: Error("Token expired.") // Session Android
|
|
||||||
|
|
||||||
internal val isRetryable: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val authTokenRequestCache = hashMapOf<String, Promise<String, Exception>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
|
||||||
|
|
||||||
fun getAuthToken(server: String): Promise<String, Exception> {
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val token = storage.getAuthToken(server)
|
|
||||||
if (token != null) { return Promise.of(token) }
|
|
||||||
// Avoid multiple token requests to the server by caching
|
|
||||||
var promise = authTokenRequestCache[server]
|
|
||||||
if (promise == null) {
|
|
||||||
promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken ->
|
|
||||||
storage.setAuthToken(server, newToken)
|
|
||||||
newToken
|
|
||||||
}.always {
|
|
||||||
authTokenRequestCache.remove(server)
|
|
||||||
}
|
|
||||||
authTokenRequestCache[server] = promise
|
|
||||||
}
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
|
||||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
|
||||||
val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic
|
|
||||||
val parameters: Map<String, Any> = mapOf( "pubKey" to userKeyPair.first )
|
|
||||||
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map { json ->
|
|
||||||
try {
|
|
||||||
val base64EncodedChallenge = json["cipherText64"] as String
|
|
||||||
val challenge = Base64.decode(base64EncodedChallenge)
|
|
||||||
val base64EncodedServerPublicKey = json["serverPubKey64"] as String
|
|
||||||
var serverPublicKey = Base64.decode(base64EncodedServerPublicKey)
|
|
||||||
// Discard the "05" prefix if needed
|
|
||||||
if (serverPublicKey.count() == 33) {
|
|
||||||
val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey)
|
|
||||||
serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded())
|
|
||||||
}
|
|
||||||
// The challenge is prefixed by the 16 bit IV
|
|
||||||
val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userKeyPair.second)
|
|
||||||
val token = tokenAsData.toString(Charsets.UTF_8)
|
|
||||||
token
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d("Loki", "Couldn't parse auth token for server: $server.")
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
|
||||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
|
||||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: throw Error.Generic
|
|
||||||
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
|
||||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map<String, Any> = mapOf(), isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
|
||||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
|
||||||
val sanitizedEndpoint = endpoint.removePrefix("/")
|
|
||||||
var url = "$server/$sanitizedEndpoint"
|
|
||||||
if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) {
|
|
||||||
val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&")
|
|
||||||
if (queryParameters.isNotEmpty()) { url += "?$queryParameters" }
|
|
||||||
}
|
|
||||||
var request = Request.Builder().url(url)
|
|
||||||
if (isAuthRequired) {
|
|
||||||
if (token == null) { throw IllegalStateException() }
|
|
||||||
request = request.header("Authorization", "Bearer $token")
|
|
||||||
}
|
|
||||||
when (verb) {
|
|
||||||
HTTPVerb.GET -> request = request.get()
|
|
||||||
HTTPVerb.DELETE -> request = request.delete()
|
|
||||||
else -> {
|
|
||||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
|
||||||
val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
|
||||||
when (verb) {
|
|
||||||
HTTPVerb.PUT -> request = request.put(body)
|
|
||||||
HTTPVerb.POST -> request = request.post(body)
|
|
||||||
HTTPVerb.PATCH -> request = request.patch(body)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey)
|
|
||||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server)
|
|
||||||
return serverPublicKeyPromise.bind { serverPublicKey ->
|
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception ->
|
|
||||||
if (exception is HTTP.HTTPRequestFailedException) {
|
|
||||||
val statusCode = exception.statusCode
|
|
||||||
if (statusCode == 401 || statusCode == 403) {
|
|
||||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
|
||||||
throw Error.TokenExpired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (isAuthRequired) {
|
|
||||||
getAuthToken(server).bind { execute(it) }
|
|
||||||
} else {
|
|
||||||
execute(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun getUserProfiles(publicKeys: Set<String>, server: String, includeAnnotations: Boolean): Promise<List<Map<*, *>>, Exception> {
|
|
||||||
val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } )
|
|
||||||
return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json ->
|
|
||||||
val data = json["data"] as? List<Map<*, *>>
|
|
||||||
if (data == null) {
|
|
||||||
Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.")
|
|
||||||
throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
data!! // For some reason the compiler can't infer that this can't be null at this point
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise<Map<*, *>, Exception> {
|
|
||||||
val annotation = mutableMapOf<String, Any>( "type" to type )
|
|
||||||
if (newValue != null) { annotation["value"] = newValue }
|
|
||||||
val parameters = mapOf( "annotations" to listOf( annotation ) )
|
|
||||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPLOAD
|
|
||||||
|
|
||||||
// TODO: migrate to v2 file server
|
|
||||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
|
||||||
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
|
||||||
// This function mimics what Signal does in PushServiceSocket
|
|
||||||
val contentType = "application/octet-stream"
|
|
||||||
val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener)
|
|
||||||
Log.d("Loki", "File size: ${attachment.dataSize} bytes.")
|
|
||||||
val body = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart("type", "network.loki")
|
|
||||||
.addFormDataPart("Content-Type", contentType)
|
|
||||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
|
||||||
.build()
|
|
||||||
val request = Request.Builder().url("$server/files").post(body)
|
|
||||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
|
||||||
val data = json["data"] as? Map<*, *>
|
|
||||||
if (data == null) {
|
|
||||||
Log.e("Loki", "Couldn't parse attachment from: $json.")
|
|
||||||
throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
|
||||||
val url = data["url"] as? String
|
|
||||||
if (id == null || url == null || url.isEmpty()) {
|
|
||||||
Log.e("Loki", "Couldn't parse upload from: $json.")
|
|
||||||
throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
UploadResult(id, url, file.transmittedDigest)
|
|
||||||
}.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
|
||||||
fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult {
|
|
||||||
val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key))
|
|
||||||
val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory,
|
|
||||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
|
||||||
val body = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart("type", "network.loki")
|
|
||||||
.addFormDataPart("Content-Type", "application/octet-stream")
|
|
||||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
|
||||||
.build()
|
|
||||||
val request = Request.Builder().url("$server/files").post(body)
|
|
||||||
return retryIfNeeded(4) {
|
|
||||||
upload(server, request) { json ->
|
|
||||||
val data = json["data"] as? Map<*, *>
|
|
||||||
if (data == null) {
|
|
||||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
|
||||||
throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
|
||||||
val url = data["url"] as? String
|
|
||||||
if (id == null || url == null || url.isEmpty()) {
|
|
||||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
|
||||||
throw Error.ParsingFailed
|
|
||||||
}
|
|
||||||
setLastProfilePictureUpload()
|
|
||||||
UploadResult(id, url, file.transmittedDigest)
|
|
||||||
}
|
|
||||||
}.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
|
||||||
private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise<UploadResult, Exception> {
|
|
||||||
val promise: Promise<Map<*, *>, Exception>
|
|
||||||
if (server == FileServerAPI.shared.server) {
|
|
||||||
request.addHeader("Authorization", "Bearer loki")
|
|
||||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token
|
|
||||||
promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey)
|
|
||||||
} else {
|
|
||||||
promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey ->
|
|
||||||
getAuthToken(server).bind { token ->
|
|
||||||
request.addHeader("Authorization", "Bearer $token")
|
|
||||||
OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return promise.map { json ->
|
|
||||||
parse(json)
|
|
||||||
}.recover { exception ->
|
|
||||||
if (exception is HTTP.HTTPRequestFailedException) {
|
|
||||||
val statusCode = exception.statusCode
|
|
||||||
if (statusCode == 401 || statusCode == 403) {
|
|
||||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
|
||||||
}
|
|
||||||
throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.")
|
|
||||||
}
|
|
||||||
throw PushNetworkException(exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Boolean.toInt(): Int { return if (this) 1 else 0 }
|
|
@ -6,7 +6,7 @@ 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 okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||||
import org.session.libsession.utilities.AESGCM
|
import org.session.libsession.utilities.AESGCM
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
@ -307,7 +307,7 @@ object OnionRequestAPI {
|
|||||||
val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2"
|
val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2"
|
||||||
val finalEncryptionResult = result.finalEncryptionResult
|
val finalEncryptionResult = result.finalEncryptionResult
|
||||||
val onion = finalEncryptionResult.ciphertext
|
val onion = finalEncryptionResult.ciphertext
|
||||||
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPI.maxFileSize.toDouble()) {
|
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPIV2.maxFileSize.toDouble()) {
|
||||||
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
|
||||||
}
|
}
|
||||||
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
@Suppress("NAME_SHADOWING") val parameters = mapOf(
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Request
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
import org.session.libsession.messaging.file_server.FileServerAPIV2
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.messages.SignalServiceAttachment
|
import org.session.libsignal.messages.SignalServiceAttachment
|
||||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
|
||||||
import org.session.libsignal.exceptions.PushNetworkException
|
|
||||||
import org.session.libsignal.utilities.Base64
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
object DownloadUtilities {
|
object DownloadUtilities {
|
||||||
@ -18,14 +12,14 @@ object DownloadUtilities {
|
|||||||
* Blocks the calling thread.
|
* Blocks the calling thread.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
fun downloadFile(destination: File, url: String) {
|
||||||
val outputStream = FileOutputStream(destination) // Throws
|
val outputStream = FileOutputStream(destination) // Throws
|
||||||
var remainingAttempts = 4
|
var remainingAttempts = 4
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
while (remainingAttempts > 0) {
|
while (remainingAttempts > 0) {
|
||||||
remainingAttempts -= 1
|
remainingAttempts -= 1
|
||||||
try {
|
try {
|
||||||
downloadFile(outputStream, url, maxSize, listener)
|
downloadFile(outputStream, url)
|
||||||
exception = null
|
exception = null
|
||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -39,66 +33,16 @@ object DownloadUtilities {
|
|||||||
* Blocks the calling thread.
|
* Blocks the calling thread.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) {
|
fun downloadFile(outputStream: OutputStream, urlAsString: String) {
|
||||||
|
val url = HttpUrl.parse(urlAsString)!!
|
||||||
if (url.contains(FileServerAPIV2.SERVER) || url.contains(FileServerAPIV2.OLD_SERVER)) {
|
val fileID = url.pathSegments().last()
|
||||||
val httpUrl = HttpUrl.parse(url)!!
|
try {
|
||||||
val fileId = httpUrl.pathSegments().last()
|
FileServerAPIV2.download(fileID.toLong()).get().let {
|
||||||
val useOldServer = url.contains(FileServerAPIV2.OLD_SERVER)
|
outputStream.write(it)
|
||||||
try {
|
|
||||||
FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let {
|
|
||||||
outputStream.write(it)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki", "Couln't download attachment due to error",e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We need to throw a PushNetworkException or NonSuccessfulResponseCodeException
|
|
||||||
// because the underlying Signal logic requires these to work correctly
|
|
||||||
val oldPrefixedHost = "https://" + HttpUrl.get(url).host()
|
|
||||||
var newPrefixedHost = oldPrefixedHost
|
|
||||||
if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) {
|
|
||||||
newPrefixedHost = FileServerAPI.shared.server
|
|
||||||
}
|
|
||||||
// Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM
|
|
||||||
// → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35
|
|
||||||
val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/")
|
|
||||||
val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID"
|
|
||||||
val request = Request.Builder().url(sanitizedURL).get()
|
|
||||||
try {
|
|
||||||
val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey
|
|
||||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get()
|
|
||||||
val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get()
|
|
||||||
val result = json["result"] as? String
|
|
||||||
if (result == null) {
|
|
||||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
|
||||||
throw PushNetworkException("Missing response body.")
|
|
||||||
}
|
|
||||||
val body = Base64.decode(result)
|
|
||||||
if (body.size > maxSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
body.inputStream().use { input ->
|
|
||||||
val buffer = ByteArray(32768)
|
|
||||||
var count = 0
|
|
||||||
var bytes = input.read(buffer)
|
|
||||||
while (bytes >= 0) {
|
|
||||||
outputStream.write(buffer, 0, bytes)
|
|
||||||
count += bytes
|
|
||||||
if (count > maxSize) {
|
|
||||||
Log.d("Loki", "Attachment size limit exceeded.")
|
|
||||||
throw PushNetworkException("Max response size exceeded.")
|
|
||||||
}
|
|
||||||
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
|
|
||||||
bytes = input.read(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Loki", "Couldn't download attachment due to error", e)
|
|
||||||
throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't download attachment.", e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -36,7 +36,7 @@ object ProfilePictureUtilities {
|
|||||||
deferred.reject(e)
|
deferred.reject(e)
|
||||||
}
|
}
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
||||||
val url = "${FileServerAPIV2.SERVER}/files/$id"
|
val url = "${FileServerAPIV2.server}/files/$id"
|
||||||
TextSecurePreferences.setProfilePictureURL(context, url)
|
TextSecurePreferences.setProfilePictureURL(context, url)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ class SSKEnvironment(
|
|||||||
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
|
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
|
||||||
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
|
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
|
||||||
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
|
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
|
||||||
fun updateOpenGroupProfilePicturesIfNeeded(context: Context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageExpirationManagerProtocol {
|
interface MessageExpirationManagerProtocol {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
|
data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
@ -3,6 +3,5 @@ package org.session.libsignal.database
|
|||||||
interface LokiUserDatabaseProtocol {
|
interface LokiUserDatabaseProtocol {
|
||||||
|
|
||||||
fun getDisplayName(publicKey: String): String?
|
fun getDisplayName(publicKey: String): String?
|
||||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
|
||||||
fun getProfilePictureURL(publicKey: String): String?
|
fun getProfilePictureURL(publicKey: String): String?
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user