Merge branch 'dev' of https://github.com/loki-project/session-android into polling-limit-after-inactivity

This commit is contained in:
Brice-W 2021-05-24 12:58:35 +10:00
commit becc3c7278
56 changed files with 263 additions and 2113 deletions

View File

@ -143,8 +143,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 170 def canonicalVersionCode = 171
def canonicalVersionName = "1.10.7" def canonicalVersionName = "1.10.8"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -22,19 +22,15 @@ import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.session.libsession.avatars.AvatarHelper; import org.session.libsession.avatars.AvatarHelper;
import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.MessagingModuleConfiguration;
import org.session.libsession.messaging.file_server.FileServerAPI;
import org.session.libsession.messaging.mentions.MentionsManager; import org.session.libsession.messaging.mentions.MentionsManager;
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2; import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2;
import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.messaging.sending_receiving.pollers.Poller;
@ -174,9 +170,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (userPublicKey != null) { if (userPublicKey != null) {
MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB); MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB);
} }
setUpStorageAPIIfNeeded();
resubmitProfilePictureIfNeeded(); resubmitProfilePictureIfNeeded();
updateOpenGroupProfilePicturesIfNeeded();
if (userPublicKey != null) { if (userPublicKey != null) {
registerForFCMIfNeeded(false); registerForFCMIfNeeded(false);
} }
@ -401,20 +395,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base))); super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
} }
private static class ProviderInitializationException extends RuntimeException { private static class ProviderInitializationException extends RuntimeException { }
}
// region Loki
public boolean setUpStorageAPIIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null || !IdentityKeyUtil.hasIdentityKey(this)) {
return false;
}
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
return true;
}
public void registerForFCMIfNeeded(final Boolean force) { public void registerForFCMIfNeeded(final Boolean force) {
if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return; if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return;
@ -490,19 +471,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}); });
} }
public void updateOpenGroupProfilePicturesIfNeeded() {
AsyncTask.execute(() -> {
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
String url = TextSecurePreferences.getProfilePictureURL(this);
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
for (String server : servers) {
if (profileKey != null) {
OpenGroupAPI.setProfilePicture(server, profileKey, url);
}
}
});
}
public void clearAllData(boolean isMigratingToV2KeyPair) { public void clearAllData(boolean isMigratingToV2KeyPair) {
String token = TextSecurePreferences.getFCMToken(this); String token = TextSecurePreferences.getFCMToken(this);
if (token != null && !token.isEmpty()) { if (token != null && !token.isEmpty()) {

View File

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

View File

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

View File

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

View File

@ -77,9 +77,7 @@ import androidx.core.view.MenuItemCompat;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@ -90,7 +88,6 @@ import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessa
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation; import org.session.libsession.messaging.messages.visible.OpenGroupInvitation;
import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.open_groups.OpenGroup;
import org.session.libsession.messaging.open_groups.OpenGroupV2; import org.session.libsession.messaging.open_groups.OpenGroupV2;
import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
@ -193,7 +190,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.PushCharacterCalculator; import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -204,7 +200,6 @@ import java.util.Locale;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import kotlin.Unit; import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -377,12 +372,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this); MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null) { if (openGroupV2 != null) {
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
} else if (openGroupV2 != null) {
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom()); PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen") if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen")
|| openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) { || openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) {
@ -1419,13 +1410,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) { public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null &&
publicChat.getChannel() == event.getChannel() &&
publicChat.getServer().equals(event.getUrl())) {
this.updateSubtitleTextView();
}
if (openGroup != null && if (openGroup != null &&
openGroup.getRoom().equals(event.getRoom()) && openGroup.getRoom().equals(event.getRoom()) &&
openGroup.getServer().equals(event.getUrl())) { openGroup.getServer().equals(event.getUrl())) {
@ -2380,13 +2365,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteIndicatorImageView.setVisibility(View.VISIBLE); muteIndicatorImageView.setVisibility(View.VISIBLE);
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())); subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) { } else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
if (publicChat != null) { if (openGroup != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
if (userCount == null) { userCount = 0; }
subtitleTextView.setText(userCount + " members");
} else if (openGroup != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer()); Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
if (userCount == null) { userCount = 0; } if (userCount == null) { userCount = 0; }
subtitleTextView.setText(userCount + " members"); subtitleTextView.setText(userCount + " members");

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import okhttp3.HttpUrl
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob
@ -14,7 +13,6 @@ import org.session.libsession.messaging.messages.signal.*
import org.session.libsession.messaging.messages.signal.IncomingTextMessage import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
@ -41,12 +39,12 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.api.OpenGroupManager import org.thoughtcrime.securesms.loki.api.OpenGroupManager
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.utilities.get import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getString import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.mms.PartAuthority
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
override fun getUserPublicKey(): String? { override fun getUserPublicKey(): String? {
return TextSecurePreferences.getLocalNumber(context) return TextSecurePreferences.getLocalNumber(context)
} }
@ -73,13 +71,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return TextSecurePreferences.getProfilePictureURL(context) return TextSecurePreferences.getProfilePictureURL(context)
} }
override fun setUserProfilePictureUrl(newProfilePicture: String) { override fun setUserProfilePictureURL(newValue: String) {
val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let { val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let {
Recipient.from(context, it, false) Recipient.from(context, it, false)
} }
TextSecurePreferences.setProfilePictureURL(context, newProfilePicture) TextSecurePreferences.setProfilePictureURL(context, newValue)
RetrieveProfileAvatarJob(ourRecipient, newProfilePicture) RetrieveProfileAvatarJob(ourRecipient, newValue)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture)) ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue))
} }
override fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? { override fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? {
@ -108,15 +106,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return registrationID return registrationID
} }
override fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> { override fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long> {
val database = DatabaseFactory.getAttachmentDatabase(context) val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() } val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
return database.insertAttachments(messageId, databaseAttachments) return database.insertAttachments(messageID, databaseAttachments)
} }
override fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment> { override fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment> {
val database = DatabaseFactory.getAttachmentDatabase(context) val database = DatabaseFactory.getAttachmentDatabase(context)
return database.getAttachmentsForMessage(messageId) return database.getAttachmentsForMessage(messageID)
} }
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? { override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
@ -186,7 +184,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return messageID return messageID
} }
// JOBS
override fun persistJob(job: Job) { override fun persistJob(job: Job) {
DatabaseFactory.getSessionJobDatabase(context).persistJob(job) DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
} }
@ -220,20 +217,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseFactory.getSessionJobDatabase(context).isJobCanceled(job) return DatabaseFactory.getSessionJobDatabase(context).isJobCanceled(job)
} }
// Authorization
override fun getAuthToken(server: String): String? {
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(server)
}
override fun setAuthToken(server: String, newValue: String?) {
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, newValue)
}
override fun removeAuthToken(server: String) {
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
}
override fun getAuthToken(room: String, server: String): String? { override fun getAuthToken(room: String, server: String): String? {
val id = "$server.$room" val id = "$server.$room"
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id) return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
@ -249,30 +232,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null) DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
} }
override fun getOpenGroup(threadID: String): OpenGroup? { override fun getV2OpenGroup(threadId: Long): OpenGroupV2? {
if (threadID.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadID)) { cursor ->
val publicChatAsJSON = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroup.fromJSON(publicChatAsJSON)
}
}
override fun getV2OpenGroup(threadId: String): OpenGroupV2? {
if (threadId.toInt() < 0) { return null } if (threadId.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor -> return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroupV2.fromJSON(publicChatAsJson) OpenGroupV2.fromJSON(publicChatAsJson)
} }
} }
override fun getThreadID(openGroupID: String): String {
val address = Address.fromSerialized(openGroupID)
val recipient = Recipient.from(context, address, false)
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient).toString()
}
override fun getOpenGroupPublicKey(server: String): String? { override fun getOpenGroupPublicKey(server: String): String? {
return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupPublicKey(server) return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupPublicKey(server)
} }
@ -281,63 +249,31 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupPublicKey(server, newValue) DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupPublicKey(server, newValue)
} }
override fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) { override fun getLastMessageServerID(room: String, server: String): Long? {
val groupID = "$server.$channel"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
}
override fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String) {
val groupID = "$server.$room"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
}
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
val groupID = "$server.$channel"
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
}
override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? {
val groupID = "$server.$room"
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
}
override fun getLastMessageServerId(room: String, server: String): Long? {
// return null if limit is set on open groups polling // return null if limit is set on open groups polling
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server) return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
} }
override fun setLastMessageServerId(room: String, server: String, newValue: Long) { override fun setLastMessageServerID(room: String, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue) DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
} }
override fun removeLastMessageServerId(room: String, server: String) { override fun removeLastMessageServerID(room: String, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server) DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
} }
override fun getLastMessageServerID(group: Long, server: String): Long? { override fun getLastDeletionServerID(room: String, server: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
}
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(group, server, newValue)
}
override fun removeLastMessageServerID(group: Long, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
}
override fun getLastDeletionServerId(room: String, server: String): Long? {
// return null if limit is set on open groups polling // return null if limit is set on open groups polling
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server) return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
} }
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) { override fun setLastDeletionServerID(room: String, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue) DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
} }
override fun removeLastDeletionServerId(room: String, server: String) { override fun removeLastDeletionServerID(room: String, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server) DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
} }
@ -345,34 +281,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue) DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
} }
override fun getLastDeletionServerID(group: Long, server: String): Long? { override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server) DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
} DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(group, server, newValue)
}
override fun removeLastDeletionServerID(group: Long, server: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server)
} }
override fun isDuplicateMessage(timestamp: Long): Boolean { override fun isDuplicateMessage(timestamp: Long): Boolean {
return getReceivedMessageTimestamps().contains(timestamp) return getReceivedMessageTimestamps().contains(timestamp)
} }
override fun setUserCount(group: Long, server: String, newValue: Int) {
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(group, server, newValue)
}
override fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) {
DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupProfilePictureURL(group, server, newValue)
}
override fun getOpenGroupProfilePictureURL(group: Long, server: String): String? {
return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupProfilePictureURL(group, server)
}
override fun updateTitle(groupID: String, newValue: String) { override fun updateTitle(groupID: String, newValue: String) {
DatabaseFactory.getGroupDatabase(context).updateTitle(groupID, newValue) DatabaseFactory.getGroupDatabase(context).updateTitle(groupID, newValue)
} }
@ -399,15 +316,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return database.getMessageFor(timestamp, address)?.getId() return database.getMessageFor(timestamp, address)?.getId()
} }
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
}
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey)
}
override fun markAsSent(timestamp: Long, author: String) { override fun markAsSent(timestamp: Long, author: String) {
val database = DatabaseFactory.getMmsSmsDatabase(context) val database = DatabaseFactory.getMmsSmsDatabase(context)
val messageRecord = database.getMessageFor(timestamp, author) ?: return val messageRecord = database.getMessageFor(timestamp, author) ?: return
@ -466,7 +374,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value) DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
} }
override fun getZombieMember(groupID: String): Set<String> { override fun getZombieMembers(groupID: String): Set<String> {
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet() return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
} }
@ -478,7 +386,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
} }
override fun updateZombieMembers(groupID: String, members: List<Address>) { override fun setZombieMembers(groupID: String, members: List<Address>) {
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members) DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
} }
@ -544,39 +452,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
} }
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().mapValues { (_,chat)->
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
}
}
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> { override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups() return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
} }
override fun addOpenGroup(serverUrl: String, channel: Long) {
val httpUrl = HttpUrl.parse(serverUrl) ?: return
if (httpUrl.queryParameterNames().contains("public_key")) {
// open group v2
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
// non-standard port, add to server
this.port(httpUrl.port())
}
}.build()
val room = httpUrl.pathSegments().firstOrNull() ?: return
val publicKey = httpUrl.queryParameter("public_key") ?: return
OpenGroupManager.add(server.toString().removeSuffix("/"), room, publicKey, context)
} else {
// TODO: No longer supported so let's remove this code
}
}
override fun getAllGroups(): List<GroupRecord> { override fun getAllGroups(): List<GroupRecord> {
return DatabaseFactory.getGroupDatabase(context).allGroups return DatabaseFactory.getGroupDatabase(context).allGroups
} }
override fun addOpenGroup(urlAsString: String) {
OpenGroupManager.addOpenGroup(urlAsString, context)
}
override fun setProfileSharing(address: Address, value: Boolean) { override fun setProfileSharing(address: Address, value: Boolean) {
val recipient = Recipient.from(context, address, false) val recipient = Recipient.from(context, address, false)
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)
@ -601,15 +488,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
} }
} }
override fun getThreadIdFor(address: Address): Long? { override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
val recipient = Recipient.from(context, address, false) val address = Address.fromSerialized(publicKeyOrOpenGroupID)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient) return getThreadId(address)
return if (threadID < 0) null else threadID
} }
fun foo() { override fun getThreadId(address: Address): Long? {
val threadDB = DatabaseFactory.getThreadDatabase(context) val recipient = Recipient.from(context, address, false)
return getThreadId(recipient)
}
override fun getThreadId(recipient: Recipient): Long? {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
return if (threadID < 0) null else threadID
} }
override fun getThreadIdForMms(mmsId: Long): Long { override fun getThreadIdForMms(mmsId: Long): Long {
@ -621,22 +512,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return threadId return threadId
} }
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
}
override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestSentTimestamp(publicKey, newValue)
}
override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? {
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestProcessedTimestamp(publicKey)
}
override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) {
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, newValue)
}
override fun getDisplayName(publicKey: String): String? { override fun getDisplayName(publicKey: String): String? {
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
} }
@ -645,10 +520,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName) DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName)
} }
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverID, publicKey)
}
override fun getProfilePictureURL(publicKey: String): String? { override fun getProfilePictureURL(publicKey: String): String? {
return DatabaseFactory.getLokiUserDatabase(context).getProfilePictureURL(publicKey) return DatabaseFactory.getLokiUserDatabase(context).getProfilePictureURL(publicKey)
} }
@ -696,7 +567,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return PartAuthority.getAttachmentThumbnailUri(attachmentId) return PartAuthority.getAttachmentThumbnailUri(attachmentId)
} }
// Data Extraction Notification
override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) {
val database = DatabaseFactory.getMmsDatabase(context) val database = DatabaseFactory.getMmsDatabase(context)
val address = fromSerialized(senderPublicKey) val address = fromSerialized(senderPublicKey)

View File

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

View File

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

View File

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

View File

@ -30,7 +30,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.open_groups.OpenGroupAPI
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2
import org.session.libsession.utilities.* import org.session.libsession.utilities.*
@ -344,16 +343,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
} }
// Delete the conversation // Delete the conversation
val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
if (v1OpenGroup != null) { if (v2OpenGroup != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server)
apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server)
apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server)
OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server)
// FIXME: No longer supported so let's remove this code
} else if (v2OpenGroup != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server) apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server)
apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server) apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server)

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.api
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
@ -49,8 +50,8 @@ object OpenGroupManager {
val existingOpenGroup = threadDB.getOpenGroupChat(threadID) val existingOpenGroup = threadDB.getOpenGroupChat(threadID)
if (existingOpenGroup != null) { return } if (existingOpenGroup != null) { return }
// Clear any existing data if needed // Clear any existing data if needed
storage.removeLastDeletionServerId(room, server) storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerId(room, server) storage.removeLastMessageServerID(room, server)
// Store the public key // Store the public key
storage.setOpenGroupPublicKey(server,publicKey) storage.setOpenGroupPublicKey(server,publicKey)
// Get an auth token // Get an auth token
@ -95,9 +96,22 @@ object OpenGroupManager {
} }
// Delete // Delete
ThreadUtils.queue { ThreadUtils.queue {
storage.removeLastDeletionServerId(room, server) storage.removeLastDeletionServerID(room, server)
storage.removeLastMessageServerId(room, server) storage.removeLastMessageServerID(room, server)
GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread GroupManager.deleteGroup(groupID, context) // Must be invoked on a background thread
} }
} }
fun addOpenGroup(urlAsString: String, context: Context) {
val url = HttpUrl.parse(urlAsString) ?: return
val builder = HttpUrl.Builder().scheme(url.scheme()).host(url.host())
if (url.port() != 80 || url.port() != 443) {
// Non-standard port; add to server
builder.port(url.port())
}
val server = builder.build()
val room = url.pathSegments().firstOrNull() ?: return
val publicKey = url.queryParameter("public_key") ?: return
add(server.toString().removeSuffix("/"), room, publicKey, context)
}
} }

