mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge branch 'dev' into nicknames
This commit is contained in:
commit
e0c1456af4
@ -143,8 +143,8 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 166
|
def canonicalVersionCode = 170
|
||||||
def canonicalVersionName = "1.10.6"
|
def canonicalVersionName = "1.10.7"
|
||||||
|
|
||||||
def postFixSize = 10
|
def postFixSize = 10
|
||||||
def abiPostFix = ['armeabi-v7a' : 1,
|
def abiPostFix = ['armeabi-v7a' : 1,
|
||||||
|
@ -22,21 +22,17 @@ 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.ClosedGroupPoller;
|
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;
|
||||||
import org.session.libsession.snode.SnodeModule;
|
import org.session.libsession.snode.SnodeModule;
|
||||||
import org.session.libsession.utilities.Address;
|
import org.session.libsession.utilities.Address;
|
||||||
@ -131,7 +127,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
// Loki
|
// Loki
|
||||||
public MessageNotifier messageNotifier = null;
|
public MessageNotifier messageNotifier = null;
|
||||||
public Poller poller = null;
|
public Poller poller = null;
|
||||||
public ClosedGroupPoller closedGroupPoller = null;
|
|
||||||
public Broadcaster broadcaster = null;
|
public Broadcaster broadcaster = null;
|
||||||
public SignalCommunicationModule communicationModule;
|
public SignalCommunicationModule communicationModule;
|
||||||
private Job firebaseInstanceIdJob;
|
private Job firebaseInstanceIdJob;
|
||||||
@ -175,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);
|
||||||
}
|
}
|
||||||
@ -206,10 +199,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
poller.setCaughtUp(false);
|
poller.setCaughtUp(false);
|
||||||
}
|
}
|
||||||
startPollingIfNeeded();
|
startPollingIfNeeded();
|
||||||
// FIXME: Open group handling
|
|
||||||
/*
|
OpenGroupManager.INSTANCE.setAllCaughtUp(false);
|
||||||
publicChatManager.markAllAsNotCaughtUp();
|
|
||||||
*/
|
|
||||||
OpenGroupManager.INSTANCE.startPolling();
|
OpenGroupManager.INSTANCE.startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,9 +214,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.stopIfNeeded();
|
poller.stopIfNeeded();
|
||||||
}
|
}
|
||||||
if (closedGroupPoller != null) {
|
ClosedGroupPollerV2.getShared().stop();
|
||||||
closedGroupPoller.stopIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -406,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;
|
||||||
@ -451,7 +427,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
poller = new Poller();
|
poller = new Poller();
|
||||||
closedGroupPoller = new ClosedGroupPoller();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startPollingIfNeeded() {
|
public void startPollingIfNeeded() {
|
||||||
@ -459,9 +434,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
poller.startIfNeeded();
|
poller.startIfNeeded();
|
||||||
}
|
}
|
||||||
if (closedGroupPoller != null) {
|
ClosedGroupPollerV2.getShared().start();
|
||||||
closedGroupPoller.startIfNeeded();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resubmitProfilePictureIfNeeded() {
|
private void resubmitProfilePictureIfNeeded() {
|
||||||
@ -498,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;
|
||||||
@ -194,7 +191,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;
|
||||||
@ -205,7 +201,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;
|
||||||
|
|
||||||
@ -378,12 +373,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")) {
|
||||||
@ -1414,13 +1405,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())) {
|
||||||
@ -2376,13 +2361,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.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
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 getOrGenerateRegistrationID(): Int {
|
override fun getOrGenerateRegistrationID(): Int {
|
||||||
@ -91,15 +89,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? {
|
||||||
@ -169,7 +167,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)
|
||||||
}
|
}
|
||||||
@ -203,20 +200,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)
|
||||||
@ -232,30 +215,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)
|
||||||
}
|
}
|
||||||
@ -264,59 +232,27 @@ 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 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 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,34 +260,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)
|
||||||
}
|
}
|
||||||
@ -378,15 +295,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
|
||||||
@ -445,7 +353,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,7 +365,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -523,39 +431,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)
|
||||||
@ -580,8 +467,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getThreadIdFor(address: Address): Long? {
|
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
|
||||||
|
val address = Address.fromSerialized(publicKeyOrOpenGroupID)
|
||||||
|
return getThreadId(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getThreadId(address: Address): Long? {
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
|
return getThreadId(recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getThreadId(recipient: Recipient): Long? {
|
||||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
||||||
return if (threadID < 0) null else threadID
|
return if (threadID < 0) null else threadID
|
||||||
}
|
}
|
||||||
@ -595,22 +491,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? {
|
||||||
val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey)
|
val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey)
|
||||||
contact?.let {
|
contact?.let {
|
||||||
@ -662,10 +542,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
threadDatabase.getOrCreateThreadIdFor(recipient)
|
threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||||
}
|
}
|
||||||
if (contacts.isNotEmpty()) {
|
if (contacts.isNotEmpty()) {
|
||||||
threadDatabase.notifyUpdatedFromConfig()
|
threadDatabase.notifyConversationListListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastUpdated(threadID: Long): Long {
|
||||||
|
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
|
return threadDB.getLastUpdated(threadID)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
|
||||||
return PartAuthority.getAttachmentDataUri(attachmentId)
|
return PartAuthority.getAttachmentDataUri(attachmentId)
|
||||||
}
|
}
|
||||||
@ -674,7 +559,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)
|
||||||
|
@ -64,7 +64,7 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
||||||
|
|
||||||
private Map<Long, Address> addressCache = new HashMap<>();
|
private final Map<Long, Address> addressCache = new HashMap<>();
|
||||||
|
|
||||||
public static final String TABLE_NAME = "thread";
|
public static final String TABLE_NAME = "thread";
|
||||||
public static final String ID = "_id";
|
public static final String ID = "_id";
|
||||||
@ -404,6 +404,21 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getLastUpdated(long threadId) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
Cursor cursor = db.query(TABLE_NAME, new String[]{DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
return cursor.getLong(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1L;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteConversation(long threadId) {
|
public void deleteConversation(long threadId) {
|
||||||
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
|
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
|
||||||
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
|
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
|
||||||
@ -471,7 +486,6 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable Recipient getRecipientForThreadId(long threadId) {
|
public @Nullable Recipient getRecipientForThreadId(long threadId) {
|
||||||
// Loki - Cache the address
|
|
||||||
if (addressCache.containsKey(threadId) && addressCache.get(threadId) != null) {
|
if (addressCache.containsKey(threadId) && addressCache.get(threadId) != null) {
|
||||||
return Recipient.from(context, addressCache.get(threadId), false);
|
return Recipient.from(context, addressCache.get(threadId), false);
|
||||||
}
|
}
|
||||||
@ -505,17 +519,13 @@ public class ThreadDatabase extends Database {
|
|||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyUpdatedFromConfig() {
|
|
||||||
notifyConversationListListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean update(long threadId, boolean unarchive) {
|
public boolean update(long threadId, boolean unarchive) {
|
||||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||||
long count = mmsSmsDatabase.getConversationCount(threadId);
|
long count = mmsSmsDatabase.getConversationCount(threadId);
|
||||||
|
|
||||||
|
boolean shouldDeleteEmptyThread = deleteThreadOnEmpty(threadId);
|
||||||
|
|
||||||
|
if (count == 0 && shouldDeleteEmptyThread) {
|
||||||
if (count == 0) {
|
|
||||||
deleteThread(threadId);
|
deleteThread(threadId);
|
||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
return true;
|
return true;
|
||||||
@ -534,9 +544,12 @@ public class ThreadDatabase extends Database {
|
|||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
deleteThread(threadId);
|
if (shouldDeleteEmptyThread) {
|
||||||
notifyConversationListListeners();
|
deleteThread(threadId);
|
||||||
return true;
|
notifyConversationListListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null)
|
if (reader != null)
|
||||||
@ -544,6 +557,11 @@ public class ThreadDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean deleteThreadOnEmpty(long threadId) {
|
||||||
|
Recipient threadRecipient = getRecipientForThreadId(threadId);
|
||||||
|
return threadRecipient != null && !threadRecipient.isOpenGroupRecipient();
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
||||||
|
@ -58,9 +58,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int lokiV23 = 44;
|
private static final int lokiV23 = 44;
|
||||||
private static final int lokiV24 = 45;
|
private static final int lokiV24 = 45;
|
||||||
private static final int lokiV25 = 46;
|
private static final int lokiV25 = 46;
|
||||||
|
private static final int lokiV26 = 47;
|
||||||
|
|
||||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||||
private static final int DATABASE_VERSION = lokiV25;
|
private static final int DATABASE_VERSION = lokiV26;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -295,6 +296,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oldVersion < lokiV25) {
|
if (oldVersion < lokiV25) {
|
||||||
|
String jobTable = SessionJobDatabase.sessionJobTable;
|
||||||
|
db.execSQL("DROP TABLE " + jobTable);
|
||||||
|
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV26) {
|
||||||
db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand());
|
db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,10 +70,6 @@ public class GroupManager {
|
|||||||
final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
|
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
|
||||||
|
|
||||||
if (!groupDatabase.getGroup(groupId).isPresent()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient);
|
long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient);
|
||||||
if (threadId != -1L) {
|
if (threadId != -1L) {
|
||||||
threadDatabase.deleteConversation(threadId);
|
threadDatabase.deleteConversation(threadId);
|
||||||
|
@ -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.utilities.*
|
import org.session.libsession.utilities.*
|
||||||
import org.session.libsignal.utilities.toHexString
|
import org.session.libsignal.utilities.toHexString
|
||||||
@ -332,16 +331,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)
|
||||||
|
@ -7,9 +7,9 @@ import androidx.work.*
|
|||||||
import nl.komponents.kovenant.Promise
|
import nl.komponents.kovenant.Promise
|
||||||
import nl.komponents.kovenant.all
|
import nl.komponents.kovenant.all
|
||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
|
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
@ -57,7 +57,10 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
promises.addAll(dmsPromise.get())
|
promises.addAll(dmsPromise.get())
|
||||||
|
|
||||||
// Closed groups
|
// Closed groups
|
||||||
promises.addAll(ClosedGroupPoller().pollOnce())
|
val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
|
allGroupPublicKeys.forEach { closedGroupPoller.poll(it) }
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
|
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
|
||||||
|
@ -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
|
||||||
@ -19,6 +20,8 @@ object OpenGroupManager {
|
|||||||
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
|
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server
|
||||||
private var isPolling = false
|
private var isPolling = false
|
||||||
|
|
||||||
|
var isAllCaughtUp = false
|
||||||
|
|
||||||
fun startPolling() {
|
fun startPolling() {
|
||||||
if (isPolling) { return }
|
if (isPolling) { return }
|
||||||
isPolling = true
|
isPolling = true
|
||||||
@ -47,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
|
||||||
@ -93,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)
|
||||||
|
@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.loki.utilities.*
|
|||||||
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val sessionJobTable = "session_job_database"
|
const val sessionJobTable = "session_job_database"
|
||||||
const val jobID = "job_id"
|
const val jobID = "job_id"
|
||||||
const val jobType = "job_type"
|
const val jobType = "job_type"
|
||||||
const val failureCount = "failure_count"
|
const val failureCount = "failure_count"
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -58,11 +58,7 @@ class UserView : LinearLayout {
|
|||||||
val contactContext = Contact.contextForRecipient(user)
|
val contactContext = Contact.contextForRecipient(user)
|
||||||
return it.displayName(contactContext)
|
return it.displayName(contactContext)
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.loki.api.OpenGroupManager;
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
@ -286,6 +287,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||||||
} finally {
|
} finally {
|
||||||
if (telcoCursor != null) telcoCursor.close();
|
if (telcoCursor != null) telcoCursor.close();
|
||||||
if (pushCursor != null) pushCursor.close();
|
if (pushCursor != null) pushCursor.close();
|
||||||
|
if (!OpenGroupManager.INSTANCE.isAllCaughtUp()) {
|
||||||
|
OpenGroupManager.INSTANCE.setAllCaughtUp(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import org.session.libsession.utilities.recipients.Recipient;
|
|||||||
import org.session.libsession.utilities.Debouncer;
|
import org.session.libsession.utilities.Debouncer;
|
||||||
import org.session.libsignal.utilities.ThreadUtils;
|
import org.session.libsignal.utilities.ThreadUtils;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
import org.thoughtcrime.securesms.loki.api.OpenGroupManager;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -42,18 +43,12 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
|||||||
@Override
|
@Override
|
||||||
public void updateNotification(@NonNull Context context) {
|
public void updateNotification(@NonNull Context context) {
|
||||||
Poller poller = ApplicationContext.getInstance(context).poller;
|
Poller poller = ApplicationContext.getInstance(context).poller;
|
||||||
// FIXME: Open group handling
|
|
||||||
boolean isCaughtUp = true;
|
boolean isCaughtUp = true;
|
||||||
if (poller != null) {
|
if (poller != null) {
|
||||||
isCaughtUp = isCaughtUp && poller.isCaughtUp();
|
isCaughtUp = isCaughtUp && poller.isCaughtUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Open group handling
|
isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp();
|
||||||
/*
|
|
||||||
if (publicChatManager != null) {
|
|
||||||
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isCaughtUp) {
|
if (isCaughtUp) {
|
||||||
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context));
|
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context));
|
||||||
@ -65,18 +60,12 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
|||||||
@Override
|
@Override
|
||||||
public void updateNotification(@NonNull Context context, long threadId) {
|
public void updateNotification(@NonNull Context context, long threadId) {
|
||||||
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
||||||
// FIXME: Open group handling
|
|
||||||
boolean isCaughtUp = true;
|
boolean isCaughtUp = true;
|
||||||
if (lokiPoller != null) {
|
if (lokiPoller != null) {
|
||||||
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Open group handling
|
isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp();
|
||||||
/*
|
|
||||||
if (publicChatManager != null) {
|
|
||||||
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isCaughtUp) {
|
if (isCaughtUp) {
|
||||||
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId));
|
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId));
|
||||||
@ -88,18 +77,12 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
|||||||
@Override
|
@Override
|
||||||
public void updateNotification(@NonNull Context context, long threadId, boolean signal) {
|
public void updateNotification(@NonNull Context context, long threadId, boolean signal) {
|
||||||
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
||||||
// FIXME: Open group handling
|
|
||||||
boolean isCaughtUp = true;
|
boolean isCaughtUp = true;
|
||||||
if (lokiPoller != null) {
|
if (lokiPoller != null) {
|
||||||
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Open group handling
|
isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp();
|
||||||
/*
|
|
||||||
if (publicChatManager != null) {
|
|
||||||
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isCaughtUp) {
|
if (isCaughtUp) {
|
||||||
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId, signal));
|
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId, signal));
|
||||||
@ -111,18 +94,12 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
|||||||
@Override
|
@Override
|
||||||
public void updateNotification(@androidx.annotation.NonNull Context context, boolean signal, int reminderCount) {
|
public void updateNotification(@androidx.annotation.NonNull Context context, boolean signal, int reminderCount) {
|
||||||
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
|
||||||
// FIXME: Open group handling
|
|
||||||
boolean isCaughtUp = true;
|
boolean isCaughtUp = true;
|
||||||
if (lokiPoller != null) {
|
if (lokiPoller != null) {
|
||||||
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Open group handling
|
isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp();
|
||||||
/*
|
|
||||||
if (publicChatManager != null) {
|
|
||||||
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (isCaughtUp) {
|
if (isCaughtUp) {
|
||||||
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, signal, reminderCount));
|
performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, signal, reminderCount));
|
||||||
|
@ -9,13 +9,14 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
|||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||||
|
|
||||||
class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
||||||
|
|
||||||
override fun setDisplayName(context: Context, recipient: Recipient, displayName: String?) {
|
override fun setDisplayName(context: Context, recipient: Recipient, displayName: String?) {
|
||||||
val sessionID = recipient.address.serialize()
|
val sessionID = recipient.address.serialize()
|
||||||
// New API
|
// New API
|
||||||
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
if (contact == null) contact = Contact(sessionID)
|
if (contact == null) contact = Contact(sessionID)
|
||||||
contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address)
|
contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address)
|
||||||
if (contact.nickname != displayName) {
|
if (contact.nickname != displayName) {
|
||||||
contact.nickname = displayName
|
contact.nickname = displayName
|
||||||
contactDatabase.setContact(contact)
|
contactDatabase.setContact(contact)
|
||||||
@ -35,7 +36,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
|||||||
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
if (contact == null) contact = Contact(sessionID)
|
if (contact == null) contact = Contact(sessionID)
|
||||||
contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address)
|
contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address)
|
||||||
if (contact.name != profileName) {
|
if (contact.name != profileName) {
|
||||||
contact.name = profileName
|
contact.name = profileName
|
||||||
contactDatabase.setContact(contact)
|
contactDatabase.setContact(contact)
|
||||||
@ -49,7 +50,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
|||||||
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
if (contact == null) contact = Contact(sessionID)
|
if (contact == null) contact = Contact(sessionID)
|
||||||
contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address)
|
contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address)
|
||||||
if (contact.profilePictureURL != profilePictureURL) {
|
if (contact.profilePictureURL != profilePictureURL) {
|
||||||
contact.profilePictureURL = profilePictureURL
|
contact.profilePictureURL = profilePictureURL
|
||||||
contactDatabase.setContact(contact)
|
contactDatabase.setContact(contact)
|
||||||
@ -64,7 +65,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
|||||||
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
if (contact == null) contact = Contact(sessionID)
|
if (contact == null) contact = Contact(sessionID)
|
||||||
contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address)
|
contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address)
|
||||||
if (!contact.profilePictureEncryptionKey.contentEquals(profileKey)) {
|
if (!contact.profilePictureEncryptionKey.contentEquals(profileKey)) {
|
||||||
contact.profilePictureEncryptionKey = profileKey
|
contact.profilePictureEncryptionKey = profileKey
|
||||||
contactDatabase.setContact(contact)
|
contactDatabase.setContact(contact)
|
||||||
@ -76,17 +77,13 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
|||||||
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateOpenGroupProfilePicturesIfNeeded(context: Context) {
|
|
||||||
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDisplayName(context: Context, recipient: Recipient): String? {
|
override fun getDisplayName(context: Context, recipient: Recipient): String? {
|
||||||
val sessionID = recipient.address.serialize()
|
val sessionID = recipient.address.serialize()
|
||||||
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
val contactDatabase = DatabaseFactory.getSessionContactDatabase(context)
|
||||||
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
var contact = contactDatabase.getContactWithSessionID(sessionID)
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
contact = Contact(sessionID)
|
contact = Contact(sessionID)
|
||||||
contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address)
|
contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address)
|
||||||
contact.name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(sessionID) ?: recipient.profileName ?: recipient.name
|
contact.name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(sessionID) ?: recipient.profileName ?: recipient.name
|
||||||
contactDatabase.setContact(contact)
|
contactDatabase.setContact(contact)
|
||||||
}
|
}
|
||||||
|
@ -1,706 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">Session</string>
|
|
||||||
<string name="yes">Da</string>
|
|
||||||
<string name="no">Ne</string>
|
|
||||||
<string name="delete">Obriši</string>
|
|
||||||
<string name="ban">Ban</string>
|
|
||||||
<string name="please_wait">Sačekajte...</string>
|
|
||||||
<string name="save">Spremi</string>
|
|
||||||
<string name="note_to_self">Osobna bilješka</string>
|
|
||||||
<string name="version_s">Version %s</string>
|
|
||||||
<!-- AbstractNotificationBuilder -->
|
|
||||||
<string name="AbstractNotificationBuilder_new_message">Nova poruka</string>
|
|
||||||
<!-- AlbumThumbnailView -->
|
|
||||||
<string name="AlbumThumbnailView_plus">\+%d</string>
|
|
||||||
<!-- ApplicationPreferencesActivity -->
|
|
||||||
<plurals name="ApplicationPreferencesActivity_messages_per_conversation">
|
|
||||||
<item quantity="one">%d poruka po razgovoru</item>
|
|
||||||
<item quantity="few">%d poruke po razgovoru</item>
|
|
||||||
<item quantity="many">%d poruka po razgovoru</item>
|
|
||||||
<item quantity="other">%d poruka po razgovoru</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="ApplicationPreferencesActivity_delete_all_old_messages_now">Obriši sve stare poruke?</string>
|
|
||||||
<plurals name="ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages">
|
|
||||||
<item quantity="one">Ovo će automatski skratiti sve razgovore na odabrani broj poruka.</item>
|
|
||||||
<item quantity="few">Ovo će automatski skratiti sve razgovore na %d najnovije poruke.</item>
|
|
||||||
<item quantity="many">Ovo će automatski skratiti sve razgovore na %d najnovijih poruka.</item>
|
|
||||||
<item quantity="other">Ovo će automatski skratiti sve razgovore na %d najnovijih poruka.</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="ApplicationPreferencesActivity_delete">Obriši</string>
|
|
||||||
<string name="ApplicationPreferencesActivity_On">Uključeno</string>
|
|
||||||
<string name="ApplicationPreferencesActivity_Off">Isključeno</string>
|
|
||||||
<!-- DraftDatabase -->
|
|
||||||
<string name="DraftDatabase_Draft_image_snippet">(slika)</string>
|
|
||||||
<string name="DraftDatabase_Draft_audio_snippet">(zvuk)</string>
|
|
||||||
<string name="DraftDatabase_Draft_video_snippet">(video)</string>
|
|
||||||
<string name="DraftDatabase_Draft_quote_snippet">(odgovor)</string>
|
|
||||||
<!-- AttchmentManager -->
|
|
||||||
<string name="AttachmentManager_cant_open_media_selection">Nije moguće pronaći aplikaciju za odabir medija.</string>
|
|
||||||
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">Session zahtijeva dozvolu pristupa Pohrani podataka za umetanje slikovnih i audio-vizualnih priloga ali zahtjev biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Pohrana\".</string>
|
|
||||||
<string name="AttachmentManager_signal_requires_contacts_permission_in_order_to_attach_contact_information">Session zahtijeva dozvolu pristupa Kontaktima za prilaganje informacije o kontaktima ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Kontakti\".</string>
|
|
||||||
<string name="AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied">Session zahtijeva dozvolu pristupa Kameri za omogućavanje slikanja ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Kamera\".</string>
|
|
||||||
<!-- AudioSlidePlayer -->
|
|
||||||
<string name="AudioSlidePlayer_error_playing_audio">Greška prilikom reprodukcije zvuka!</string>
|
|
||||||
<!-- BucketedThreadMedia -->
|
|
||||||
<string name="BucketedThreadMedia_Today">Danas</string>
|
|
||||||
<string name="BucketedThreadMedia_Yesterday">Jučer</string>
|
|
||||||
<string name="BucketedThreadMedia_This_week">Ovaj tjedan</string>
|
|
||||||
<string name="BucketedThreadMedia_This_month">Ovaj mjesec</string>
|
|
||||||
<!-- CommunicationActions -->
|
|
||||||
<string name="CommunicationActions_no_browser_found">Web pretraživać nije pronađen.</string>
|
|
||||||
<!-- ContactsCursorLoader -->
|
|
||||||
<string name="ContactsCursorLoader_groups">Grupe</string>
|
|
||||||
<!-- ConversationItem -->
|
|
||||||
<string name="ConversationItem_error_not_delivered">Slanje nije uspjelo, dodirnite za detalje</string>
|
|
||||||
<string name="ConversationItem_received_key_exchange_message_tap_to_process">Poruka za razmjenu ključeva je primljena, pritisnite da biste nastavili.</string>
|
|
||||||
<string name="ConversationItem_group_action_left">%1$s je napustio grupu.</string>
|
|
||||||
<string name="ConversationItem_click_to_approve_unencrypted">Slanje neuspješno, pritisnite za nesigurnu rezervu</string>
|
|
||||||
<string name="ConversationItem_unable_to_open_media">Nije moguće pronaći aplikaciju za otvaranje ovog medija.</string>
|
|
||||||
<string name="ConversationItem_copied_text">Kopirano %s</string>
|
|
||||||
<string name="ConversationItem_read_more">Pročitaj više</string>
|
|
||||||
<string name="ConversationItem_download_more">Preuzmi više</string>
|
|
||||||
<string name="ConversationItem_pending">Na čekanju</string>
|
|
||||||
<!-- ConversationActivity -->
|
|
||||||
<string name="ConversationActivity_add_attachment">Dodaj privitak</string>
|
|
||||||
<string name="ConversationActivity_select_contact_info">Odaberite informacije kontakta</string>
|
|
||||||
<string name="ConversationActivity_sorry_there_was_an_error_setting_your_attachment">Došlo je do greške prilikom postavljanja privitka.</string>
|
|
||||||
<string name="ConversationActivity_message">Message</string>
|
|
||||||
<string name="ConversationActivity_invalid_recipient">Naispravan primatelj!</string>
|
|
||||||
<string name="ConversationActivity_added_to_home_screen">Dodano na početni ekran</string>
|
|
||||||
<string name="ConversationActivity_leave_group">Napusti grupu?</string>
|
|
||||||
<string name="ConversationActivity_are_you_sure_you_want_to_leave_this_group">Jeste li sigurni da želite napustiti ovu grupu?</string>
|
|
||||||
<string name="ConversationActivity_error_leaving_group">Greška pri napuštanju grupe</string>
|
|
||||||
<string name="ConversationActivity_unblock_this_contact_question">Ukloni blokadu ovog kontakta?</string>
|
|
||||||
<string name="ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact">Ponovno ćete moći primati poruke ili pozive ovog korisnika.</string>
|
|
||||||
<string name="ConversationActivity_unblock">Ukloni blokadu</string>
|
|
||||||
<string name="ConversationActivity_attachment_exceeds_size_limits">Privitak prelazi ograničenje veličine za tip poruke koju šaljete.</string>
|
|
||||||
<string name="ConversationActivity_unable_to_record_audio">Nije moguće snimati svuk!</string>
|
|
||||||
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">Na vašem uređaju ne postoji aplikacija koja bi rukovala ovom poveznicom.</string>
|
|
||||||
<string name="ConversationActivity_invite_to_open_group">Add members</string>
|
|
||||||
<string name="ConversationActivity_join_open_group">Join %s</string>
|
|
||||||
<string name="ConversationActivity_join_open_group_confirmation_message">Are you sure you want to join the <b>%s</b> open group?</string>
|
|
||||||
<string name="ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone">Kako biste poslali audio poruku, dozvolite Sessionu pristup vašem mikrofonu.</string>
|
|
||||||
<string name="ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages">Session zahtijeva dozvolu pristupa Mikrofonu za slanje zvučnih poruka ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Mikrofon\".</string>
|
|
||||||
<string name="ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera">Kako biste snimili slike i video, dozvolite Sessionu pristup kameri.</string>
|
|
||||||
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">Session zahtijeva dozvolu Kameri za fotografiranje ili snimanje videa, ali pristup biva odbijen. Molim nastavite s postavkama aplikacije, odaberite \"Dozvole\" i omogućite \"Kamera\". </string>
|
|
||||||
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Session treba pristup kameri kako bi snimio slike ili video.</string>
|
|
||||||
<string name="ConversationActivity_quoted_contact_message">%1$s%2$s</string>
|
|
||||||
<string name="ConversationActivity_search_position">%1$d od %2$d</string>
|
|
||||||
<string name="ConversationActivity_no_results">Nema rezultata</string>
|
|
||||||
<!-- ConversationAdapter -->
|
|
||||||
<plurals name="ConversationAdapter_n_unread_messages">
|
|
||||||
<item quantity="one">%d nepročitana poruka</item>
|
|
||||||
<item quantity="few">%d nepročitane poruke</item>
|
|
||||||
<item quantity="many">%d nepročitanih poruka</item>
|
|
||||||
<item quantity="other">%d nepročitanih poruka</item>
|
|
||||||
</plurals>
|
|
||||||
<!-- ConversationFragment -->
|
|
||||||
<plurals name="ConversationFragment_delete_selected_messages">
|
|
||||||
<item quantity="one">Obriši odabrane poruke?</item>
|
|
||||||
<item quantity="few">Obriši odabrane poruke?</item>
|
|
||||||
<item quantity="many">Obriši odabrane poruke?</item>
|
|
||||||
<item quantity="other">Obriši odabrane poruke?</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="ConversationFragment_this_will_permanently_delete_all_n_selected_messages">
|
|
||||||
<item quantity="one">Ovo će trajno obrisati odabrane poruke.</item>
|
|
||||||
<item quantity="few">Ovo će trajno obrisati sve %1$d odabrane poruke.</item>
|
|
||||||
<item quantity="many">Ovo će trajno obrisati svih %1$d odabranih poruka.</item>
|
|
||||||
<item quantity="other">Ovo će trajno obrisati svih %1$d odabranih poruka.</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="ConversationFragment_ban_selected_user">Ban this user?</string>
|
|
||||||
<string name="ConversationFragment_save_to_sd_card">Spremi na disk?</string>
|
|
||||||
<plurals name="ConversationFragment_saving_n_media_to_storage_warning">
|
|
||||||
<item quantity="one">Spremanje svih medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi?</item>
|
|
||||||
<item quantity="few">Spremanje sva %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi?</item>
|
|
||||||
<item quantity="many">Spremanje svih %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi?</item>
|
|
||||||
<item quantity="other">Spremanje svih %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi?</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
|
|
||||||
<item quantity="one">Greška prilikom spremanja privitaka na disk!</item>
|
|
||||||
<item quantity="few">Greška prilikom spremanja privitaka na disk!</item>
|
|
||||||
<item quantity="many">Greška prilikom spremanja privitaka na disk!</item>
|
|
||||||
<item quantity="other">Greška prilikom spremanja privitaka na disk!</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="ConversationFragment_saving_n_attachments">
|
|
||||||
<item quantity="one">Spremanje privitaka</item>
|
|
||||||
<item quantity="few">Spremanje %1$d privitka</item>
|
|
||||||
<item quantity="many">Spremanje %1$d privitaka</item>
|
|
||||||
<item quantity="other">Spremanje %1$d privitaka</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="ConversationFragment_saving_n_attachments_to_sd_card">
|
|
||||||
<item quantity="one">Spremanje privitaka na disk...</item>
|
|
||||||
<item quantity="few">Spremanje %1$d privitka na disk...</item>
|
|
||||||
<item quantity="many">Spremanje %1$d privitaka na disk...</item>
|
|
||||||
<item quantity="other">Spremanje %1$d privitaka na disk...</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="ConversationFragment_pending">U toku...</string>
|
|
||||||
<string name="ConversationFragment_push">Podaci (Session)</string>
|
|
||||||
<string name="ConversationFragment_mms">MMS</string>
|
|
||||||
<string name="ConversationFragment_sms">SMS</string>
|
|
||||||
<string name="ConversationFragment_deleting">Brisanje</string>
|
|
||||||
<string name="ConversationFragment_deleting_messages">Brisanje poruka...</string>
|
|
||||||
<string name="ConversationFragment_banning">Banning</string>
|
|
||||||
<string name="ConversationFragment_banning_user">Banning user…</string>
|
|
||||||
<string name="ConversationFragment_quoted_message_not_found">Originalna poruka nije pronađena</string>
|
|
||||||
<string name="ConversationFragment_quoted_message_no_longer_available">Originalna poruka više nije dostupna</string>
|
|
||||||
<!-- ConversationListItem -->
|
|
||||||
<string name="ConversationListItem_key_exchange_message">Poruka za razmjenu ključa</string>
|
|
||||||
<!-- CreateProfileActivity -->
|
|
||||||
<string name="CreateProfileActivity_profile_photo">Profilna slika</string>
|
|
||||||
<!-- CustomDefaultPreference -->
|
|
||||||
<string name="CustomDefaultPreference_using_custom">Koristeći prilagođeno: %s</string>
|
|
||||||
<string name="CustomDefaultPreference_using_default">Koristeći zadano: %s</string>
|
|
||||||
<string name="CustomDefaultPreference_none">Niti jedna</string>
|
|
||||||
<!-- DateUtils -->
|
|
||||||
<string name="DateUtils_just_now">Sada</string>
|
|
||||||
<string name="DateUtils_minutes_ago">%d min</string>
|
|
||||||
<string name="DateUtils_today">Danas</string>
|
|
||||||
<string name="DateUtils_yesterday">Jučer</string>
|
|
||||||
<!-- DeviceListItem -->
|
|
||||||
<string name="DeviceListItem_today">Danas</string>
|
|
||||||
<!-- DocumentView -->
|
|
||||||
<string name="DocumentView_unknown_file">Nepoznata datoteka</string>
|
|
||||||
<!-- GiphyActivity -->
|
|
||||||
<string name="GiphyActivity_error_while_retrieving_full_resolution_gif">Greška pri dohvaćanju GIFa pune rezolucije</string>
|
|
||||||
<!-- GiphyFragmentPageAdapter -->
|
|
||||||
<string name="GiphyFragmentPagerAdapter_gifs">GIFovi</string>
|
|
||||||
<string name="GiphyFragmentPagerAdapter_stickers">Naljepnice</string>
|
|
||||||
<!-- CropImageActivity -->
|
|
||||||
<string name="CropImageActivity_profile_avatar">Slika profila</string>
|
|
||||||
<!-- InputPanel -->
|
|
||||||
<string name="InputPanel_tap_and_hold_to_record_a_voice_message_release_to_send">Pritisnite i držite kako biste snimili glasovnu poruku, pustite za slanje</string>
|
|
||||||
<!-- LongMessageActivity -->
|
|
||||||
<string name="LongMessageActivity_unable_to_find_message">Nije moguće naći poruku</string>
|
|
||||||
<string name="LongMessageActivity_message_from_s">Poruka od %1$s</string>
|
|
||||||
<string name="LongMessageActivity_your_message">Vaša poruka</string>
|
|
||||||
<!-- MediaOverviewActivity -->
|
|
||||||
<string name="MediaOverviewActivity_Media">Medij</string>
|
|
||||||
<plurals name="MediaOverviewActivity_Media_delete_confirm_title">
|
|
||||||
<item quantity="one">Izbriši označenu poruku?</item>
|
|
||||||
<item quantity="few">Izbriši označene poruke?</item>
|
|
||||||
<item quantity="many">Izbriši označene poruke?</item>
|
|
||||||
<item quantity="other">Izbriši označene poruke?</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="MediaOverviewActivity_Media_delete_confirm_message">
|
|
||||||
<item quantity="one">Ovo će trajno obrisati označenu poruku.</item>
|
|
||||||
<item quantity="few">Broj poruka koji će biti trajno izbrisan: %1$d</item>
|
|
||||||
<item quantity="many">Broj poruka koji će biti trajno izbrisan: %1$d</item>
|
|
||||||
<item quantity="other">Broj poruka koji će biti trajno izbrisan: %1$d</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="MediaOverviewActivity_Media_delete_progress_title">Brisanje</string>
|
|
||||||
<string name="MediaOverviewActivity_Media_delete_progress_message">Brisanje poruka...</string>
|
|
||||||
<string name="MediaOverviewActivity_Documents">Dokumenti</string>
|
|
||||||
<string name="MediaOverviewActivity_Select_all">Odaberi sve</string>
|
|
||||||
<string name="MediaOverviewActivity_collecting_attachments">Prikupljanje privitaka...</string>
|
|
||||||
<!-- NotificationMmsMessageRecord -->
|
|
||||||
<string name="NotificationMmsMessageRecord_multimedia_message">Multimedijalna poruka</string>
|
|
||||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Preuzimanje MMS poruke</string>
|
|
||||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Greška pri preuzimanju MMS poruke, pritisnite za ponovni pokušaj</string>
|
|
||||||
<!-- MediaPickerActivity -->
|
|
||||||
<string name="MediaPickerActivity_send_to">Send to %s</string>
|
|
||||||
<!-- MediaSendActivity -->
|
|
||||||
<string name="MediaSendActivity_add_a_caption">Add a caption...</string>
|
|
||||||
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">Stavka je uklonjena jer je prešla limit veličine</string>
|
|
||||||
<string name="MediaSendActivity_camera_unavailable">Kamera nije dostupna</string>
|
|
||||||
<string name="MediaSendActivity_message_to_s">Message to %s</string>
|
|
||||||
<plurals name="MediaSendActivity_cant_share_more_than_n_items">
|
|
||||||
<item quantity="one">Ne možete dijeliti više od %dstavke.</item>
|
|
||||||
<item quantity="few">Ne možete dijeliti više od %d stavki.</item>
|
|
||||||
<item quantity="many">Ne možete dijeliti više od %dstavki.</item>
|
|
||||||
<item quantity="other">Ne možete dijeliti više od %dstavki.</item>
|
|
||||||
</plurals>
|
|
||||||
<!-- MediaRepository -->
|
|
||||||
<string name="MediaRepository_all_media">Svi mediji</string>
|
|
||||||
<!-- MessageRecord -->
|
|
||||||
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Primljena je poruka kriptirana starom inačicom Session aplikacije koja više nije podržana. Molimo zapitajte pošiljatelja da ažurira na najnoviju inačicu aplikacije i ponovno pošalje poruku.</string>
|
|
||||||
<string name="MessageRecord_left_group">Napustili ste grupu.</string>
|
|
||||||
<string name="MessageRecord_you_updated_group">Ažurirali ste grupu.</string>
|
|
||||||
<string name="MessageRecord_s_updated_group">%s je ažurirao grupu.</string>
|
|
||||||
<!-- ExpirationDialog -->
|
|
||||||
<string name="ExpirationDialog_disappearing_messages">Nestajuće poruke</string>
|
|
||||||
<string name="ExpirationDialog_your_messages_will_not_expire">Vaše poruke neće isteći.</string>
|
|
||||||
<string name="ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen">Primljene i poslane poruke u ovom razgovoru će nestati %s nakon što su viđene.</string>
|
|
||||||
<!-- PassphrasePromptActivity -->
|
|
||||||
<string name="PassphrasePromptActivity_enter_passphrase">Unesite lozinku</string>
|
|
||||||
<!-- RecipientPreferencesActivity -->
|
|
||||||
<string name="RecipientPreferenceActivity_block_this_contact_question">Blokiraj kontakt?</string>
|
|
||||||
<string name="RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact">Nećete više primati poruke i pozive ovog korisnika.</string>
|
|
||||||
<string name="RecipientPreferenceActivity_block">Blokiraj</string>
|
|
||||||
<string name="RecipientPreferenceActivity_unblock_this_contact_question">Ukloni blokadu ovog kontakta?</string>
|
|
||||||
<string name="RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact">Ponovno ćete moći primati poruke ili pozive ovog korisnika.</string>
|
|
||||||
<string name="RecipientPreferenceActivity_unblock">Ukloni blokadu</string>
|
|
||||||
<!-- Slide -->
|
|
||||||
<string name="Slide_image">Slika</string>
|
|
||||||
<string name="Slide_audio">Audio</string>
|
|
||||||
<string name="Slide_video">Video</string>
|
|
||||||
<!-- SmsMessageRecord -->
|
|
||||||
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Primljena iskvarena poruka
|
|
||||||
razmjene ključeva!</string>
|
|
||||||
<string name="SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version">Primljena poruka razmjene ključeva za pogrešnu inačicu protokola.</string>
|
|
||||||
<string name="SmsMessageRecord_received_message_with_new_safety_number_tap_to_process">Primljena je poruka s novim sigurnosnim brojem. Pritisnite za obradu i prikaz.</string>
|
|
||||||
<string name="SmsMessageRecord_secure_session_reset">Resetiranje sigurne sesije.</string>
|
|
||||||
<string name="SmsMessageRecord_secure_session_reset_s">%s resetiranje sigurne sesije.</string>
|
|
||||||
<string name="SmsMessageRecord_duplicate_message">Dupla poruka.</string>
|
|
||||||
<!-- ThreadRecord -->
|
|
||||||
<string name="ThreadRecord_group_updated">Grupa je ažurirana</string>
|
|
||||||
<string name="ThreadRecord_left_the_group">Napustio/la grupu</string>
|
|
||||||
<string name="ThreadRecord_secure_session_reset">Resetiranje sigurne sesije.</string>
|
|
||||||
<string name="ThreadRecord_draft">Skica:</string>
|
|
||||||
<string name="ThreadRecord_called">Zvali ste</string>
|
|
||||||
<string name="ThreadRecord_called_you">Zvali su vas</string>
|
|
||||||
<string name="ThreadRecord_missed_call">Propušteni poziv</string>
|
|
||||||
<string name="ThreadRecord_media_message">Multimedijalna poruka</string>
|
|
||||||
<string name="ThreadRecord_s_is_on_signal">%s je dostupan na Sessionu!</string>
|
|
||||||
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
|
|
||||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Vrijeme nestajanja poruke postavljeno na %s</string>
|
|
||||||
<string name="ThreadRecord_s_took_a_screenshot">%s took a screenshot.</string>
|
|
||||||
<string name="ThreadRecord_media_saved_by_s">Media saved by %s.</string>
|
|
||||||
<string name="ThreadRecord_safety_number_changed">Sigurnosni broj je izmijenjen</string>
|
|
||||||
<string name="ThreadRecord_your_safety_number_with_s_has_changed">Vaš sigurnosni broj s %s je izmjenjen.</string>
|
|
||||||
<string name="ThreadRecord_you_marked_verified">Označili ste provjerenim</string>
|
|
||||||
<string name="ThreadRecord_you_marked_unverified">Označili ste nepotvrđenim</string>
|
|
||||||
<string name="ThreadRecord_empty_message">This conversation is empty</string>
|
|
||||||
<string name="ThreadRecord_open_group_invitation">Open group invitation</string>
|
|
||||||
<!-- UpdateApkReadyListener -->
|
|
||||||
<string name="UpdateApkReadyListener_Signal_update">Session ažuriranje</string>
|
|
||||||
<string name="UpdateApkReadyListener_a_new_version_of_signal_is_available_tap_to_update">Nova inačica Sessiona je dostupna, pritisnite za ažuriranje</string>
|
|
||||||
<!-- MessageDisplayHelper -->
|
|
||||||
<string name="MessageDisplayHelper_bad_encrypted_message">Loše kriptirana poruka</string>
|
|
||||||
<string name="MessageDisplayHelper_message_encrypted_for_non_existing_session">Poruka kriptirana za nepostojeću sesiju</string>
|
|
||||||
<!-- MmsMessageRecord -->
|
|
||||||
<string name="MmsMessageRecord_bad_encrypted_mms_message">Loše kriptirana MMS poruka</string>
|
|
||||||
<string name="MmsMessageRecord_mms_message_encrypted_for_non_existing_session">MMS poruka kriptirana za nepostojeću sesiju</string>
|
|
||||||
<!-- MuteDialog -->
|
|
||||||
<string name="MuteDialog_mute_notifications">Utišaj obavijesti</string>
|
|
||||||
<!-- KeyCachingService -->
|
|
||||||
<string name="KeyCachingService_signal_passphrase_cached">Pritisnite za otvaranje.</string>
|
|
||||||
<string name="KeyCachingService_passphrase_cached">Session je otključan</string>
|
|
||||||
<string name="KeyCachingService_lock">Zaključaj Session</string>
|
|
||||||
<!-- MediaPreviewActivity -->
|
|
||||||
<string name="MediaPreviewActivity_you">Vi</string>
|
|
||||||
<string name="MediaPreviewActivity_unssuported_media_type">Nepodržani tip medija</string>
|
|
||||||
<string name="MediaPreviewActivity_draft">Skica</string>
|
|
||||||
<string name="MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied">Session zahtjeva pristup pohrani kako bi se omogućilo zapisivanje podataka na vanjski medij, ali je trajno odbijeno. Molimo nastavite do opcija aplikacije, odaberite \"Dozvole\", i uključite \"Pohrana\".</string>
|
|
||||||
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Nije moguće spremanje na vanjski medij bez dozvola</string>
|
|
||||||
<string name="MediaPreviewActivity_media_delete_confirmation_title">Obriši poruku?</string>
|
|
||||||
<string name="MediaPreviewActivity_media_delete_confirmation_message">Radnja će trajno izbrisati ovu poruku.</string>
|
|
||||||
<!-- MessageNotifier -->
|
|
||||||
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d novih poruka u %2$d razgovora</string>
|
|
||||||
<string name="MessageNotifier_most_recent_from_s">Najnovije od: %1$s</string>
|
|
||||||
<string name="MessageNotifier_locked_message">Zaključana poruka</string>
|
|
||||||
<string name="MessageNotifier_message_delivery_failed">Neuspješna isporuka poruke.</string>
|
|
||||||
<string name="MessageNotifier_failed_to_deliver_message">Isporuka poruke nije uspjela.</string>
|
|
||||||
<string name="MessageNotifier_error_delivering_message">Greška prilikom isporuke poruke.</string>
|
|
||||||
<string name="MessageNotifier_mark_all_as_read">Označi sve kao pročitano</string>
|
|
||||||
<string name="MessageNotifier_mark_read">Označi pročitano</string>
|
|
||||||
<string name="MessageNotifier_reply">Odgovori</string>
|
|
||||||
<string name="MessageNotifier_pending_signal_messages">Session poruke na čekanju</string>
|
|
||||||
<string name="MessageNotifier_you_have_pending_signal_messages">Imate Session poruka na čekanju, pritisnite kako biste ih otvorili i preuzeli</string>
|
|
||||||
<string name="MessageNotifier_contact_message">%1$s%2$s</string>
|
|
||||||
<string name="MessageNotifier_unknown_contact_message">Kontakt</string>
|
|
||||||
<!-- Notification Channels -->
|
|
||||||
<string name="NotificationChannel_messages">Zadano</string>
|
|
||||||
<string name="NotificationChannel_calls">Pozivi</string>
|
|
||||||
<string name="NotificationChannel_failures">Neuspjesi</string>
|
|
||||||
<string name="NotificationChannel_backups">Sigurnosne kopije</string>
|
|
||||||
<string name="NotificationChannel_locked_status">Status zaključavanja</string>
|
|
||||||
<string name="NotificationChannel_app_updates">Ažuriranja aplikacije</string>
|
|
||||||
<string name="NotificationChannel_other">Ostalo</string>
|
|
||||||
<string name="NotificationChannel_group_messages">Poruke</string>
|
|
||||||
<string name="NotificationChannel_missing_display_name">Nepoznato</string>
|
|
||||||
<!-- QuickResponseService -->
|
|
||||||
<string name="QuickResponseService_quick_response_unavailable_when_Signal_is_locked">Brzi odgovor nije dostupan kada je Session zaključan!</string>
|
|
||||||
<string name="QuickResponseService_problem_sending_message">Greška prilikom slanja poruke!</string>
|
|
||||||
<!-- SaveAttachmentTask -->
|
|
||||||
<string name="SaveAttachmentTask_saved_to">Spremljeno u %s</string>
|
|
||||||
<string name="SaveAttachmentTask_saved">Spremljeno</string>
|
|
||||||
<!-- SearchToolbar -->
|
|
||||||
<string name="SearchToolbar_search">Traži</string>
|
|
||||||
<!-- ShortcutLauncherActivity -->
|
|
||||||
<string name="ShortcutLauncherActivity_invalid_shortcut">Nevažeći prečac</string>
|
|
||||||
<!-- SingleRecipientNotificationBuilder -->
|
|
||||||
<string name="SingleRecipientNotificationBuilder_signal">Session</string>
|
|
||||||
<string name="SingleRecipientNotificationBuilder_new_message">Nova poruka</string>
|
|
||||||
<!-- TransferControlView -->
|
|
||||||
<plurals name="TransferControlView_n_items">
|
|
||||||
<item quantity="one">%d Item</item>
|
|
||||||
<item quantity="few">%d Items</item>
|
|
||||||
<item quantity="many">%d Items</item>
|
|
||||||
<item quantity="other">%d Items</item>
|
|
||||||
</plurals>
|
|
||||||
<!-- VideoPlayer -->
|
|
||||||
<string name="VideoPlayer_error_playing_video">Greška pri video reprodukciji</string>
|
|
||||||
<!-- attachment_type_selector -->
|
|
||||||
<string name="attachment_type_selector__audio">Zvuk</string>
|
|
||||||
<string name="attachment_type_selector__audio_description">Zvuk</string>
|
|
||||||
<string name="attachment_type_selector__contact">Kontakt</string>
|
|
||||||
<string name="attachment_type_selector__contact_description">Kontakt</string>
|
|
||||||
<string name="attachment_type_selector__camera">Kamera</string>
|
|
||||||
<string name="attachment_type_selector__camera_description">Kamera</string>
|
|
||||||
<string name="attachment_type_selector__location">Položaj</string>
|
|
||||||
<string name="attachment_type_selector__location_description">Položaj</string>
|
|
||||||
<string name="attachment_type_selector__gif">GIF</string>
|
|
||||||
<string name="attachment_type_selector__gif_description">Gif</string>
|
|
||||||
<string name="attachment_type_selector__gallery_description">Slika ili video</string>
|
|
||||||
<string name="attachment_type_selector__file_description">Datoteka</string>
|
|
||||||
<string name="attachment_type_selector__gallery">Galerija</string>
|
|
||||||
<string name="attachment_type_selector__file">Datoteka</string>
|
|
||||||
<string name="attachment_type_selector__drawer_description">Uključi/isključi ladicu dodataka</string>
|
|
||||||
<!-- contact_selection_group_activity -->
|
|
||||||
<string name="contact_selection_group_activity__finding_contacts">Učitavanje kontakata...</string>
|
|
||||||
<!-- conversation_activity -->
|
|
||||||
<string name="conversation_activity__send">Pošalji</string>
|
|
||||||
<string name="conversation_activity__compose_description">Sastavljanje poruke</string>
|
|
||||||
<string name="conversation_activity__emoji_toggle_description">Uključi/isključi emoji tipkovnicu</string>
|
|
||||||
<string name="conversation_activity__attachment_thumbnail">Priložena sličica</string>
|
|
||||||
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Uključi/isključi ladicu brze kamere</string>
|
|
||||||
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Snimi i pošalji audio privitak</string>
|
|
||||||
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Lock recording of audio attachment</string>
|
|
||||||
<string name="conversation_activity__enable_signal_for_sms">Omogući Session za SMS</string>
|
|
||||||
<!-- conversation_input_panel -->
|
|
||||||
<string name="conversation_input_panel__slide_to_cancel">Kliznite za otkazivanje</string>
|
|
||||||
<string name="conversation_input_panel__cancel">Odustani</string>
|
|
||||||
<!-- conversation_item -->
|
|
||||||
<string name="conversation_item__mms_image_description">Multimedijalna poruka</string>
|
|
||||||
<string name="conversation_item__secure_message_description">Sigurna poruka</string>
|
|
||||||
<!-- conversation_item_sent -->
|
|
||||||
<string name="conversation_item_sent__send_failed_indicator_description">Neuspješno slanje</string>
|
|
||||||
<string name="conversation_item_sent__pending_approval_description">Odobrenje u tijeku</string>
|
|
||||||
<string name="conversation_item_sent__delivered_description">Isporučeno</string>
|
|
||||||
<string name="conversation_item_sent__message_read">Poruka pročitana</string>
|
|
||||||
<!-- conversation_item_received -->
|
|
||||||
<string name="conversation_item_received__contact_photo_description">Slika kontakta</string>
|
|
||||||
<!-- audio_view -->
|
|
||||||
<string name="audio_view__play_accessibility_description">Reprodukcija</string>
|
|
||||||
<string name="audio_view__pause_accessibility_description">Zaustavi</string>
|
|
||||||
<string name="audio_view__download_accessibility_description">Preuzmi</string>
|
|
||||||
<!-- open_group_invitation_view -->
|
|
||||||
<string name="open_group_invitation_view__join_accessibility_description">Join</string>
|
|
||||||
<string name="open_group_invitation_view__open_group_invitation">Open group invitation</string>
|
|
||||||
<!-- QuoteView -->
|
|
||||||
<string name="QuoteView_audio">Zvuk</string>
|
|
||||||
<string name="QuoteView_video">Video</string>
|
|
||||||
<string name="QuoteView_photo">Fotografija</string>
|
|
||||||
<string name="QuoteView_you">Vi</string>
|
|
||||||
<string name="QuoteView_original_missing">Originalna poruka nije pronađena</string>
|
|
||||||
<!-- conversation_fragment -->
|
|
||||||
<string name="conversation_fragment__scroll_to_the_bottom_content_description">Kliži do dna</string>
|
|
||||||
<!-- giphy_activity -->
|
|
||||||
<string name="giphy_activity_toolbar__search_gifs_and_stickers">Pretraži GIFove i naljepnice</string>
|
|
||||||
<!-- giphy_fragment -->
|
|
||||||
<string name="giphy_fragment__nothing_found">Ništa nije pronađeno.</string>
|
|
||||||
<!-- load_more_header -->
|
|
||||||
<string name="load_more_header__see_full_conversation">Prikaži cijeli razgovor</string>
|
|
||||||
<string name="load_more_header__loading">Učitavanje</string>
|
|
||||||
<!-- media_overview_activity -->
|
|
||||||
<string name="media_overview_activity__no_media">Bez medija</string>
|
|
||||||
<!-- message_recipients_list_item -->
|
|
||||||
<string name="message_recipients_list_item__resend">PONOVNO POŠALJI</string>
|
|
||||||
<!-- recipient_preferences -->
|
|
||||||
<string name="recipient_preferences__block">Blokiraj</string>
|
|
||||||
<!-- message_details_header -->
|
|
||||||
<string name="message_details_header__issues_need_your_attention">Neki problemi zahtjevaju vašu pozornost.</string>
|
|
||||||
<string name="message_details_header__sent">Poslano</string>
|
|
||||||
<string name="message_details_header__received">Primljeno</string>
|
|
||||||
<string name="message_details_header__disappears">Nestaje</string>
|
|
||||||
<string name="message_details_header__via">Putem</string>
|
|
||||||
<string name="message_details_header__to">Prima:</string>
|
|
||||||
<string name="message_details_header__from">Šalje:</string>
|
|
||||||
<string name="message_details_header__with">Sa:</string>
|
|
||||||
<!-- AndroidManifest.xml -->
|
|
||||||
<string name="AndroidManifest__create_passphrase">Stvorite lozinku</string>
|
|
||||||
<string name="AndroidManifest__select_contacts">Odaberite kontakte</string>
|
|
||||||
<string name="AndroidManifest__media_preview">Prikaz medija</string>
|
|
||||||
<!-- arrays.xml -->
|
|
||||||
<string name="arrays__use_default">Koristi zadano</string>
|
|
||||||
<string name="arrays__use_custom">Koristi prilagođeno</string>
|
|
||||||
<string name="arrays__mute_for_one_hour">Utišaj na 1 sat</string>
|
|
||||||
<string name="arrays__mute_for_two_hours">Utišaj na 2 sata</string>
|
|
||||||
<string name="arrays__mute_for_one_day">Utišaj na 1 dan</string>
|
|
||||||
<string name="arrays__mute_for_seven_days">Utišaj na 7 dana</string>
|
|
||||||
<string name="arrays__mute_for_one_year">Utišaj na 1 godinu</string>
|
|
||||||
<string name="arrays__settings_default">Zadane postavke</string>
|
|
||||||
<string name="arrays__enabled">Omogućeno</string>
|
|
||||||
<string name="arrays__disabled">Onemogućeno</string>
|
|
||||||
<string name="arrays__name_and_message">Ime i poruka</string>
|
|
||||||
<string name="arrays__name_only">Samo ime</string>
|
|
||||||
<string name="arrays__no_name_or_message">Nema imena ili poruke</string>
|
|
||||||
<string name="arrays__images">Slike</string>
|
|
||||||
<string name="arrays__audio">Audio</string>
|
|
||||||
<string name="arrays__video">Video</string>
|
|
||||||
<string name="arrays__documents">Dokumenti</string>
|
|
||||||
<string name="arrays__small">Mala</string>
|
|
||||||
<string name="arrays__normal">Normalna</string>
|
|
||||||
<string name="arrays__large">Velika</string>
|
|
||||||
<string name="arrays__extra_large">Ekstra velika</string>
|
|
||||||
<string name="arrays__default">Zadano</string>
|
|
||||||
<string name="arrays__high">Visoko</string>
|
|
||||||
<string name="arrays__max">Maks</string>
|
|
||||||
<!-- plurals.xml -->
|
|
||||||
<plurals name="hours_ago">
|
|
||||||
<item quantity="one">%d sat</item>
|
|
||||||
<item quantity="few">%d sata</item>
|
|
||||||
<item quantity="many">%d sati</item>
|
|
||||||
<item quantity="other">%d sati</item>
|
|
||||||
</plurals>
|
|
||||||
<!-- preferences.xml -->
|
|
||||||
<string name="preferences__pref_enter_sends_title">Enter šalje poruku</string>
|
|
||||||
<string name="preferences__pressing_the_enter_key_will_send_text_messages">Pritisak na Enter će poslati tekst poruku</string>
|
|
||||||
<string name="preferences__send_link_previews">Send link previews</string>
|
|
||||||
<string name="preferences__previews_are_supported_for">Previews are supported for Imgur, Instagram, Pinterest, Reddit, and YouTube links</string>
|
|
||||||
<string name="preferences__screen_security">Sigurnost ekrana</string>
|
|
||||||
<string name="preferences__disable_screen_security_to_allow_screen_shots">Onemogući snimanje ekrana na popisu nedavnih i unutar aplikacije</string>
|
|
||||||
<string name="preferences__notifications">Obavijesti</string>
|
|
||||||
<string name="preferences__led_color">LED boja</string>
|
|
||||||
<string name="preferences__led_color_unknown">Nepoznato</string>
|
|
||||||
<string name="preferences__pref_led_blink_title">LED uzorak treptanja</string>
|
|
||||||
<string name="preferences__sound">Zvuk</string>
|
|
||||||
<string name="preferences__silent">Bezvučno</string>
|
|
||||||
<string name="preferences__repeat_alerts">Ponovi upozorenja</string>
|
|
||||||
<string name="preferences__never">Nikada</string>
|
|
||||||
<string name="preferences__one_time">Jednom</string>
|
|
||||||
<string name="preferences__two_times">Dva puta</string>
|
|
||||||
<string name="preferences__three_times">Tri puta</string>
|
|
||||||
<string name="preferences__five_times">Pet puta</string>
|
|
||||||
<string name="preferences__ten_times">Deset puta</string>
|
|
||||||
<string name="preferences__vibrate">Vibracija</string>
|
|
||||||
<string name="preferences__green">Zelena</string>
|
|
||||||
<string name="preferences__red">Crvena</string>
|
|
||||||
<string name="preferences__blue">Plava</string>
|
|
||||||
<string name="preferences__orange">Narančasta</string>
|
|
||||||
<string name="preferences__cyan">Cijan</string>
|
|
||||||
<string name="preferences__magenta">Magenta</string>
|
|
||||||
<string name="preferences__white">Bijela</string>
|
|
||||||
<string name="preferences__none">Niti jedna</string>
|
|
||||||
<string name="preferences__fast">Brzo</string>
|
|
||||||
<string name="preferences__normal">Normalno</string>
|
|
||||||
<string name="preferences__slow">Sporo</string>
|
|
||||||
<string name="preferences__automatically_delete_older_messages_once_a_conversation_exceeds_a_specified_length">Automatski obriši starije poruke nakon što razgovor pređe određenu duljinu</string>
|
|
||||||
<string name="preferences__delete_old_messages">Obriši stare poruke</string>
|
|
||||||
<string name="preferences__conversation_length_limit">Maksimalna duljina razgovora</string>
|
|
||||||
<string name="preferences__trim_all_conversations_now">Skrati sve razgovore odmah</string>
|
|
||||||
<string name="preferences__scan_through_all_conversations_and_enforce_conversation_length_limits">Skeniraj sve razgovore i primijeni ograničenje duljine razgovora</string>
|
|
||||||
<string name="preferences__default">Zadano</string>
|
|
||||||
<string name="preferences__incognito_keyboard">Inkognito tipkovnica</string>
|
|
||||||
<string name="preferences__read_receipts">Potvrde čitanja</string>
|
|
||||||
<string name="preferences__if_read_receipts_are_disabled_you_wont_be_able_to_see_read_receipts">Ukoliko ste onemogućili potvrdu čitanja kod sebe, nećete moći vidjeti potvrde čitanja od drugih.</string>
|
|
||||||
<string name="preferences__typing_indicators">Pokazatelji tipkanja</string>
|
|
||||||
<string name="preferences__if_typing_indicators_are_disabled_you_wont_be_able_to_see_typing_indicators">If typing indicators are disabled, you won\'t be able to see typing indicators from others.</string>
|
|
||||||
<string name="preferences__request_keyboard_to_disable_personalized_learning">Zahtijevaj od tipkovnice da onemogući personalizirano učenje</string>
|
|
||||||
<string name="preferences__light_theme">Svijetla</string>
|
|
||||||
<string name="preferences__dark_theme">Tamna</string>
|
|
||||||
<string name="preferences_chats__message_trimming">Skraćivanje poruke</string>
|
|
||||||
<string name="preferences_advanced__use_system_emoji">Koristi emotikone sustava</string>
|
|
||||||
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Onemogući ugrađenu Session podršku za emotikone</string>
|
|
||||||
<string name="preferences_app_protection__app_access">Pristup aplikaciji</string>
|
|
||||||
<string name="preferences_app_protection__communication">Komunikacija</string>
|
|
||||||
<string name="preferences_chats__chats">Razgovori</string>
|
|
||||||
<string name="preferences_notifications__messages">Poruke</string>
|
|
||||||
<string name="preferences_notifications__in_chat_sounds">Zvuk unutar razgovora</string>
|
|
||||||
<string name="preferences_notifications__show">Prikaži</string>
|
|
||||||
<string name="preferences_notifications__priority">Prioritet</string>
|
|
||||||
<!-- **************************************** -->
|
|
||||||
<!-- menus -->
|
|
||||||
<!-- **************************************** -->
|
|
||||||
<!-- contact_selection_list -->
|
|
||||||
<string name="contact_selection_list__unknown_contact">Nova poruka za...</string>
|
|
||||||
<!-- conversation_context -->
|
|
||||||
<string name="conversation_context__menu_message_details">Detalji poruke</string>
|
|
||||||
<string name="conversation_context__menu_copy_text">Kopiraj tekst</string>
|
|
||||||
<string name="conversation_context__menu_delete_message">Obriši poruku</string>
|
|
||||||
<string name="conversation_context__menu_ban_user">Ban user</string>
|
|
||||||
<string name="conversation_context__menu_resend_message">Ponovno pošalji poruku</string>
|
|
||||||
<string name="conversation_context__menu_reply_to_message">Reply to message</string>
|
|
||||||
<!-- conversation_context_image -->
|
|
||||||
<string name="conversation_context_image__save_attachment">Spremi privitak</string>
|
|
||||||
<!-- conversation_expiring_off -->
|
|
||||||
<string name="conversation_expiring_off__disappearing_messages">Nestajuće poruke</string>
|
|
||||||
<!-- conversation_expiring_on -->
|
|
||||||
<string name="menu_conversation_expiring_on__messages_expiring">Poruke ističu</string>
|
|
||||||
<!-- conversation_muted -->
|
|
||||||
<string name="conversation_muted__unmute">Ukloni utišanje</string>
|
|
||||||
<!-- conversation_unmuted -->
|
|
||||||
<string name="conversation_unmuted__mute_notifications">Utišaj obavijesti</string>
|
|
||||||
<!-- conversation -->
|
|
||||||
<string name="conversation__menu_edit_group">Uredi grupu</string>
|
|
||||||
<string name="conversation__menu_leave_group">Napusti grupu</string>
|
|
||||||
<string name="conversation__menu_view_all_media">Svi mediji</string>
|
|
||||||
<string name="conversation__menu_add_shortcut">Add to home screen</string>
|
|
||||||
<!-- conversation_popup -->
|
|
||||||
<string name="conversation_popup__menu_expand_popup">Proširi skočni prozor</string>
|
|
||||||
<!-- conversation_group_options -->
|
|
||||||
<string name="conversation_group_options__delivery">Isporuka</string>
|
|
||||||
<string name="conversation_group_options__conversation">Razgovor</string>
|
|
||||||
<string name="conversation_group_options__broadcast">Emitiranje</string>
|
|
||||||
<!-- media_preview -->
|
|
||||||
<string name="media_preview__save_title">Spremi</string>
|
|
||||||
<string name="media_preview__forward_title">Naprijed</string>
|
|
||||||
<string name="media_preview__all_media_title">Svi mediji</string>
|
|
||||||
<!-- media_overview -->
|
|
||||||
<string name="media_overview_documents_fragment__no_documents_found">Nema dokumenata</string>
|
|
||||||
<!-- media_preview_activity -->
|
|
||||||
<string name="media_preview_activity__media_content_description">Prikaz medija</string>
|
|
||||||
<!-- Trimmer -->
|
|
||||||
<string name="trimmer__deleting">Brisanje</string>
|
|
||||||
<string name="trimmer__deleting_old_messages">Brisanje starih poruka...</string>
|
|
||||||
<string name="trimmer__old_messages_successfully_deleted">Stare poruke su uspješno obrisane</string>
|
|
||||||
<!-- transport_selection_list_item -->
|
|
||||||
<string name="Permissions_permission_required">Permission required</string>
|
|
||||||
<string name="Permissions_continue">Nastavi</string>
|
|
||||||
<string name="Permissions_not_now">Not now</string>
|
|
||||||
<string name="backup_enable_dialog__backups_will_be_saved_to_external_storage_and_encrypted_with_the_passphrase_below_you_must_have_this_passphrase_in_order_to_restore_a_backup">Backups will be saved to external storage and encrypted with the passphrase below. You must have this passphrase in order to restore a backup.</string>
|
|
||||||
<string name="backup_enable_dialog__i_have_written_down_this_passphrase">I have written down this passphrase. Without it, I will be unable to restore a backup.</string>
|
|
||||||
<string name="registration_activity__skip">Preskoči</string>
|
|
||||||
<string name="RegistrationActivity_backup_failure_downgrade">Cannot import backups from newer versions of Session</string>
|
|
||||||
<string name="RegistrationActivity_incorrect_backup_passphrase">Incorrect backup passphrase</string>
|
|
||||||
<string name="BackupDialog_enable_local_backups">Enable local backups?</string>
|
|
||||||
<string name="BackupDialog_enable_backups">Enable backups</string>
|
|
||||||
<string name="BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box">Please acknowledge your understanding by marking the confirmation check box.</string>
|
|
||||||
<string name="BackupDialog_delete_backups">Delete backups?</string>
|
|
||||||
<string name="BackupDialog_disable_and_delete_all_local_backups">Disable and delete all local backups?</string>
|
|
||||||
<string name="BackupDialog_delete_backups_statement">Delete backups</string>
|
|
||||||
<string name="BackupDialog_copied_to_clipboard">Kopirano u međuspremnik</string>
|
|
||||||
<string name="LocalBackupJob_creating_backup">Stvaranje sigurnosne kopije...</string>
|
|
||||||
<string name="ProgressPreference_d_messages_so_far">%d messages so far</string>
|
|
||||||
<string name="BackupUtil_never">Nikada</string>
|
|
||||||
<string name="preferences_app_protection__screen_lock">Screen lock</string>
|
|
||||||
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Lock Session access with Android screen lock or fingerprint</string>
|
|
||||||
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Screen lock inactivity timeout</string>
|
|
||||||
<string name="AppProtectionPreferenceFragment_none">Niti jedna</string>
|
|
||||||
<!-- Conversation activity -->
|
|
||||||
<string name="activity_conversation_copy_public_key_button_title">Copy public key</string>
|
|
||||||
<!-- Session -->
|
|
||||||
<string name="continue_2">Continue</string>
|
|
||||||
<string name="copy">Copy</string>
|
|
||||||
<string name="invalid_url">Invalid URL</string>
|
|
||||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
|
||||||
<string name="next">Next</string>
|
|
||||||
<string name="share">Share</string>
|
|
||||||
<string name="invalid_session_id">Invalid Session ID</string>
|
|
||||||
<string name="cancel">Cancel</string>
|
|
||||||
<string name="your_session_id">Your Session ID</string>
|
|
||||||
<string name="activity_landing_title_2">Your Session begins here...</string>
|
|
||||||
<string name="activity_landing_register_button_title">Create Session ID</string>
|
|
||||||
<string name="activity_landing_restore_button_title">Continue Your Session</string>
|
|
||||||
<string name="view_fake_chat_bubble_1">What\'s Session?</string>
|
|
||||||
<string name="view_fake_chat_bubble_2">It\'s a decentralized, encrypted messaging app</string>
|
|
||||||
<string name="view_fake_chat_bubble_3">So it doesn\'t collect my personal information or my conversation metadata? How does it work?</string>
|
|
||||||
<string name="view_fake_chat_bubble_4">Using a combination of advanced anonymous routing and end-to-end encryption technologies.</string>
|
|
||||||
<string name="view_fake_chat_bubble_5">Friends don\'t let friends use compromised messengers. You\'re welcome.</string>
|
|
||||||
<string name="activity_register_title">Say hello to your Session ID</string>
|
|
||||||
<string name="activity_register_explanation">Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design.</string>
|
|
||||||
<string name="activity_restore_title">Restore your account</string>
|
|
||||||
<string name="activity_restore_explanation">Enter the recovery phrase that was given to you when you signed up to restore your account.</string>
|
|
||||||
<string name="activity_restore_seed_edit_text_hint">Enter your recovery phrase</string>
|
|
||||||
<string name="activity_display_name_title_2">Pick your display name</string>
|
|
||||||
<string name="activity_display_name_explanation">This will be your name when you use Session. It can be your real name, an alias, or anything else you like.</string>
|
|
||||||
<string name="activity_display_name_edit_text_hint">Enter a display name</string>
|
|
||||||
<string name="activity_display_name_display_name_missing_error">Please pick a display name</string>
|
|
||||||
<string name="activity_display_name_display_name_too_long_error">Please pick a shorter display name</string>
|
|
||||||
<string name="activity_pn_mode_recommended_option_tag">Recommended</string>
|
|
||||||
<string name="activity_pn_mode_no_option_picked_dialog_title">Please Pick an Option</string>
|
|
||||||
<string name="activity_home_empty_state_message">You don\'t have any contacts yet</string>
|
|
||||||
<string name="activity_home_empty_state_button_title">Start a Session</string>
|
|
||||||
<string name="activity_home_leave_group_dialog_message">Are you sure you want to leave this group?</string>
|
|
||||||
<string name="activity_home_leaving_group_failed_message">"Couldn't leave group"</string>
|
|
||||||
<string name="activity_home_delete_conversation_dialog_message">Are you sure you want to delete this conversation?</string>
|
|
||||||
<string name="activity_home_conversation_deleted_message">Conversation deleted</string>
|
|
||||||
<string name="activity_seed_title">Your Recovery Phrase</string>
|
|
||||||
<string name="activity_seed_title_2">Meet your recovery phrase</string>
|
|
||||||
<string name="activity_seed_explanation">Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don\'t give it to anyone.</string>
|
|
||||||
<string name="activity_seed_reveal_button_title">Hold to reveal</string>
|
|
||||||
<string name="view_seed_reminder_subtitle_1">Secure your account by saving your recovery phrase</string>
|
|
||||||
<string name="view_seed_reminder_subtitle_2">Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID.</string>
|
|
||||||
<string name="view_seed_reminder_subtitle_3">Make sure to store your recovery phrase in a safe place</string>
|
|
||||||
<string name="activity_path_title">Path</string>
|
|
||||||
<string name="activity_path_explanation">Session hides your IP by bouncing your messages through several Service Nodes in Session\'s decentralized network. These are the countries your connection is currently being bounced through:</string>
|
|
||||||
<string name="activity_path_device_row_title">You</string>
|
|
||||||
<string name="activity_path_guard_node_row_title">Entry Node</string>
|
|
||||||
<string name="activity_path_service_node_row_title">Service Node</string>
|
|
||||||
<string name="activity_path_destination_row_title">Destination</string>
|
|
||||||
<string name="activity_path_learn_more_button_title">Learn More</string>
|
|
||||||
<string name="activity_create_private_chat_title">New Session</string>
|
|
||||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Enter Session ID</string>
|
|
||||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scan QR Code</string>
|
|
||||||
<string name="activity_create_private_chat_scan_qr_code_explanation">Scan a user\'s QR code to start a session. QR codes can be found by tapping the QR code icon in account settings.</string>
|
|
||||||
<string name="fragment_enter_public_key_edit_text_hint">Enter Session ID of recipient</string>
|
|
||||||
<string name="fragment_enter_public_key_explanation">Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code.</string>
|
|
||||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session needs camera access to scan QR codes</string>
|
|
||||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Grant Camera Access</string>
|
|
||||||
<string name="activity_create_closed_group_title">New Closed Group</string>
|
|
||||||
<string name="activity_create_closed_group_edit_text_hint">Enter a group name</string>
|
|
||||||
<string name="activity_create_closed_group_empty_state_message">You don\'t have any contacts yet</string>
|
|
||||||
<string name="activity_create_closed_group_empty_state_button_title">Start a Session</string>
|
|
||||||
<string name="activity_create_closed_group_group_name_missing_error">Please enter a group name</string>
|
|
||||||
<string name="activity_create_closed_group_group_name_too_long_error">Please enter a shorter group name</string>
|
|
||||||
<string name="activity_create_closed_group_not_enough_group_members_error">Please pick at least 1 group member</string>
|
|
||||||
<string name="activity_create_closed_group_too_many_group_members_error">A closed group cannot have more than 100 members</string>
|
|
||||||
<string name="activity_join_public_chat_title">Join Open Group</string>
|
|
||||||
<string name="activity_join_public_chat_error">Couldn\'t join group</string>
|
|
||||||
<string name="activity_join_public_chat_enter_group_url_tab_title">Open Group URL</string>
|
|
||||||
<string name="activity_join_public_chat_scan_qr_code_tab_title">Scan QR Code</string>
|
|
||||||
<string name="activity_join_public_chat_scan_qr_code_explanation">Scan the QR code of the open group you\'d like to join</string>
|
|
||||||
<string name="fragment_enter_chat_url_edit_text_hint">Enter an open group URL</string>
|
|
||||||
<string name="activity_settings_title">Settings</string>
|
|
||||||
<string name="activity_settings_display_name_edit_text_hint">Enter a display name</string>
|
|
||||||
<string name="activity_settings_display_name_missing_error">Please pick a display name</string>
|
|
||||||
<string name="activity_settings_display_name_too_long_error">Please pick a shorter display name</string>
|
|
||||||
<string name="activity_settings_privacy_button_title">Privacy</string>
|
|
||||||
<string name="activity_settings_notifications_button_title">Notifications</string>
|
|
||||||
<string name="activity_settings_chats_button_title">Chats</string>
|
|
||||||
<string name="activity_settings_devices_button_title">Devices</string>
|
|
||||||
<string name="activity_settings_invite_button_title">Invite</string>
|
|
||||||
<string name="activity_settings_recovery_phrase_button_title">Recovery Phrase</string>
|
|
||||||
<string name="activity_settings_clear_all_data_button_title">Clear Data</string>
|
|
||||||
<string name="activity_notification_settings_title">Notifications</string>
|
|
||||||
<string name="activity_notification_settings_style_section_title">Notification Style</string>
|
|
||||||
<string name="activity_notification_settings_content_section_title">Notification Content</string>
|
|
||||||
<string name="activity_privacy_settings_title">Privacy</string>
|
|
||||||
<string name="activity_chat_settings_title">Chats</string>
|
|
||||||
<string name="preferences_notifications_strategy_category_title">Notification Strategy</string>
|
|
||||||
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Change name</string>
|
|
||||||
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Unlink device</string>
|
|
||||||
<string name="dialog_seed_title">Your Recovery Phrase</string>
|
|
||||||
<string name="dialog_seed_explanation">This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device.</string>
|
|
||||||
<string name="dialog_clear_all_data_title">Clear All Data</string>
|
|
||||||
<string name="dialog_clear_all_data_explanation">This will permanently delete your messages, sessions, and contacts.</string>
|
|
||||||
<string name="activity_qr_code_title">QR Code</string>
|
|
||||||
<string name="activity_qr_code_view_my_qr_code_tab_title">View My QR Code</string>
|
|
||||||
<string name="activity_qr_code_view_scan_qr_code_tab_title">Scan QR Code</string>
|
|
||||||
<string name="activity_qr_code_view_scan_qr_code_explanation">Scan someone\'s QR code to start a conversation with them</string>
|
|
||||||
<string name="fragment_view_my_qr_code_explanation">This is your QR code. Other users can scan it to start a session with you.</string>
|
|
||||||
<string name="fragment_view_my_qr_code_share_title">Share QR Code</string>
|
|
||||||
<string name="fragment_contact_selection_contacts_title">Contacts</string>
|
|
||||||
<string name="fragment_contact_selection_closed_groups_title">Closed Groups</string>
|
|
||||||
<string name="fragment_contact_selection_open_groups_title">Open Groups</string>
|
|
||||||
<!-- Next round of translation -->
|
|
||||||
<string name="menu_apply_button">Apply</string>
|
|
||||||
<string name="menu_done_button">Done</string>
|
|
||||||
<string name="activity_edit_closed_group_title">Edit Group</string>
|
|
||||||
<string name="activity_edit_closed_group_edit_text_hint">Enter a new group name</string>
|
|
||||||
<string name="activity_edit_closed_group_edit_members">Members</string>
|
|
||||||
<string name="activity_edit_closed_group_add_members">Add members</string>
|
|
||||||
<string name="activity_edit_closed_group_group_name_missing_error">Group name can\'t be empty</string>
|
|
||||||
<string name="activity_edit_closed_group_group_name_too_long_error">Please enter a shorter group name</string>
|
|
||||||
<string name="activity_edit_closed_group_not_enough_group_members_error">Groups must have at least 1 group member</string>
|
|
||||||
<string name="fragment_edit_group_bottom_sheet_remove">Remove user from group</string>
|
|
||||||
<string name="activity_select_contacts_title">Select Contacts</string>
|
|
||||||
<string name="view_reset_secure_session_done_message">Secure session reset done</string>
|
|
||||||
<string name="dialog_ui_mode_title">Theme</string>
|
|
||||||
<string name="dialog_ui_mode_option_day">Day</string>
|
|
||||||
<string name="dialog_ui_mode_option_night">Night</string>
|
|
||||||
<string name="dialog_ui_mode_option_system_default">System default</string>
|
|
||||||
<string name="activity_conversation_menu_copy_session_id">Copy Session ID</string>
|
|
||||||
<string name="attachment">Attachment</string>
|
|
||||||
<string name="attachment_type_voice_message">Voice Message</string>
|
|
||||||
<string name="details">Details</string>
|
|
||||||
<string name="dialog_backup_activation_failed">Failed to activate backups. Please try again or contact support.</string>
|
|
||||||
<string name="activity_backup_restore_title">Restore backup</string>
|
|
||||||
<string name="activity_backup_restore_select_file">Select a file</string>
|
|
||||||
<string name="activity_backup_restore_explanation_1">Select a backup file and enter the passphrase it was created with.</string>
|
|
||||||
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
|
||||||
<!-- LinkDeviceActivity -->
|
|
||||||
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
|
||||||
<string name="activity_join_public_chat_join_rooms">Or join one of these…</string>
|
|
||||||
</resources>
|
|
@ -28,7 +28,7 @@ dependencies {
|
|||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
implementation 'com.annimon:stream:1.1.8'
|
implementation 'com.annimon:stream:1.1.8'
|
||||||
implementation 'com.makeramen:roundedimageview:2.1.0'
|
implementation 'com.makeramen:roundedimageview:2.1.0'
|
||||||
implementation 'com.esotericsoftware:kryo:4.0.1'
|
implementation 'com.esotericsoftware:kryo:5.1.1'
|
||||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||||
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"
|
||||||
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
implementation "org.whispersystems:curve25519-java:$curve25519Version"
|
||||||
|
@ -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.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
@ -10,7 +9,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
|
||||||
@ -19,6 +17,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
|
||||||
@ -33,10 +32,8 @@ 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(newProfilePicture: String)
|
||||||
|
// Signal
|
||||||
// Signal Protocol
|
|
||||||
|
|
||||||
fun getOrGenerateRegistrationID(): Int
|
fun getOrGenerateRegistrationID(): Int
|
||||||
|
|
||||||
// Jobs
|
// Jobs
|
||||||
@ -56,48 +53,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)
|
||||||
@ -107,11 +96,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)
|
||||||
@ -119,9 +107,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?
|
||||||
@ -135,58 +123,29 @@ 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
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
// Session Contact (Loki User)
|
|
||||||
fun getDisplayName(publicKey: String): String?
|
fun getDisplayName(publicKey: String): String?
|
||||||
fun getProfilePictureURL(publicKey: String): String?
|
fun getProfilePictureURL(publicKey: String): String?
|
||||||
fun getContactWithSessionID(sessionID: String): Contact?
|
fun getContactWithSessionID(sessionID: String): Contact?
|
||||||
fun getAllContacts(): Set<Contact>
|
fun getAllContacts(): Set<Contact>
|
||||||
fun setContact(contact: Contact)
|
fun setContact(contact: Contact)
|
||||||
|
|
||||||
// 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?
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import org.session.libsession.database.MessageDataProvider
|
|||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
|
|
||||||
class MessagingModuleConfiguration(
|
class MessagingModuleConfiguration(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val storage: StorageProtocol,
|
val storage: StorageProtocol,
|
||||||
val messageDataProvider: MessageDataProvider
|
val messageDataProvider: MessageDataProvider
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -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)
|
||||||
@ -158,7 +148,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
.putString(THREAD_ID_KEY, threadID)
|
.putString(THREAD_ID_KEY, threadID)
|
||||||
.putByteArray(MESSAGE_KEY, serializedMessage)
|
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||||
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
|
.putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID)
|
||||||
.build();
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFactoryKey(): String {
|
override fun getFactoryKey(): String {
|
||||||
@ -172,7 +162,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val input = Input(serializedMessage)
|
val input = Input(serializedMessage)
|
||||||
val message: Message = kryo.readObject(input, Message::class.java)
|
val message = kryo.readObject(input, Message::class.java)
|
||||||
input.close()
|
input.close()
|
||||||
return AttachmentUploadJob(
|
return AttachmentUploadJob(
|
||||||
data.getLong(ATTACHMENT_ID_KEY),
|
data.getLong(ATTACHMENT_ID_KEY),
|
||||||
|
@ -88,17 +88,16 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val output = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
|
||||||
// Message
|
// Message
|
||||||
kryo.writeClassAndObject(output, message)
|
val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
||||||
output.close()
|
kryo.writeClassAndObject(messageOutput, message)
|
||||||
val serializedMessage = output.toBytes()
|
messageOutput.close()
|
||||||
output.clear()
|
val serializedMessage = messageOutput.toBytes()
|
||||||
// Destination
|
// Destination
|
||||||
kryo.writeClassAndObject(output, destination)
|
val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE)
|
||||||
output.close()
|
kryo.writeClassAndObject(destinationOutput, destination)
|
||||||
val serializedDestination = output.toBytes()
|
destinationOutput.close()
|
||||||
output.clear()
|
val serializedDestination = destinationOutput.toBytes()
|
||||||
// Serialize
|
// Serialize
|
||||||
return Data.Builder()
|
return Data.Builder()
|
||||||
.putByteArray(MESSAGE_KEY, serializedMessage)
|
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||||
@ -116,6 +115,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
val serializedMessage = data.getByteArray(MESSAGE_KEY)
|
||||||
val serializedDestination = data.getByteArray(DESTINATION_KEY)
|
val serializedDestination = data.getByteArray(DESTINATION_KEY)
|
||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
|
kryo.isRegistrationRequired = false
|
||||||
// Message
|
// Message
|
||||||
val messageInput = Input(serializedMessage)
|
val messageInput = Input(serializedMessage)
|
||||||
val message: Message
|
val message: Message
|
||||||
|
@ -67,7 +67,9 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
val output = Output(serializedMessage)
|
val output = Output(serializedMessage)
|
||||||
kryo.writeObject(output, message)
|
kryo.writeObject(output, message)
|
||||||
output.close()
|
output.close()
|
||||||
return Data.Builder().putByteArray(MESSAGE_KEY, serializedMessage).build();
|
return Data.Builder()
|
||||||
|
.putByteArray(MESSAGE_KEY, serializedMessage)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFactoryKey(): String {
|
override fun getFactoryKey(): String {
|
||||||
@ -81,7 +83,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
val kryo = Kryo()
|
val kryo = Kryo()
|
||||||
kryo.isRegistrationRequired = false
|
kryo.isRegistrationRequired = false
|
||||||
val input = Input(serializedMessage)
|
val input = Input(serializedMessage)
|
||||||
val message: SnodeMessage = kryo.readObject(input, SnodeMessage::class.java)
|
val message = kryo.readObject(input, SnodeMessage::class.java)
|
||||||
input.close()
|
input.close()
|
||||||
return NotifyPNServerJob(message)
|
return NotifyPNServerJob(message)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
@ -203,27 +204,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 +255,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())
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
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.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
@ -71,6 +72,8 @@ fun MessageSender.create(name: String, members: Collection<String>): Promise<Str
|
|||||||
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTime)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||||
|
// Start polling
|
||||||
|
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||||
// Fulfill the promise
|
// Fulfill the promise
|
||||||
deferred.resolve(groupID)
|
deferred.resolve(groupID)
|
||||||
}
|
}
|
||||||
@ -189,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
|
||||||
|
@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.PointerAtt
|
|||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
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
|
||||||
@ -68,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,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)
|
||||||
}
|
}
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
|
||||||
@ -139,9 +139,8 @@ 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)
|
||||||
profileManager.setProfileKey(context, recipient, message.profileKey)
|
profileManager.setProfileKey(context, recipient, message.profileKey)
|
||||||
// handle profile photo
|
|
||||||
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) {
|
||||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage.addContacts(message.contacts)
|
storage.addContacts(message.contacts)
|
||||||
@ -162,12 +161,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!!
|
||||||
@ -291,6 +287,8 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
|
|||||||
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
|
||||||
|
// Start polling
|
||||||
|
ClosedGroupPollerV2.shared.startPolling(groupPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
|
||||||
@ -477,8 +475,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
|
||||||
@ -530,8 +528,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) {
|
||||||
@ -567,5 +565,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
|
|||||||
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||||
// Notify the PN server
|
// Notify the PN server
|
||||||
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||||
|
// Stop polling
|
||||||
|
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
package org.session.libsession.messaging.sending_receiving.pollers
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import nl.komponents.kovenant.Promise
|
|
||||||
import nl.komponents.kovenant.functional.bind
|
|
||||||
import nl.komponents.kovenant.functional.map
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
import org.session.libsignal.crypto.getRandomElementOrNull
|
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.successBackground
|
|
||||||
|
|
||||||
class ClosedGroupPoller {
|
|
||||||
private var isPolling = false
|
|
||||||
private val handler: Handler by lazy { Handler() }
|
|
||||||
|
|
||||||
private val task = object : Runnable {
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
poll()
|
|
||||||
handler.postDelayed(this, pollInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Settings
|
|
||||||
companion object {
|
|
||||||
private val pollInterval: Long = 6 * 1000
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Error
|
|
||||||
class InsufficientSnodesException() : Exception("No snodes left to poll.")
|
|
||||||
class PollingCanceledException() : Exception("Polling canceled.")
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Public API
|
|
||||||
public fun startIfNeeded() {
|
|
||||||
if (isPolling) { return }
|
|
||||||
isPolling = true
|
|
||||||
task.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun pollOnce(): List<Promise<Unit, Exception>> {
|
|
||||||
if (isPolling) { return listOf() }
|
|
||||||
isPolling = true
|
|
||||||
return poll()
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun stopIfNeeded() {
|
|
||||||
isPolling = false
|
|
||||||
handler.removeCallbacks(task)
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Private API
|
|
||||||
private fun poll(): List<Promise<Unit, Exception>> {
|
|
||||||
if (!isPolling) { return listOf() }
|
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val publicKeys = storage.getAllActiveClosedGroupPublicKeys()
|
|
||||||
return publicKeys.map { publicKey ->
|
|
||||||
val promise = SnodeAPI.getSwarm(publicKey).bind { swarm ->
|
|
||||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
|
||||||
if (!isPolling) { throw PollingCanceledException() }
|
|
||||||
SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) }
|
|
||||||
}
|
|
||||||
promise.successBackground { messages ->
|
|
||||||
if (!storage.isGroupActive(publicKey)) { return@successBackground }
|
|
||||||
messages.forEach { envelope ->
|
|
||||||
val job = MessageReceiveJob(envelope.toByteArray())
|
|
||||||
JobQueue.shared.add(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
promise.fail {
|
|
||||||
Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.")
|
|
||||||
}
|
|
||||||
promise.map { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
@ -0,0 +1,115 @@
|
|||||||
|
package org.session.libsession.messaging.sending_receiving.pollers
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.bind
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.crypto.getRandomElementOrNull
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class ClosedGroupPollerV2 {
|
||||||
|
private val executorService = Executors.newScheduledThreadPool(4)
|
||||||
|
private var isPolling = mutableMapOf<String, Boolean>()
|
||||||
|
private var futures = mutableMapOf<String, ScheduledFuture<*>>()
|
||||||
|
|
||||||
|
private fun isPolling(groupPublicKey: String): Boolean {
|
||||||
|
return isPolling[groupPublicKey] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val minPollInterval = 4 * 1000
|
||||||
|
private val maxPollInterval = 2 * 60 * 1000
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
val shared = ClosedGroupPollerV2()
|
||||||
|
}
|
||||||
|
|
||||||
|
class InsufficientSnodesException() : Exception("No snodes left to poll.")
|
||||||
|
class PollingCanceledException() : Exception("Polling canceled.")
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
|
allGroupPublicKeys.forEach { startPolling(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startPolling(groupPublicKey: String) {
|
||||||
|
if (isPolling(groupPublicKey)) { return }
|
||||||
|
setUpPolling(groupPublicKey)
|
||||||
|
isPolling[groupPublicKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
|
||||||
|
allGroupPublicKeys.forEach { stopPolling(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopPolling(groupPublicKey: String) {
|
||||||
|
futures[groupPublicKey]?.cancel(false)
|
||||||
|
isPolling[groupPublicKey] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpPolling(groupPublicKey: String) {
|
||||||
|
poll(groupPublicKey).success {
|
||||||
|
pollRecursively(groupPublicKey)
|
||||||
|
}.fail {
|
||||||
|
// The error is logged in poll(_:)
|
||||||
|
pollRecursively(groupPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pollRecursively(groupPublicKey: String) {
|
||||||
|
if (!isPolling(groupPublicKey)) { return }
|
||||||
|
// Get the received date of the last message in the thread. If we don't have any messages yet, pick some
|
||||||
|
// reasonable fake time interval to use instead.
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||||
|
val threadID = storage.getThreadId(groupID) ?: return
|
||||||
|
val lastUpdated = storage.getLastUpdated(threadID)
|
||||||
|
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
|
||||||
|
val minPollInterval = Companion.minPollInterval
|
||||||
|
val limit: Long = 12 * 60 * 60 * 1000
|
||||||
|
val a = (Companion.maxPollInterval - minPollInterval).toDouble() / limit.toDouble()
|
||||||
|
val nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
|
||||||
|
Log.d("Loki", "Next poll interval for closed group with public key: $groupPublicKey is ${nextPollInterval / 1000} s.")
|
||||||
|
executorService?.schedule({
|
||||||
|
poll(groupPublicKey).success {
|
||||||
|
pollRecursively(groupPublicKey)
|
||||||
|
}.fail {
|
||||||
|
// The error is logged in poll(_:)
|
||||||
|
pollRecursively(groupPublicKey)
|
||||||
|
}
|
||||||
|
}, nextPollInterval.toLong(), TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun poll(groupPublicKey: String): Promise<Unit, Exception> {
|
||||||
|
if (!isPolling(groupPublicKey)) { return Promise.of(Unit) }
|
||||||
|
val promise = SnodeAPI.getSwarm(groupPublicKey).bind { swarm ->
|
||||||
|
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
||||||
|
if (!isPolling(groupPublicKey)) { throw PollingCanceledException() }
|
||||||
|
SnodeAPI.getRawMessages(snode, groupPublicKey).map { SnodeAPI.parseRawMessagesResponse(it, snode, groupPublicKey) }
|
||||||
|
}
|
||||||
|
promise.success { envelopes ->
|
||||||
|
if (!isPolling(groupPublicKey)) { return@success }
|
||||||
|
envelopes.forEach { envelope ->
|
||||||
|
val job = MessageReceiveJob(envelope.toByteArray())
|
||||||
|
JobQueue.shared.add(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
promise.fail {
|
||||||
|
Log.d("Loki", "Polling failed for closed group with public key: $groupPublicKey due to error: $it.")
|
||||||
|
}
|
||||||
|
return promise.map { }
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -77,7 +77,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)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,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)
|
|
||||||
fun getDisplayName(context: Context, recipient: Recipient): String?
|
fun getDisplayName(context: Context, recipient: Recipient): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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