View File

@ -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,26 +56,8 @@ 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 isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
if (!isOpenGroupV2) {
val publicChatId = OpenGroup.getId(channel, serverUrl)
return try {
Log.v(TAG, "Updating open group info for $publicChatId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
Result.failure()
}
} else {
val openGroupId = "$serverUrl.$room" val openGroupId = "$serverUrl.$room"
return try { return try {
Log.v(TAG, "Updating open group info for $openGroupId.") Log.v(TAG, "Updating open group info for $openGroupId.")
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!) OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
@ -86,7 +67,5 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
Log.e(TAG, "Failed to update open group info for $openGroupId", e) Log.e(TAG, "Failed to update open group info for $openGroupId", e)
Result.failure() Result.failure()
} }
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,11 +52,7 @@ class UserView : LinearLayout {
if (publicKey == null || publicKey.isBlank()) { if (publicKey == null || publicKey.isBlank()) {
return null return null
} else { } else {
var result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) val result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(openGroupThreadID)
if (result == null && publicChat != null) {
result = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
}
return result ?: publicKey return result ?: publicKey
} }
} }

View File

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

View File

@ -25,8 +25,4 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
val database = DatabaseFactory.getRecipientDatabase(context) val database = DatabaseFactory.getRecipientDatabase(context)
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode) database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
} }
override fun updateOpenGroupProfilePicturesIfNeeded(context: Context) {
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
}
} }

View File

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

View File

@ -1,6 +1,5 @@
package org.session.libsession.database package org.session.libsession.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.AttachmentUploadJob
@ -9,7 +8,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
@ -18,6 +16,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
@ -32,14 +31,12 @@ interface StorageProtocol {
fun getUserDisplayName(): String? fun getUserDisplayName(): String?
fun getUserProfileKey(): ByteArray? fun getUserProfileKey(): ByteArray?
fun getUserProfilePictureURL(): String? fun getUserProfilePictureURL(): String?
fun setUserProfilePictureUrl(newProfilePicture: String) fun setUserProfilePictureURL(newValue: String)
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
fun getDisplayNameForRecipient(recipientPublicKey: String): String? fun getDisplayNameForRecipient(recipientPublicKey: String): String?
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray) fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
// Signal Protocol // Signal
fun getOrGenerateRegistrationID(): Int fun getOrGenerateRegistrationID(): Int
// Jobs // Jobs
@ -59,48 +56,40 @@ interface StorageProtocol {
// Open Groups // Open Groups
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
fun getV2OpenGroup(threadId: String): OpenGroupV2? fun getV2OpenGroup(threadId: Long): OpenGroupV2?
fun addOpenGroup(urlAsString: String)
// Open Groups
fun getThreadID(openGroupID: String): String?
fun addOpenGroup(serverUrl: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
// Open Group Public Keys // Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String? fun getOpenGroupPublicKey(server: String): String?
fun setOpenGroupPublicKey(server: String, newValue: String) fun setOpenGroupPublicKey(server: String, newValue: String)
// Open Group User Info
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
// Open Group Metadata // Open Group Metadata
fun updateTitle(groupID: String, newValue: String) fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray) fun updateProfilePicture(groupID: String, newValue: ByteArray)
fun setUserCount(room: String, server: String, newValue: Int) fun setUserCount(room: String, server: String, newValue: Int)
// Last Message Server ID // Last Message Server ID
fun getLastMessageServerId(room: String, server: String): Long? fun getLastMessageServerID(room: String, server: String): Long?
fun setLastMessageServerId(room: String, server: String, newValue: Long) fun setLastMessageServerID(room: String, server: String, newValue: Long)
fun removeLastMessageServerId(room: String, server: String) fun removeLastMessageServerID(room: String, server: String)
// Last Deletion Server ID // Last Deletion Server ID
fun getLastDeletionServerId(room: String, server: String): Long? fun getLastDeletionServerID(room: String, server: String): Long?
fun setLastDeletionServerId(room: String, server: String, newValue: Long) fun setLastDeletionServerID(room: String, server: String, newValue: Long)
fun removeLastDeletionServerId(room: String, server: String) fun removeLastDeletionServerID(room: String, server: String)
// Message Handling // Message Handling
fun isDuplicateMessage(timestamp: Long): Boolean fun isDuplicateMessage(timestamp: Long): Boolean
fun getReceivedMessageTimestamps(): Set<Long> fun getReceivedMessageTimestamps(): Set<Long>
fun addReceivedMessageTimestamp(timestamp: Long) fun addReceivedMessageTimestamp(timestamp: Long)
fun removeReceivedMessageTimestamps(timestamps: Set<Long>) fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
// Returns the IDs of the saved attachments. /**
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> * Returns the IDs of the saved attachments.
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment> */
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
fun markAsSent(timestamp: Long, author: String) fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String) fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception) fun setErrorMessage(timestamp: Long, author: String, error: Exception)
@ -110,11 +99,10 @@ interface StorageProtocol {
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long) fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
fun isGroupActive(groupPublicKey: String): Boolean fun isGroupActive(groupPublicKey: String): Boolean
fun setActive(groupID: String, value: Boolean) fun setActive(groupID: String, value: Boolean)
fun getZombieMember(groupID: String): Set<String> fun getZombieMembers(groupID: String): Set<String>
fun removeMember(groupID: String, member: Address) fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>) fun updateMembers(groupID: String, members: List<Address>)
fun updateZombieMembers(groupID: String, members: List<Address>) fun setZombieMembers(groupID: String, members: List<Address>)
// Closed Group
fun getAllClosedGroupPublicKeys(): Set<String> fun getAllClosedGroupPublicKeys(): Set<String>
fun getAllActiveClosedGroupPublicKeys(): Set<String> fun getAllActiveClosedGroupPublicKeys(): Set<String>
fun addClosedGroupPublicKey(groupPublicKey: String) fun addClosedGroupPublicKey(groupPublicKey: String)
@ -138,58 +126,27 @@ interface StorageProtocol {
// Thread // Thread
fun getOrCreateThreadIdFor(address: Address): Long fun getOrCreateThreadIdFor(address: Address): Long
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
fun getThreadIdFor(address: Address): Long? fun getThreadId(publicKeyOrOpenGroupID: String): Long?
fun getThreadId(address: Address): Long?
fun getThreadId(recipient: Recipient): Long?
fun getThreadIdForMms(mmsId: Long): Long fun getThreadIdForMms(mmsId: Long): Long
fun getLastUpdated(threadID: Long): Long fun getLastUpdated(threadID: Long): Long
// Session Request // Contacts
fun getSessionRequestSentTimestamp(publicKey: String): Long?
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long)
// Loki User
fun getDisplayName(publicKey: String): String? fun getDisplayName(publicKey: String): String?
fun setDisplayName(publicKey: String, newName: String) fun setDisplayName(publicKey: String, newName: String)
fun getServerDisplayName(serverID: String, publicKey: String): String?
fun getProfilePictureURL(publicKey: String): String? fun getProfilePictureURL(publicKey: String): String?
// Recipient
fun getRecipientSettings(address: Address): RecipientSettings? fun getRecipientSettings(address: Address): RecipientSettings?
fun addContacts(contacts: List<ConfigurationMessage.Contact>) fun addContacts(contacts: List<ConfigurationMessage.Contact>)
// PartAuthority // Attachments
fun getAttachmentDataUri(attachmentId: AttachmentId): Uri fun getAttachmentDataUri(attachmentId: AttachmentId): Uri
fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri
// Message Handling // Message Handling
/// Returns the ID of the `TSIncomingMessage` that was constructed. /**
* Returns the ID of the `TSIncomingMessage` that was constructed.
*/
fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long?
// Data Extraction Notification
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
// DEPRECATED
fun getAuthToken(server: String): String?
fun setAuthToken(server: String, newValue: String?)
fun removeAuthToken(server: String)
fun getLastMessageServerID(group: Long, server: String): Long?
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
fun removeLastMessageServerID(group: Long, server: String)
fun getLastDeletionServerID(group: Long, server: String): Long?
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
fun removeLastDeletionServerID(group: Long, server: String)
fun getOpenGroup(threadID: String): OpenGroup?
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun setUserCount(group: Long, server: String, newValue: Int)
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,11 +54,12 @@ 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
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> { private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
@ -84,7 +85,7 @@ object MessageSender {
when (destination) { when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.") is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.")
} }
// Validate the message // Validate the message
if (!message.isValid()) { throw Error.InvalidMessage } if (!message.isValid()) { throw Error.InvalidMessage }
@ -122,7 +123,7 @@ object MessageSender {
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!! val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey) ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey)
} }
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.") is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
} }
// Wrap the result // Wrap the result
val kind: SignalServiceProtos.Envelope.Type val kind: SignalServiceProtos.Envelope.Type
@ -136,7 +137,7 @@ object MessageSender {
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
senderPublicKey = destination.groupPublicKey senderPublicKey = destination.groupPublicKey
} }
is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.") is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.")
} }
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
// Send the result // Send the result
@ -162,9 +163,11 @@ object MessageSender {
} }
handleSuccessfulMessageSend(message, destination, isSyncMessage) handleSuccessfulMessageSend(message, destination, isSyncMessage)
var shouldNotify = (message is VisibleMessage && !isSyncMessage) var shouldNotify = (message is VisibleMessage && !isSyncMessage)
/*
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) { if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
shouldNotify = true shouldNotify = true
} }
*/
if (shouldNotify) { if (shouldNotify) {
val notifyPNServerJob = NotifyPNServerJob(snodeMessage) val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
JobQueue.shared.add(notifyPNServerJob) JobQueue.shared.add(notifyPNServerJob)
@ -203,27 +206,6 @@ object MessageSender {
try { try {
when (destination) { when (destination) {
is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.") is Destination.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.")
is Destination.OpenGroup -> {
message.recipient = "${destination.server}.${destination.channel}"
val server = destination.server
val channel = destination.channel
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
throw Error.InvalidMessage
}
// Convert the message to an open group message
val openGroupMessage = OpenGroupMessage.from(message, server) ?: run {
throw Error.InvalidMessage
}
// Send the result
OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success {
message.openGroupServerMessageID = it.serverID
handleSuccessfulMessageSend(message, destination)
deferred.resolve(Unit)
}.fail {
handleFailure(it)
}
}
is Destination.OpenGroupV2 -> { is Destination.OpenGroupV2 -> {
message.recipient = "${destination.server}.${destination.room}" message.recipient = "${destination.server}.${destination.room}"
val server = destination.server val server = destination.server
@ -275,7 +257,7 @@ object MessageSender {
// Track the open group server message ID // Track the open group server message ID
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) { if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray()) val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded)) val threadID = storage.getThreadId(Address.fromSerialized(encoded))
if (threadID != null && threadID >= 0) { if (threadID != null && threadID >= 0) {
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
} }

View File

@ -192,8 +192,8 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
// Save the new group members // Save the new group members
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
// Update the zombie list // Update the zombie list
val oldZombies = storage.getZombieMember(groupID) val oldZombies = storage.getZombieMembers(groupID)
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) }) storage.setZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val name = group.title val name = group.title
// Send the update to the group // Send the update to the group

View File

@ -69,21 +69,21 @@ private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) { fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val address = Address.fromSerialized(senderPublicKey) val address = Address.fromSerialized(senderPublicKey)
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1) SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1)
} }
fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) { fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val address = Address.fromSerialized(senderPublicKey) val address = Address.fromSerialized(senderPublicKey)
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false) SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false)
} }
fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) { fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val address = Address.fromSerialized(senderPublicKey) val address = Address.fromSerialized(senderPublicKey)
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1) SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
} }
@ -123,11 +123,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name, handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!) closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
} }
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL } val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
for (openGroup in message.openGroups) { for (openGroup in message.openGroups) {
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue if (allV2OpenGroups.contains(openGroup)) continue
storage.addOpenGroup(openGroup, 1) storage.addOpenGroup(openGroup)
} }
if (message.displayName.isNotEmpty()) { if (message.displayName.isNotEmpty()) {
TextSecurePreferences.setProfileName(context, message.displayName) TextSecurePreferences.setProfileName(context, message.displayName)
@ -138,7 +137,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
val profileKey = Base64.encodeBytes(message.profileKey) val profileKey = Base64.encodeBytes(message.profileKey)
ProfileKeyUtil.setEncodedProfileKey(context, profileKey) ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey) storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
storage.setUserProfilePictureUrl(message.profilePicture!!) storage.setUserProfilePictureURL(message.profilePicture!!)
} }
storage.addContacts(message.contacts) storage.addContacts(message.contacts)
} }
@ -158,12 +157,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
throw MessageReceiver.Error.NoThread throw MessageReceiver.Error.NoThread
} }
val openGroup = threadID.let {
storage.getOpenGroup(it.toString())
}
// Update profile if needed // Update profile if needed
val profile = message.profile val profile = message.profile
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups if (profile != null && userPublicKey != message.sender) {
val profileManager = SSKEnvironment.shared.profileManager val profileManager = SSKEnvironment.shared.profileManager
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false) val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
val displayName = profile.displayName!! val displayName = profile.displayName!!
@ -475,8 +471,8 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
} }
// Update zombie members // Update zombie members
val zombies = storage.getZombieMember(groupID) val zombies = storage.getZombieMembers(groupID)
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) }) storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
// Notify the user // Notify the user
// We don't display zombie members in the notification as users have already been notified when those members left // We don't display zombie members in the notification as users have already been notified when those members left
@ -528,8 +524,8 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
} else { } else {
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
// Update zombie members // Update zombie members
val zombies = storage.getZombieMember(groupID) val zombies = storage.getZombieMembers(groupID)
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) }) storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
} }
// Notify the user // Notify the user
if (userLeft) { if (userLeft) {

View File

@ -28,7 +28,7 @@ class ClosedGroupPollerV2 {
companion object { companion object {
private val minPollInterval = 4 * 1000 private val minPollInterval = 4 * 1000
private val maxPollInterval = 2 * 60 * 1000 private val maxPollInterval = 4 * 60 * 1000
@JvmStatic @JvmStatic
val shared = ClosedGroupPollerV2() val shared = ClosedGroupPollerV2()
@ -75,7 +75,7 @@ class ClosedGroupPollerV2 {
// reasonable fake time interval to use instead. // reasonable fake time interval to use instead.
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val threadID = storage.getThreadID(groupID)?.toLongOrNull() ?: return val threadID = storage.getThreadId(groupID) ?: return
val lastUpdated = storage.getLastUpdated(threadID) val lastUpdated = storage.getLastUpdated(threadID)
val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000 val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000
val minPollInterval = Companion.minPollInterval val minPollInterval = Companion.minPollInterval

View File

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

View File

@ -78,7 +78,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService:
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
val threadID = storage.getThreadIdFor(Address.fromSerialized(groupID)) ?: return val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID -> val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverID ->
val messageID = dataProvider.getMessageID(serverID, threadID) val messageID = dataProvider.getMessageID(serverID, threadID)
if (messageID == null) { if (messageID == null) {

View File

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

View File

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

View File

@ -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)!!
val fileId = httpUrl.pathSegments().last()
val useOldServer = url.contains(FileServerAPIV2.OLD_SERVER)
try { try {
FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let { FileServerAPIV2.download(fileID.toLong()).get().let {
outputStream.write(it) outputStream.write(it)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Couln't download attachment due to error",e) Log.e("Loki", "Couldn't download attachment.", e)
throw 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)
}
}
} }
} }

View File

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

View File

@ -33,7 +33,6 @@ class SSKEnvironment(
fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String)
fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray)
fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode)
fun updateOpenGroupProfilePicturesIfNeeded(context: Context)
} }
interface MessageExpirationManagerProtocol { interface MessageExpirationManagerProtocol {

View File

@ -0,0 +1,3 @@
package org.session.libsession.utilities
data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)

View File

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