diff --git a/app/build.gradle b/app/build.gradle index 85214e2b96..f45317006a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,8 +143,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 166 -def canonicalVersionName = "1.10.6" +def canonicalVersionCode = 170 +def canonicalVersionName = "1.10.7" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index afcfff4a21..d58916b71b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -22,21 +22,17 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Handler; import android.os.Looper; - import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; import androidx.multidex.MultiDexApplication; - import org.conscrypt.Conscrypt; import org.session.libsession.avatars.AvatarHelper; 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.open_groups.OpenGroupAPI; import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; -import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller; +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2; import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.snode.SnodeModule; import org.session.libsession.utilities.Address; @@ -131,7 +127,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Loki public MessageNotifier messageNotifier = null; public Poller poller = null; - public ClosedGroupPoller closedGroupPoller = null; public Broadcaster broadcaster = null; public SignalCommunicationModule communicationModule; private Job firebaseInstanceIdJob; @@ -175,9 +170,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (userPublicKey != null) { MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB); } - setUpStorageAPIIfNeeded(); resubmitProfilePictureIfNeeded(); - updateOpenGroupProfilePicturesIfNeeded(); if (userPublicKey != null) { registerForFCMIfNeeded(false); } @@ -206,10 +199,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc poller.setCaughtUp(false); } startPollingIfNeeded(); - // FIXME: Open group handling - /* - publicChatManager.markAllAsNotCaughtUp(); - */ + + OpenGroupManager.INSTANCE.setAllCaughtUp(false); OpenGroupManager.INSTANCE.startPolling(); } @@ -223,9 +214,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (poller != null) { poller.stopIfNeeded(); } - if (closedGroupPoller != null) { - closedGroupPoller.stopIfNeeded(); - } + ClosedGroupPollerV2.getShared().stop(); } @Override @@ -406,20 +395,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base))); } - 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; - } + private static class ProviderInitializationException extends RuntimeException { } public void registerForFCMIfNeeded(final Boolean force) { if (firebaseInstanceIdJob != null && firebaseInstanceIdJob.isActive() && !force) return; @@ -451,7 +427,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc return; } poller = new Poller(); - closedGroupPoller = new ClosedGroupPoller(); } public void startPollingIfNeeded() { @@ -459,9 +434,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (poller != null) { poller.startIfNeeded(); } - if (closedGroupPoller != null) { - closedGroupPoller.startIfNeeded(); - } + ClosedGroupPollerV2.getShared().start(); } private void resubmitProfilePictureIfNeeded() { @@ -498,19 +471,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc }); } - public void updateOpenGroupProfilePicturesIfNeeded() { - AsyncTask.execute(() -> { - byte[] profileKey = ProfileKeyUtil.getProfileKey(this); - String url = TextSecurePreferences.getProfilePictureURL(this); - Set servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers(); - for (String server : servers) { - if (profileKey != null) { - OpenGroupAPI.setProfilePicture(server, profileKey, url); - } - } - }); - } - public void clearAllData(boolean isMigratingToV2KeyPair) { String token = TextSecurePreferences.getFCMToken(this); if (token != null && !token.isEmpty()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java index f61186144c..314653743c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -30,18 +30,15 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.Loader; - - import org.session.libsession.messaging.messages.visible.LinkPreview; import org.session.libsession.messaging.messages.visible.OpenGroupInvitation; import org.session.libsession.messaging.messages.visible.Quote; 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.utilities.UpdateMessageData; import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus; @@ -264,7 +261,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity } toFrom.setText(toFromRes); long threadID = messageRecord.getThreadId(); - OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadID); + OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID); if (openGroup != null && messageRecord.isOutgoing()) { toFrom.setVisibility(View.GONE); separator.setVisibility(View.GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 8608426055..d017e770f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -5,10 +5,9 @@ import android.text.TextUtils import com.google.protobuf.ByteString import org.greenrobot.eventbus.EventBus 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.utilities.Address -import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsession.utilities.UploadResult import org.session.libsession.utilities.Util import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.messages.SignalServiceAttachment @@ -104,11 +103,7 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp) } - override fun getOpenGroup(threadID: Long): OpenGroup? { - return null // TODO: Implement - } - - override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { + override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index ce4f00c530..b556d20f55 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -13,24 +13,18 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; - import com.annimon.stream.Stream; 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.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; - import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.recipients.RecipientModifiedListener; import org.session.libsession.utilities.TextSecurePreferences; @@ -197,15 +191,11 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener boolean outgoing = messageType != MESSAGE_TYPE_INCOMING; 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(); - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID); if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) { quoteeDisplayName = TextSecurePreferences.getProfileName(getContext()); - } else if (publicChat != null) { - quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), senderHexEncodedPublicKey); } else { quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(senderHexEncodedPublicKey); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 4997f47f73..e9fddd1e03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -77,9 +77,7 @@ import androidx.core.view.MenuItemCompat; import androidx.lifecycle.ViewModelProviders; import androidx.loader.app.LoaderManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import com.annimon.stream.Stream; - import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; 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.visible.OpenGroupInvitation; import org.session.libsession.messaging.messages.visible.VisibleMessage; -import org.session.libsession.messaging.open_groups.OpenGroup; import org.session.libsession.messaging.open_groups.OpenGroupV2; import org.session.libsession.messaging.sending_receiving.MessageSender; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; @@ -194,7 +191,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.PushCharacterCalculator; - import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -205,7 +201,6 @@ import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - import kotlin.Unit; import network.loki.messenger.R; @@ -378,12 +373,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this); - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); - if (publicChat != null) { - // Request open group info update and handle the successful result in #onOpenGroupInfoUpdated(). - PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel()); - } else if (openGroupV2 != null) { + if (openGroupV2 != null) { PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom()); if (openGroupV2.getRoom().equals("session") || openGroupV2.getRoom().equals("oxen") || openGroupV2.getRoom().equals("lokinet") || openGroupV2.getRoom().equals("crypto")) { @@ -1414,13 +1405,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Subscribe(threadMode = ThreadMode.MAIN) public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) { - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(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 && openGroup.getRoom().equals(event.getRoom()) && openGroup.getServer().equals(event.getUrl())) { @@ -2376,13 +2361,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity muteIndicatorImageView.setVisibility(View.VISIBLE); 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")) { - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId); OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId); - if (publicChat != null) { - Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer()); - if (userCount == null) { userCount = 0; } - subtitleTextView.setText(userCount + " members"); - } else if (openGroup != null) { + if (openGroup != null) { Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer()); if (userCount == null) { userCount = 0; } subtitleTextView.setText(userCount + " members"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 275cbba606..425160b45d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -54,17 +54,13 @@ import androidx.loader.content.Loader; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnScrollListener; - import com.annimon.stream.Stream; - import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.messages.control.DataExtractionNotification; import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.Quote; import org.session.libsession.messaging.messages.visible.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.OpenGroupV2; import org.session.libsession.messaging.sending_receiving.MessageSender; @@ -398,9 +394,8 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); - boolean isPublicChat = (publicChat != null || openGroupChat != null); + boolean isPublicChat = (openGroupChat != null); int selectedMessageCount = messageRecords.size(); boolean areAllSentByUser = true; Set uniqueUserSet = new HashSet<>(); @@ -412,10 +407,7 @@ public class ConversationFragment extends Fragment menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext()); boolean userCanModerate = - (isPublicChat && - ((publicChat != null && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer())) - || (openGroupChat != null && OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer()))) - ); + (isPublicChat && (OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer()))); boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate); // allow banning if moderating a public chat and only one user's messages are selected 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.setCancelable(true); - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @@ -527,7 +518,7 @@ public class ConversationFragment extends Fragment { @Override protected Void doInBackground(MessageRecord... messageRecords) { - if (publicChat != null || openGroupChat != null) { + if (openGroupChat != null) { ArrayList serverIDs = new ArrayList<>(); ArrayList ignoredMessages = new ArrayList<>(); ArrayList failedMessages = new ArrayList<>(); @@ -541,29 +532,7 @@ public class ConversationFragment extends Fragment ignoredMessages.add(messageRecord.getId()); } } - if (publicChat != 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) { + if (openGroupChat != null) { for (Long serverId : serverIDs) { OpenGroupAPIV2 .deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer()) @@ -617,7 +586,6 @@ public class ConversationFragment extends Fragment builder.setTitle(R.string.ConversationFragment_ban_selected_user); builder.setCancelable(true); - final OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); builder.setPositiveButton(R.string.ban, (dialog, which) -> { @@ -630,17 +598,7 @@ public class ConversationFragment extends Fragment @Override protected Void doInBackground(String... userPublicKeyParam) { String userPublicKey = userPublicKeyParam[0]; - if (publicChat != 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) { + if (openGroupChat != null) { OpenGroupAPIV2 .ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer()) .success(l -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 1454e94336..7467cdcc78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -45,17 +45,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.annimon.stream.Stream; - import org.session.libsession.messaging.jobs.AttachmentDownloadJob; 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.OpenGroupV2; import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress; @@ -760,10 +755,6 @@ public class ConversationItem extends LinearLayout String publicKey = recipient.getAddress().toString(); profilePictureView.setPublicKey(publicKey); 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.setAdditionalPublicKey(null); profilePictureView.setRSSFeed(false); @@ -898,20 +889,7 @@ public class ConversationItem extends LinearLayout @SuppressLint("SetTextI18n") private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { if (groupThread && !messageRecord.isOutgoing()) { - // Show custom display names for group chats 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); @@ -952,12 +930,8 @@ public class ConversationItem extends LinearLayout profilePictureView.setVisibility(VISIBLE); int visibility = View.GONE; - OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId()); OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId()); - if (publicChat != null) { - boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer()); - visibility = isModerator ? View.VISIBLE : View.GONE; - } else if (openGroupV2 != null) { + if (openGroupV2 != null) { boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index ab19938d41..fd1b4591d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database import android.content.Context import android.net.Uri -import okhttp3.HttpUrl import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.contacts.Contact 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.visible.Attachment 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.attachments.AttachmentId 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.database.LokiThreadDatabase 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.getString import org.thoughtcrime.securesms.mms.PartAuthority class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { + override fun getUserPublicKey(): String? { return TextSecurePreferences.getLocalNumber(context) } @@ -73,13 +71,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return TextSecurePreferences.getProfilePictureURL(context) } - override fun setUserProfilePictureUrl(newProfilePicture: String) { + override fun setUserProfilePictureURL(newValue: String) { val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) } - TextSecurePreferences.setProfilePictureURL(context, newProfilePicture) - RetrieveProfileAvatarJob(ourRecipient, newProfilePicture) - ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newProfilePicture)) + TextSecurePreferences.setProfilePictureURL(context, newValue) + RetrieveProfileAvatarJob(ourRecipient, newValue) + ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(ourRecipient, newValue)) } override fun getOrGenerateRegistrationID(): Int { @@ -91,15 +89,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return registrationID } - override fun persistAttachments(messageId: Long, attachments: List): List { + override fun persistAttachments(messageID: Long, attachments: List): List { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() } - return database.insertAttachments(messageId, databaseAttachments) + return database.insertAttachments(messageID, databaseAttachments) } - override fun getAttachmentsForMessage(messageId: Long): List { + override fun getAttachmentsForMessage(messageID: Long): List { val database = DatabaseFactory.getAttachmentDatabase(context) - return database.getAttachmentsForMessage(messageId) + return database.getAttachmentsForMessage(messageID) } override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? { @@ -169,7 +167,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return messageID } - // JOBS override fun persistJob(job: Job) { DatabaseFactory.getSessionJobDatabase(context).persistJob(job) } @@ -203,20 +200,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, 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? { val id = "$server.$room" return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id) @@ -232,30 +215,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null) } - override fun getOpenGroup(threadID: String): OpenGroup? { - 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? { + 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 -> + return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor -> val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat) 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? { return DatabaseFactory.getLokiAPIDatabase(context).getOpenGroupPublicKey(server) } @@ -264,59 +232,27 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).setOpenGroupPublicKey(server, newValue) } - override fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) { - 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? { + override fun getLastMessageServerID(room: String, server: String): Long? { 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) } - override fun removeLastMessageServerId(room: String, server: String) { + override fun removeLastMessageServerID(room: String, server: String) { DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server) } - override fun getLastMessageServerID(group: Long, 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? { + override fun getLastDeletionServerID(room: String, server: String): Long? { 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) } - override fun removeLastDeletionServerId(room: String, server: String) { + override fun removeLastDeletionServerID(room: String, server: String) { DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server) } @@ -324,34 +260,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue) } - override fun getLastDeletionServerID(group: Long, server: String): Long? { - return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server) - } - - 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 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 isDuplicateMessage(timestamp: Long): Boolean { 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) { DatabaseFactory.getGroupDatabase(context).updateTitle(groupID, newValue) } @@ -378,15 +295,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, 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) { val database = DatabaseFactory.getMmsSmsDatabase(context) val messageRecord = database.getMessageFor(timestamp, author) ?: return @@ -445,7 +353,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getGroupDatabase(context).setActive(groupID, value) } - override fun getZombieMember(groupID: String): Set { + override fun getZombieMembers(groupID: String): Set { return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet() } @@ -457,7 +365,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members) } - override fun updateZombieMembers(groupID: String, members: List
) { + override fun setZombieMembers(groupID: String, members: List
) { DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members) } @@ -523,39 +431,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) } - override fun getAllOpenGroups(): Map { - return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().mapValues { (_,chat)-> - OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable) - } - } - override fun getAllV2OpenGroups(): Map { 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 { return DatabaseFactory.getGroupDatabase(context).allGroups } + override fun addOpenGroup(urlAsString: String) { + OpenGroupManager.addOpenGroup(urlAsString, context) + } + override fun setProfileSharing(address: Address, value: Boolean) { val recipient = Recipient.from(context, address, false) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value) @@ -580,8 +467,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } } - override fun getThreadIdFor(address: Address): Long? { + override fun getThreadId(publicKeyOrOpenGroupID: String): Long? { + val address = Address.fromSerialized(publicKeyOrOpenGroupID) + return getThreadId(address) + } + + override fun getThreadId(address: Address): Long? { val recipient = Recipient.from(context, address, false) + return getThreadId(recipient) + } + + override fun getThreadId(recipient: Recipient): Long? { val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient) return if (threadID < 0) null else threadID } @@ -595,22 +491,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, 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? { val contact = DatabaseFactory.getSessionContactDatabase(context).getContactWithSessionID(publicKey) contact?.let { @@ -662,10 +542,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, threadDatabase.getOrCreateThreadIdFor(recipient) } if (contacts.isNotEmpty()) { - threadDatabase.notifyUpdatedFromConfig() + threadDatabase.notifyConversationListListeners() } } + override fun getLastUpdated(threadID: Long): Long { + val threadDB = DatabaseFactory.getThreadDatabase(context) + return threadDB.getLastUpdated(threadID) + } + override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri { return PartAuthority.getAttachmentDataUri(attachmentId) } @@ -674,7 +559,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return PartAuthority.getAttachmentThumbnailUri(attachmentId) } - // Data Extraction Notification override fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) { val database = DatabaseFactory.getMmsDatabase(context) val address = fromSerialized(senderPublicKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index ea73484659..ebb0dbab26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -64,7 +64,7 @@ public class ThreadDatabase extends Database { private static final String TAG = ThreadDatabase.class.getSimpleName(); - private Map addressCache = new HashMap<>(); + private final Map addressCache = new HashMap<>(); public static final String TABLE_NAME = "thread"; public static final String ID = "_id"; @@ -404,6 +404,21 @@ public class ThreadDatabase extends Database { } } + public Long getLastUpdated(long threadId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, new String[]{DATE}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null); + + try { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } + + return -1L; + } finally { + if (cursor != null) cursor.close(); + } + } + public void deleteConversation(long threadId) { DatabaseFactory.getSmsDatabase(context).deleteThread(threadId); DatabaseFactory.getMmsDatabase(context).deleteThread(threadId); @@ -471,7 +486,6 @@ public class ThreadDatabase extends Database { } public @Nullable Recipient getRecipientForThreadId(long threadId) { - // Loki - Cache the address if (addressCache.containsKey(threadId) && addressCache.get(threadId) != null) { return Recipient.from(context, addressCache.get(threadId), false); } @@ -505,17 +519,13 @@ public class ThreadDatabase extends Database { notifyConversationListeners(threadId); } - public void notifyUpdatedFromConfig() { - notifyConversationListListeners(); - } - public boolean update(long threadId, boolean unarchive) { MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); long count = mmsSmsDatabase.getConversationCount(threadId); + boolean shouldDeleteEmptyThread = deleteThreadOnEmpty(threadId); - - if (count == 0) { + if (count == 0 && shouldDeleteEmptyThread) { deleteThread(threadId); notifyConversationListListeners(); return true; @@ -534,9 +544,12 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); return false; } else { - deleteThread(threadId); - notifyConversationListListeners(); - return true; + if (shouldDeleteEmptyThread) { + deleteThread(threadId); + notifyConversationListListeners(); + return true; + } + return false; } } finally { if (reader != null) @@ -544,6 +557,11 @@ public class ThreadDatabase extends Database { } } + private boolean deleteThreadOnEmpty(long threadId) { + Recipient threadRecipient = getRecipientForThreadId(threadId); + return threadRecipient != null && !threadRecipient.isOpenGroupRecipient(); + } + private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { if (messageRecord.isMms()) { MmsMessageRecord record = (MmsMessageRecord) messageRecord; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index b25a1d67f6..15a71c7ef0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -58,9 +58,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV23 = 44; private static final int lokiV24 = 45; private static final int lokiV25 = 46; + private static final int lokiV26 = 47; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV25; + private static final int DATABASE_VERSION = lokiV26; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -295,6 +296,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV25) { + String jobTable = SessionJobDatabase.sessionJobTable; + db.execSQL("DROP TABLE " + jobTable); + db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand()); + } + + if (oldVersion < lokiV26) { db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index 0b5b7340d6..8f09dbbbca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -70,10 +70,6 @@ public class GroupManager { final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false); - if (!groupDatabase.getGroup(groupId).isPresent()) { - return false; - } - long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient); if (threadId != -1L) { threadDatabase.deleteConversation(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 1c7bf1142f..3cabdce5d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -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); 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 InputStream inputStream; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 080ce6c88c..34a010000e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -100,7 +100,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); try { - DownloadUtilities.downloadFile(downloadDestination, profileAvatar, MAX_PROFILE_SIZE_BYTES, null); + DownloadUtilities.downloadFile(downloadDestination, profileAvatar); InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey); File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt index 6450115184..a1bc90237b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/BackupRestoreActivity.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityBackupRestoreBinding import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.backup.FullBackupImporter @@ -61,31 +60,31 @@ class BackupRestoreActivity : BaseActionBarActivity() { super.onCreate(savedInstanceState) setUpActionBarSessionLogo() - val viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_backup_restore) - viewBinding.lifecycleOwner = this - viewBinding.viewModel = viewModel +// val viewBinding = DataBindingUtil.setContentView(this, R.layout.activity_backup_restore) +// viewBinding.lifecycleOwner = this +// viewBinding.viewModel = viewModel - viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() } +// viewBinding.restoreButton.setOnClickListener { viewModel.tryRestoreBackup() } - viewBinding.buttonSelectFile.setOnClickListener { - fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly - // and the backup files are unavailable for selection. -// type = BackupUtil.BACKUP_FILE_MIME_TYPE - type = "*/*" - }) - } +// viewBinding.buttonSelectFile.setOnClickListener { +// fileSelectionResultLauncher.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply { +// //FIXME On some old APIs (tested on 21 & 23) the mime type doesn't filter properly +// // and the backup files are unavailable for selection. +//// type = BackupUtil.BACKUP_FILE_MIME_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. - viewModel.backupFile.observe(this, { backupFile -> - if (backupFile != null) viewBinding.backupCode.post { - viewBinding.backupCode.requestFocus() - (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager) - .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT) - } - }) +// viewModel.backupFile.observe(this, { backupFile -> +// if (backupFile != null) viewBinding.backupCode.post { +// viewBinding.backupCode.requestFocus() +// (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager) +// .showSoftInput(viewBinding.backupCode, InputMethodManager.SHOW_IMPLICIT) +// } +// }) // React to backup import result. viewModel.backupImportResult.observe(this) { result -> @@ -116,8 +115,8 @@ class BackupRestoreActivity : BaseActionBarActivity() { openURL("https://getsession.org/privacy-policy/") } }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance() - viewBinding.termsTextView.text = termsExplanation +// viewBinding.termsTextView.movementMethod = LinkMovementMethod.getInstance() +// viewBinding.termsTextView.text = termsExplanation //endregion } @@ -190,7 +189,6 @@ class BackupRestoreViewModel(application: Application): AndroidViewModel(applica TextSecurePreferences.setHasViewedSeed(context, true) TextSecurePreferences.setHasSeenWelcomeScreen(context, true) val application = ApplicationContext.getInstance(context) - application.setUpStorageAPIIfNeeded() BackupRestoreResult.SUCCESS } catch (e: DatabaseDowngradeException) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index b5c02f6bd7..f8ed445146 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -30,7 +30,6 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.session.libsession.messaging.jobs.JobQueue 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.utilities.* import org.session.libsignal.utilities.toHexString @@ -332,16 +331,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } // Delete the conversation - val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) - if (v1OpenGroup != 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) { + if (v2OpenGroup != null) { val apiDB = DatabaseFactory.getLokiAPIDatabase(context) apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server) apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server) @@ -400,4 +391,4 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), show(intent) } // endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt index 49662194e2..7e4210246b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/LinkDeviceActivity.kt @@ -122,7 +122,6 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel } // start polling and wait for updated message ApplicationContext.getInstance(this@LinkDeviceActivity).apply { - setUpStorageAPIIfNeeded() startPollingIfNeeded() } TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt index d5ce931eab..fe29f58914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/PNModeActivity.kt @@ -153,7 +153,6 @@ class PNModeActivity : BaseActionBarActivity() { } TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView)) val application = ApplicationContext.getInstance(this) - application.setUpStorageAPIIfNeeded() application.startPollingIfNeeded() application.registerForFCMIfNeeded(true) val intent = Intent(this, HomeActivity::class.java) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index c45cb8036c..c521c45c9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -26,7 +26,6 @@ import nl.komponents.kovenant.all import nl.komponents.kovenant.ui.alwaysUi import nl.komponents.kovenant.ui.successUi 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.ProfilePictureUtilities import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol @@ -179,8 +178,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { val promises = mutableListOf>() val displayName = displayNameToBeUploaded if (displayName != null) { - val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() - promises.addAll(servers.map { OpenGroupAPI.setDisplayName(displayName, it) }) TextSecurePreferences.setProfileName(this, displayName) } val profilePicture = profilePictureToBeUploaded @@ -195,7 +192,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) TextSecurePreferences.setLastProfilePictureUpload(this, Date().time) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) - ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() } if (profilePicture != null || displayName != null) { MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt index fc3e7c1bba..d070c25f2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -7,9 +7,9 @@ import androidx.work.* import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.map +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.MessageReceiveJob -import org.session.libsession.messaging.open_groups.OpenGroupV2 -import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2 import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.TextSecurePreferences @@ -57,7 +57,10 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor promises.addAll(dmsPromise.get()) // Closed groups - promises.addAll(ClosedGroupPoller().pollOnce()) + val closedGroupPoller = ClosedGroupPollerV2() // Intentionally don't use shared + val storage = MessagingModuleConfiguration.shared.storage + val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + allGroupPublicKeys.forEach { closedGroupPoller.poll(it) } // Open Groups val threadDB = DatabaseFactory.getLokiThreadDatabase(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt index 62aca11265..e63e775d35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/OpenGroupManager.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.api import android.content.Context import android.graphics.Bitmap import androidx.annotation.WorkerThread +import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupV2 @@ -19,6 +20,8 @@ object OpenGroupManager { private var pollers = mutableMapOf() // One for each server private var isPolling = false + var isAllCaughtUp = false + fun startPolling() { if (isPolling) { return } isPolling = true @@ -47,8 +50,8 @@ object OpenGroupManager { val existingOpenGroup = threadDB.getOpenGroupChat(threadID) if (existingOpenGroup != null) { return } // Clear any existing data if needed - storage.removeLastDeletionServerId(room, server) - storage.removeLastMessageServerId(room, server) + storage.removeLastDeletionServerID(room, server) + storage.removeLastMessageServerID(room, server) // Store the public key storage.setOpenGroupPublicKey(server,publicKey) // Get an auth token @@ -93,9 +96,22 @@ object OpenGroupManager { } // Delete ThreadUtils.queue { - storage.removeLastDeletionServerId(room, server) - storage.removeLastMessageServerId(room, server) + storage.removeLastDeletionServerID(room, server) + storage.removeLastMessageServerID(room, server) 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) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt index 7880efbc55..db0e6c5e7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatInfoUpdateWorker.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.loki.api import android.content.Context import androidx.work.* -import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities @@ -57,36 +56,16 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) override fun doWork(): Result { val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!! - val channel = inputData.getLong(DATA_KEY_CHANNEL, -1) 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" - - return try { - Log.v(TAG, "Updating open group info for $openGroupId.") - OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!) - Log.v(TAG, "Open group info was successfully updated for $openGroupId.") - Result.success() - } catch (e: Exception) { - Log.e(TAG, "Failed to update open group info for $openGroupId", e) - Result.failure() - } - + val openGroupId = "$serverUrl.$room" + return try { + Log.v(TAG, "Updating open group info for $openGroupId.") + OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!) + Log.v(TAG, "Open group info was successfully updated for $openGroupId.") + Result.success() + } catch (e: Exception) { + Log.e(TAG, "Failed to update open group info for $openGroupId", e) + Result.failure() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt index c591160c32..b0e1cfe992 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt @@ -3,17 +3,13 @@ package org.thoughtcrime.securesms.loki.database import android.content.ContentValues import android.content.Context import android.database.Cursor -import org.session.libsession.messaging.open_groups.OpenGroup - import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.utilities.* - import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient - import org.session.libsignal.utilities.JsonUtil 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" val publicChatTable = "loki_public_chat_database" val threadID = "thread_id" - private val friendRequestStatus = "friend_request_status" private val sessionResetStatus = "session_reset_status" val publicChat = "public_chat" @JvmStatic @@ -37,28 +32,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient) } - fun getAllPublicChats(): Map { - val database = databaseHelper.readableDatabase - var cursor: Cursor? = null - val result = mutableMapOf() - 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 { val database = databaseHelper.readableDatabase var cursor: Cursor? = null @@ -79,20 +52,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return result } - fun getAllPublicChatServers(): Set { - 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? { if (threadID < 0) { return null @@ -114,19 +73,4 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson())) database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString())) } - - fun setPublicChat(publicChat: OpenGroup, threadID: Long) { - if (threadID < 0) { - return - } - val database = databaseHelper.writableDatabase - val 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())) - } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt index 3323283514..1a7efd48ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiUserDatabase.kt @@ -54,27 +54,6 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database 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? { return if (publicKey == TextSecurePreferences.getLocalNumber(context)) { TextSecurePreferences.getProfilePictureURL(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt index 55409e1a5e..6d070865ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.loki.utilities.* class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { companion object { - private const val sessionJobTable = "session_job_database" + const val sessionJobTable = "session_job_database" const val jobID = "job_id" const val jobType = "job_type" const val failureCount = "failure_count" diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt index c1145c5f49..b270cdecf6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt @@ -39,12 +39,6 @@ object SessionMetaProtocol { return shouldIgnoreMessage } - @JvmStatic - fun shouldIgnoreDecryptionException(context: Context, timestamp: Long): Boolean { - val restorationTimestamp = TextSecurePreferences.getRestorationTime(context) - return timestamp <= restorationTimestamp - } - @JvmStatic fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) { val displayName = content.senderDisplayName.orNull() ?: return @@ -58,24 +52,6 @@ object SessionMetaProtocol { 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 fun canUserReplyToNotification(recipient: Recipient): Boolean { // TODO return !recipient.address.isRSSFeed diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt index 50b69274da..300d5466a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/MentionUtilities.kt @@ -27,15 +27,12 @@ object MentionUtilities { var matcher = pattern.matcher(text) val mentions = mutableListOf, String>>() var startIndex = 0 - val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! if (matcher.find(startIndex)) { while (true) { val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val userDisplayName: String? = if (publicKey.toLowerCase() == userPublicKey.toLowerCase()) { TextSecurePreferences.getProfileName(context) - } else if (publicChat != null) { - DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey) } else { DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt index 7db784d8ca..ad3d35f809 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/NotificationUtilities.kt @@ -6,13 +6,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.session.libsession.utilities.recipients.Recipient 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 displayName = if (publicChat != null) { - DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey) - } else { - DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) - } + val displayName = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) + // FIXME: Add short ID here? return displayName ?: publicKey } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt index 8c08d53e3b..2f562e87c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt @@ -3,18 +3,10 @@ package org.thoughtcrime.securesms.loki.utilities import android.content.Context import androidx.annotation.WorkerThread 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.OpenGroupV2 import org.session.libsession.utilities.recipients.Recipient 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.groups.GroupManager import java.util.* //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. */ - @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 @WorkerThread @Throws(Exception::class) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt index d702610668..b7745a67a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateSelectionView.kt @@ -17,10 +17,10 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } var glide: GlideRequests? = null set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue } - var publicChatServer: String? = null - set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatServer = publicChatServer } - var publicChatChannel: Long? = null - set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatChannel = publicChatChannel } + var openGroupServer: String? = null + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer } + var openGroupRoom: String? = null + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupRoom = openGroupRoom } var onMentionCandidateSelected: ((Mention) -> Unit)? = null private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) } @@ -29,8 +29,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS var mentionCandidates = listOf() set(newValue) { field = newValue; notifyDataSetChanged() } var glide: GlideRequests? = null - var publicChatServer: String? = null - var publicChatChannel: Long? = null + var openGroupServer: String? = null + var openGroupRoom: String? = null override fun getCount(): Int { return mentionCandidates.count() @@ -49,8 +49,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS val mentionCandidate = getItem(position) cell.glide = glide cell.mentionCandidate = mentionCandidate - cell.publicChatServer = publicChatServer - cell.publicChatChannel = publicChatChannel + cell.openGroupServer = openGroupServer + cell.openGroupRoom = openGroupRoom return cell } } @@ -68,10 +68,10 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS } fun show(mentionCandidates: List, threadID: Long) { - val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) - if (publicChat != null) { - publicChatServer = publicChat.server - publicChatChannel = publicChat.channel + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + if (openGroup != null) { + openGroupServer = openGroup.server + openGroupRoom = openGroup.room } this.mentionCandidates = mentionCandidates val layoutParams = this.layoutParams as ViewGroup.LayoutParams diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt index 07233c3e06..0a748c98f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/MentionCandidateView.kt @@ -8,16 +8,16 @@ import android.view.ViewGroup import android.widget.LinearLayout import kotlinx.android.synthetic.main.view_mention_candidate.view.* 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.open_groups.OpenGroupAPIV2 import org.thoughtcrime.securesms.mms.GlideRequests class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { var mentionCandidate = Mention("", "") set(newValue) { field = newValue; update() } var glide: GlideRequests? = null - var publicChatServer: String? = null - var publicChatChannel: Long? = null + var openGroupServer: String? = null + var openGroupRoom: String? = null constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null) @@ -37,8 +37,8 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: profilePictureView.isRSSFeed = false profilePictureView.glide = glide!! profilePictureView.update() - if (publicChatServer != null && publicChatChannel != null) { - val isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!) + if (openGroupServer != null && openGroupRoom != null) { + val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!) moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE } else { moderatorIconImageView.visibility = View.GONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt index fbc469379e..2226e9ef8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ProfilePictureView.kt @@ -29,7 +29,7 @@ class ProfilePictureView : RelativeLayout { var additionalDisplayName: String? = null var isRSSFeed = false var isLarge = false - private val imagesCached = mutableSetOf() + private val profilePicturesCached = mutableMapOf() // region Lifecycle constructor(context: Context) : super(context) { @@ -61,11 +61,7 @@ class ProfilePictureView : RelativeLayout { if (publicKey == null || publicKey.isBlank()) { return null } else { - var 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) - } + val result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) return result ?: publicKey } } @@ -146,13 +142,13 @@ class ProfilePictureView : RelativeLayout { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { if (publicKey.isNotEmpty()) { 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 - if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" - && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { + val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject + if (signalProfilePicture != null && avatar != "0" && avatar != "") { glide.clear(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView) - imagesCached.add(publicKey) + profilePicturesCached[publicKey] = recipient.profileAvatar } else { val sizeInPX = resources.getDimensionPixelSize(sizeResId) glide.clear(imageView) @@ -162,7 +158,7 @@ class ProfilePictureView : RelativeLayout { publicKey, displayName )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) - imagesCached.add(publicKey) + profilePicturesCached[publicKey] = recipient.profileAvatar } } else { imageView.setImageDrawable(null) @@ -170,7 +166,7 @@ class ProfilePictureView : RelativeLayout { } fun recycle() { - imagesCached.clear() + profilePicturesCached.clear() } // endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt index 765dd27171..b99a9d3b20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/UserView.kt @@ -58,11 +58,7 @@ class UserView : LinearLayout { val contactContext = Contact.contextForRecipient(user) return it.displayName(contactContext) } - var 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) - } + val result = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey) return result ?: publicKey } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 12fc8279f0..f71050aba9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms; 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 { @@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints { @Override public int getImageMaxSize(Context context) { - return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier()); + return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); } @Override public int getGifMaxSize(Context context) { - return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier()); + return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); } @Override public int getVideoMaxSize(Context context) { - return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier()); + return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); } @Override public int getAudioMaxSize(Context context) { - return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier()); + return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); } @Override public int getDocumentMaxSize(Context context) { - return (int) (((double) FileServerAPI.Companion.getMaxFileSize()) / FileServerAPI.Companion.getFileSizeORMultiplier()); + return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index d5ab94c1a0..55dd3f17a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; +import org.thoughtcrime.securesms.loki.api.OpenGroupManager; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -286,6 +287,9 @@ public class DefaultMessageNotifier implements MessageNotifier { } finally { if (telcoCursor != null) telcoCursor.close(); if (pushCursor != null) pushCursor.close(); + if (!OpenGroupManager.INSTANCE.isAllCaughtUp()) { + OpenGroupManager.INSTANCE.setAllCaughtUp(true); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index 46b4d836d7..448d977da9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -12,6 +12,7 @@ import org.session.libsession.utilities.recipients.Recipient; import org.session.libsession.utilities.Debouncer; import org.session.libsignal.utilities.ThreadUtils; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.loki.api.OpenGroupManager; import java.util.concurrent.TimeUnit; @@ -42,18 +43,12 @@ public class OptimizedMessageNotifier implements MessageNotifier { @Override public void updateNotification(@NonNull Context context) { Poller poller = ApplicationContext.getInstance(context).poller; - // FIXME: Open group handling boolean isCaughtUp = true; if (poller != null) { isCaughtUp = isCaughtUp && poller.isCaughtUp(); } - // FIXME: Open group handling - /* - if (publicChatManager != null) { - isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp(); - } - */ + isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp(); if (isCaughtUp) { performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context)); @@ -65,18 +60,12 @@ public class OptimizedMessageNotifier implements MessageNotifier { @Override public void updateNotification(@NonNull Context context, long threadId) { Poller lokiPoller = ApplicationContext.getInstance(context).poller; - // FIXME: Open group handling boolean isCaughtUp = true; if (lokiPoller != null) { isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp(); } - // FIXME: Open group handling - /* - if (publicChatManager != null) { - isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp(); - } - */ + isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp(); if (isCaughtUp) { performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId)); @@ -88,18 +77,12 @@ public class OptimizedMessageNotifier implements MessageNotifier { @Override public void updateNotification(@NonNull Context context, long threadId, boolean signal) { Poller lokiPoller = ApplicationContext.getInstance(context).poller; - // FIXME: Open group handling boolean isCaughtUp = true; if (lokiPoller != null) { isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp(); } - // FIXME: Open group handling - /* - if (publicChatManager != null) { - isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp(); - } - */ + isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp(); if (isCaughtUp) { performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, threadId, signal)); @@ -111,18 +94,12 @@ public class OptimizedMessageNotifier implements MessageNotifier { @Override public void updateNotification(@androidx.annotation.NonNull Context context, boolean signal, int reminderCount) { Poller lokiPoller = ApplicationContext.getInstance(context).poller; - // FIXME: Open group handling boolean isCaughtUp = true; if (lokiPoller != null) { isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp(); } - // FIXME: Open group handling - /* - if (publicChatManager != null) { - isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp(); - } - */ + isCaughtUp = isCaughtUp && OpenGroupManager.INSTANCE.isAllCaughtUp(); if (isCaughtUp) { performOnBackgroundThreadIfNeeded(() -> wrapped.updateNotification(context, signal, reminderCount)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt index c4ba993743..cda261994b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sskenvironment/ProfileManager.kt @@ -9,13 +9,14 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob class ProfileManager: SSKEnvironment.ProfileManagerProtocol { + override fun setDisplayName(context: Context, recipient: Recipient, displayName: String?) { val sessionID = recipient.address.serialize() // New API val contactDatabase = DatabaseFactory.getSessionContactDatabase(context) var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) contact = Contact(sessionID) - contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address) + contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address) if (contact.nickname != displayName) { contact.nickname = displayName contactDatabase.setContact(contact) @@ -35,7 +36,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol { val contactDatabase = DatabaseFactory.getSessionContactDatabase(context) var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) contact = Contact(sessionID) - contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address) + contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address) if (contact.name != profileName) { contact.name = profileName contactDatabase.setContact(contact) @@ -49,7 +50,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol { val contactDatabase = DatabaseFactory.getSessionContactDatabase(context) var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) contact = Contact(sessionID) - contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address) + contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address) if (contact.profilePictureURL != profilePictureURL) { contact.profilePictureURL = profilePictureURL contactDatabase.setContact(contact) @@ -64,7 +65,7 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol { val contactDatabase = DatabaseFactory.getSessionContactDatabase(context) var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) contact = Contact(sessionID) - contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address) + contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address) if (!contact.profilePictureEncryptionKey.contentEquals(profileKey)) { contact.profilePictureEncryptionKey = profileKey contactDatabase.setContact(contact) @@ -76,17 +77,13 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol { database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode) } - override fun updateOpenGroupProfilePicturesIfNeeded(context: Context) { - ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded() - } - override fun getDisplayName(context: Context, recipient: Recipient): String? { val sessionID = recipient.address.serialize() val contactDatabase = DatabaseFactory.getSessionContactDatabase(context) var contact = contactDatabase.getContactWithSessionID(sessionID) if (contact == null) { contact = Contact(sessionID) - contact.threadID = DatabaseFactory.getStorage(context).getThreadIdFor(recipient.address) + contact.threadID = DatabaseFactory.getStorage(context).getThreadId(recipient.address) contact.name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(sessionID) ?: recipient.profileName ?: recipient.name contactDatabase.setContact(contact) } diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml deleted file mode 100644 index ee47a132e5..0000000000 --- a/app/src/main/res/values-sh/strings.xml +++ /dev/null @@ -1,706 +0,0 @@ - - - Session - Da - Ne - Obriši - Ban - Sačekajte... - Spremi - Osobna bilješka - Version %s - - Nova poruka - - \+%d - - - %d poruka po razgovoru - %d poruke po razgovoru - %d poruka po razgovoru - %d poruka po razgovoru - - Obriši sve stare poruke? - - Ovo će automatski skratiti sve razgovore na odabrani broj poruka. - Ovo će automatski skratiti sve razgovore na %d najnovije poruke. - Ovo će automatski skratiti sve razgovore na %d najnovijih poruka. - Ovo će automatski skratiti sve razgovore na %d najnovijih poruka. - - Obriši - Uključeno - Isključeno - - (slika) - (zvuk) - (video) - (odgovor) - - Nije moguće pronaći aplikaciju za odabir medija. - Session zahtijeva dozvolu pristupa Pohrani podataka za umetanje slikovnih i audio-vizualnih priloga ali zahtjev biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Pohrana\". - Session zahtijeva dozvolu pristupa Kontaktima za prilaganje informacije o kontaktima ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Kontakti\". - Session zahtijeva dozvolu pristupa Kameri za omogućavanje slikanja ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Kamera\". - - Greška prilikom reprodukcije zvuka! - - Danas - Jučer - Ovaj tjedan - Ovaj mjesec - - Web pretraživać nije pronađen. - - Grupe - - Slanje nije uspjelo, dodirnite za detalje - Poruka za razmjenu ključeva je primljena, pritisnite da biste nastavili. - %1$s je napustio grupu. - Slanje neuspješno, pritisnite za nesigurnu rezervu - Nije moguće pronaći aplikaciju za otvaranje ovog medija. - Kopirano %s - Pročitaj više - Preuzmi više - Na čekanju - - Dodaj privitak - Odaberite informacije kontakta - Došlo je do greške prilikom postavljanja privitka. - Message - Naispravan primatelj! - Dodano na početni ekran - Napusti grupu? - Jeste li sigurni da želite napustiti ovu grupu? - Greška pri napuštanju grupe - Ukloni blokadu ovog kontakta? - Ponovno ćete moći primati poruke ili pozive ovog korisnika. - Ukloni blokadu - Privitak prelazi ograničenje veličine za tip poruke koju šaljete. - Nije moguće snimati svuk! - Na vašem uređaju ne postoji aplikacija koja bi rukovala ovom poveznicom. - Add members - Join %s - Are you sure you want to join the %s open group? - Kako biste poslali audio poruku, dozvolite Sessionu pristup vašem mikrofonu. - Session zahtijeva dozvolu pristupa Mikrofonu za slanje zvučnih poruka ali pristup biva odbijen. Molim otvorite opcije aplikacije, odaberite \"Dozvole\" i uključite \"Mikrofon\". - Kako biste snimili slike i video, dozvolite Sessionu pristup kameri. - Session zahtijeva dozvolu Kameri za fotografiranje ili snimanje videa, ali pristup biva odbijen. Molim nastavite s postavkama aplikacije, odaberite \"Dozvole\" i omogućite \"Kamera\". - Session treba pristup kameri kako bi snimio slike ili video. - %1$s%2$s - %1$d od %2$d - Nema rezultata - - - %d nepročitana poruka - %d nepročitane poruke - %d nepročitanih poruka - %d nepročitanih poruka - - - - Obriši odabrane poruke? - Obriši odabrane poruke? - Obriši odabrane poruke? - Obriši odabrane poruke? - - - Ovo će trajno obrisati odabrane poruke. - Ovo će trajno obrisati sve %1$d odabrane poruke. - Ovo će trajno obrisati svih %1$d odabranih poruka. - Ovo će trajno obrisati svih %1$d odabranih poruka. - - Ban this user? - Spremi na disk? - - Spremanje svih medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi? - Spremanje sva %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi? - Spremanje svih %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi? - Spremanje svih %1$d medija na disk će omogućiti pristup mediju iz drugih aplikacija na vašem uređaju.\n\nNastavi? - - - Greška prilikom spremanja privitaka na disk! - Greška prilikom spremanja privitaka na disk! - Greška prilikom spremanja privitaka na disk! - Greška prilikom spremanja privitaka na disk! - - - Spremanje privitaka - Spremanje %1$d privitka - Spremanje %1$d privitaka - Spremanje %1$d privitaka - - - Spremanje privitaka na disk... - Spremanje %1$d privitka na disk... - Spremanje %1$d privitaka na disk... - Spremanje %1$d privitaka na disk... - - U toku... - Podaci (Session) - MMS - SMS - Brisanje - Brisanje poruka... - Banning - Banning user… - Originalna poruka nije pronađena - Originalna poruka više nije dostupna - - Poruka za razmjenu ključa - - Profilna slika - - Koristeći prilagođeno: %s - Koristeći zadano: %s - Niti jedna - - Sada - %d min - Danas - Jučer - - Danas - - Nepoznata datoteka - - Greška pri dohvaćanju GIFa pune rezolucije - - GIFovi - Naljepnice - - Slika profila - - Pritisnite i držite kako biste snimili glasovnu poruku, pustite za slanje - - Nije moguće naći poruku - Poruka od %1$s - Vaša poruka - - Medij - - Izbriši označenu poruku? - Izbriši označene poruke? - Izbriši označene poruke? - Izbriši označene poruke? - - - Ovo će trajno obrisati označenu poruku. - Broj poruka koji će biti trajno izbrisan: %1$d - Broj poruka koji će biti trajno izbrisan: %1$d - Broj poruka koji će biti trajno izbrisan: %1$d - - Brisanje - Brisanje poruka... - Dokumenti - Odaberi sve - Prikupljanje privitaka... - - Multimedijalna poruka - Preuzimanje MMS poruke - Greška pri preuzimanju MMS poruke, pritisnite za ponovni pokušaj - - Send to %s - - Add a caption... - Stavka je uklonjena jer je prešla limit veličine - Kamera nije dostupna - Message to %s - - Ne možete dijeliti više od %dstavke. - Ne možete dijeliti više od %d stavki. - Ne možete dijeliti više od %dstavki. - Ne možete dijeliti više od %dstavki. - - - Svi mediji - - Primljena je poruka kriptirana starom inačicom Session aplikacije koja više nije podržana. Molimo zapitajte pošiljatelja da ažurira na najnoviju inačicu aplikacije i ponovno pošalje poruku. - Napustili ste grupu. - Ažurirali ste grupu. - %s je ažurirao grupu. - - Nestajuće poruke - Vaše poruke neće isteći. - Primljene i poslane poruke u ovom razgovoru će nestati %s nakon što su viđene. - - Unesite lozinku - - Blokiraj kontakt? - Nećete više primati poruke i pozive ovog korisnika. - Blokiraj - Ukloni blokadu ovog kontakta? - Ponovno ćete moći primati poruke ili pozive ovog korisnika. - Ukloni blokadu - - Slika - Audio - Video - - Primljena iskvarena poruka -razmjene ključeva! - Primljena poruka razmjene ključeva za pogrešnu inačicu protokola. - Primljena je poruka s novim sigurnosnim brojem. Pritisnite za obradu i prikaz. - Resetiranje sigurne sesije. - %s resetiranje sigurne sesije. - Dupla poruka. - - Grupa je ažurirana - Napustio/la grupu - Resetiranje sigurne sesije. - Skica: - Zvali ste - Zvali su vas - Propušteni poziv - Multimedijalna poruka - %s je dostupan na Sessionu! - Disappearing messages disabled - Vrijeme nestajanja poruke postavljeno na %s - %s took a screenshot. - Media saved by %s. - Sigurnosni broj je izmijenjen - Vaš sigurnosni broj s %s je izmjenjen. - Označili ste provjerenim - Označili ste nepotvrđenim - This conversation is empty - Open group invitation - - Session ažuriranje - Nova inačica Sessiona je dostupna, pritisnite za ažuriranje - - Loše kriptirana poruka - Poruka kriptirana za nepostojeću sesiju - - Loše kriptirana MMS poruka - MMS poruka kriptirana za nepostojeću sesiju - - Utišaj obavijesti - - Pritisnite za otvaranje. - Session je otključan - Zaključaj Session - - Vi - Nepodržani tip medija - Skica - Session zahtjeva pristup pohrani kako bi se omogućilo zapisivanje podataka na vanjski medij, ali je trajno odbijeno. Molimo nastavite do opcija aplikacije, odaberite \"Dozvole\", i uključite \"Pohrana\". - Nije moguće spremanje na vanjski medij bez dozvola - Obriši poruku? - Radnja će trajno izbrisati ovu poruku. - - %1$d novih poruka u %2$d razgovora - Najnovije od: %1$s - Zaključana poruka - Neuspješna isporuka poruke. - Isporuka poruke nije uspjela. - Greška prilikom isporuke poruke. - Označi sve kao pročitano - Označi pročitano - Odgovori - Session poruke na čekanju - Imate Session poruka na čekanju, pritisnite kako biste ih otvorili i preuzeli - %1$s%2$s - Kontakt - - Zadano - Pozivi - Neuspjesi - Sigurnosne kopije - Status zaključavanja - Ažuriranja aplikacije - Ostalo - Poruke - Nepoznato - - Brzi odgovor nije dostupan kada je Session zaključan! - Greška prilikom slanja poruke! - - Spremljeno u %s - Spremljeno - - Traži - - Nevažeći prečac - - Session - Nova poruka - - - %d Item - %d Items - %d Items - %d Items - - - Greška pri video reprodukciji - - Zvuk - Zvuk - Kontakt - Kontakt - Kamera - Kamera - Položaj - Položaj - GIF - Gif - Slika ili video - Datoteka - Galerija - Datoteka - Uključi/isključi ladicu dodataka - - Učitavanje kontakata... - - Pošalji - Sastavljanje poruke - Uključi/isključi emoji tipkovnicu - Priložena sličica - Uključi/isključi ladicu brze kamere - Snimi i pošalji audio privitak - Lock recording of audio attachment - Omogući Session za SMS - - Kliznite za otkazivanje - Odustani - - Multimedijalna poruka - Sigurna poruka - - Neuspješno slanje - Odobrenje u tijeku - Isporučeno - Poruka pročitana - - Slika kontakta - - Reprodukcija - Zaustavi - Preuzmi - - Join - Open group invitation - - Zvuk - Video - Fotografija - Vi - Originalna poruka nije pronađena - - Kliži do dna - - Pretraži GIFove i naljepnice - - Ništa nije pronađeno. - - Prikaži cijeli razgovor - Učitavanje - - Bez medija - - PONOVNO POŠALJI - - Blokiraj - - Neki problemi zahtjevaju vašu pozornost. - Poslano - Primljeno - Nestaje - Putem - Prima: - Šalje: - Sa: - - Stvorite lozinku - Odaberite kontakte - Prikaz medija - - Koristi zadano - Koristi prilagođeno - Utišaj na 1 sat - Utišaj na 2 sata - Utišaj na 1 dan - Utišaj na 7 dana - Utišaj na 1 godinu - Zadane postavke - Omogućeno - Onemogućeno - Ime i poruka - Samo ime - Nema imena ili poruke - Slike - Audio - Video - Dokumenti - Mala - Normalna - Velika - Ekstra velika - Zadano - Visoko - Maks - - - %d sat - %d sata - %d sati - %d sati - - - Enter šalje poruku - Pritisak na Enter će poslati tekst poruku - Send link previews - Previews are supported for Imgur, Instagram, Pinterest, Reddit, and YouTube links - Sigurnost ekrana - Onemogući snimanje ekrana na popisu nedavnih i unutar aplikacije - Obavijesti - LED boja - Nepoznato - LED uzorak treptanja - Zvuk - Bezvučno - Ponovi upozorenja - Nikada - Jednom - Dva puta - Tri puta - Pet puta - Deset puta - Vibracija - Zelena - Crvena - Plava - Narančasta - Cijan - Magenta - Bijela - Niti jedna - Brzo - Normalno - Sporo - Automatski obriši starije poruke nakon što razgovor pređe određenu duljinu - Obriši stare poruke - Maksimalna duljina razgovora - Skrati sve razgovore odmah - Skeniraj sve razgovore i primijeni ograničenje duljine razgovora - Zadano - Inkognito tipkovnica - Potvrde čitanja - Ukoliko ste onemogućili potvrdu čitanja kod sebe, nećete moći vidjeti potvrde čitanja od drugih. - Pokazatelji tipkanja - If typing indicators are disabled, you won\'t be able to see typing indicators from others. - Zahtijevaj od tipkovnice da onemogući personalizirano učenje - Svijetla - Tamna - Skraćivanje poruke - Koristi emotikone sustava - Onemogući ugrađenu Session podršku za emotikone - Pristup aplikaciji - Komunikacija - Razgovori - Poruke - Zvuk unutar razgovora - Prikaži - Prioritet - - - - - Nova poruka za... - - Detalji poruke - Kopiraj tekst - Obriši poruku - Ban user - Ponovno pošalji poruku - Reply to message - - Spremi privitak - - Nestajuće poruke - - Poruke ističu - - Ukloni utišanje - - Utišaj obavijesti - - Uredi grupu - Napusti grupu - Svi mediji - Add to home screen - - Proširi skočni prozor - - Isporuka - Razgovor - Emitiranje - - Spremi - Naprijed - Svi mediji - - Nema dokumenata - - Prikaz medija - - Brisanje - Brisanje starih poruka... - Stare poruke su uspješno obrisane - - Permission required - Nastavi - Not now - Backups will be saved to external storage and encrypted with the passphrase below. You must have this passphrase in order to restore a backup. - I have written down this passphrase. Without it, I will be unable to restore a backup. - Preskoči - Cannot import backups from newer versions of Session - Incorrect backup passphrase - Enable local backups? - Enable backups - Please acknowledge your understanding by marking the confirmation check box. - Delete backups? - Disable and delete all local backups? - Delete backups - Kopirano u međuspremnik - Stvaranje sigurnosne kopije... - %d messages so far - Nikada - Screen lock - Lock Session access with Android screen lock or fingerprint - Screen lock inactivity timeout - Niti jedna - - Copy public key - - Continue - Copy - Invalid URL - Copied to clipboard - Next - Share - Invalid Session ID - Cancel - Your Session ID - Your Session begins here... - Create Session ID - Continue Your Session - What\'s Session? - It\'s a decentralized, encrypted messaging app - So it doesn\'t collect my personal information or my conversation metadata? How does it work? - Using a combination of advanced anonymous routing and end-to-end encryption technologies. - Friends don\'t let friends use compromised messengers. You\'re welcome. - Say hello to your Session ID - Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design. - Restore your account - Enter the recovery phrase that was given to you when you signed up to restore your account. - Enter your recovery phrase - Pick your display name - This will be your name when you use Session. It can be your real name, an alias, or anything else you like. - Enter a display name - Please pick a display name - Please pick a shorter display name - Recommended - Please Pick an Option - You don\'t have any contacts yet - Start a Session - Are you sure you want to leave this group? - "Couldn't leave group" - Are you sure you want to delete this conversation? - Conversation deleted - Your Recovery Phrase - Meet your recovery phrase - Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and don\'t give it to anyone. - Hold to reveal - Secure your account by saving your recovery phrase - Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID. - Make sure to store your recovery phrase in a safe place - Path - Session hides your IP by bouncing your messages through several Service Nodes in Session\'s decentralized network. These are the countries your connection is currently being bounced through: - You - Entry Node - Service Node - Destination - Learn More - New Session - Enter Session ID - Scan QR Code - Scan a user\'s QR code to start a session. QR codes can be found by tapping the QR code icon in account settings. - Enter Session ID of recipient - Users can share their Session ID by going into their account settings and tapping \"Share Session ID\", or by sharing their QR code. - Session needs camera access to scan QR codes - Grant Camera Access - New Closed Group - Enter a group name - You don\'t have any contacts yet - Start a Session - Please enter a group name - Please enter a shorter group name - Please pick at least 1 group member - A closed group cannot have more than 100 members - Join Open Group - Couldn\'t join group - Open Group URL - Scan QR Code - Scan the QR code of the open group you\'d like to join - Enter an open group URL - Settings - Enter a display name - Please pick a display name - Please pick a shorter display name - Privacy - Notifications - Chats - Devices - Invite - Recovery Phrase - Clear Data - Notifications - Notification Style - Notification Content - Privacy - Chats - Notification Strategy - Change name - Unlink device - Your Recovery Phrase - This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device. - Clear All Data - This will permanently delete your messages, sessions, and contacts. - QR Code - View My QR Code - Scan QR Code - Scan someone\'s QR code to start a conversation with them - This is your QR code. Other users can scan it to start a session with you. - Share QR Code - Contacts - Closed Groups - Open Groups - - Apply - Done - Edit Group - Enter a new group name - Members - Add members - Group name can\'t be empty - Please enter a shorter group name - Groups must have at least 1 group member - Remove user from group - Select Contacts - Secure session reset done - Theme - Day - Night - System default - Copy Session ID - Attachment - Voice Message - Details - Failed to activate backups. Please try again or contact support. - Restore backup - Select a file - Select a backup file and enter the passphrase it was created with. - 30-digit passphrase - - This is taking a while, would you like to skip? - Or join one of these… - diff --git a/libsession/build.gradle b/libsession/build.gradle index bc78036b1e..6763e0cff5 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.annimon:stream:1.1.8' implementation 'com.makeramen:roundedimageview:2.1.0' - implementation 'com.esotericsoftware:kryo:4.0.1' + implementation 'com.esotericsoftware:kryo:5.1.1' implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "org.whispersystems:curve25519-java:$curve25519Version" diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index 4650afc528..4cf93a429d 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -1,9 +1,8 @@ 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.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.SignalServiceAttachmentStream import java.io.InputStream @@ -13,31 +12,20 @@ interface MessageDataProvider { fun getMessageID(serverID: Long): Long? fun getMessageID(serverId: Long, threadId: Long): Pair? fun deleteMessage(messageID: Long, isSms: Boolean) - fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? - fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer? - fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? - fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) - fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream) - fun isOutgoingMessage(timestamp: Long): Boolean - - fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) + fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleFailedAttachmentUpload(attachmentId: Long) - fun getMessageForQuote(timestamp: Long, author: Address): Pair? fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List fun getMessageBodyFor(timestamp: Long, author: String): String - fun getAttachmentIDsFor(messageID: Long): List fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? - - fun getOpenGroup(threadID: Long): OpenGroup? } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index ed878a730e..1d9211846e 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -1,6 +1,5 @@ package org.session.libsession.database - import android.content.Context import android.net.Uri import org.session.libsession.messaging.contacts.Contact @@ -10,7 +9,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.visible.Attachment 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.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage @@ -19,6 +17,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupRecord +import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient.RecipientSettings import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer @@ -33,10 +32,8 @@ interface StorageProtocol { fun getUserDisplayName(): String? fun getUserProfileKey(): ByteArray? fun getUserProfilePictureURL(): String? - fun setUserProfilePictureUrl(newProfilePicture: String) - - // Signal Protocol - + fun setUserProfilePictureURL(newProfilePicture: String) + // Signal fun getOrGenerateRegistrationID(): Int // Jobs @@ -56,48 +53,40 @@ interface StorageProtocol { // Open Groups fun getAllV2OpenGroups(): Map - fun getV2OpenGroup(threadId: String): OpenGroupV2? - - // Open Groups - fun getThreadID(openGroupID: String): String? - fun addOpenGroup(serverUrl: String, channel: Long) + fun getV2OpenGroup(threadId: Long): OpenGroupV2? + fun addOpenGroup(urlAsString: String) fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) - fun getQuoteServerID(quoteID: Long, publicKey: String): Long? // Open Group Public Keys fun getOpenGroupPublicKey(server: String): 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 - fun updateTitle(groupID: String, newValue: String) fun updateProfilePicture(groupID: String, newValue: ByteArray) fun setUserCount(room: String, server: String, newValue: Int) // Last Message Server ID - fun getLastMessageServerId(room: String, server: String): Long? - fun setLastMessageServerId(room: String, server: String, newValue: Long) - fun removeLastMessageServerId(room: String, server: String) + fun getLastMessageServerID(room: String, server: String): Long? + fun setLastMessageServerID(room: String, server: String, newValue: Long) + fun removeLastMessageServerID(room: String, server: String) // Last Deletion Server ID - fun getLastDeletionServerId(room: String, server: String): Long? - fun setLastDeletionServerId(room: String, server: String, newValue: Long) - fun removeLastDeletionServerId(room: String, server: String) + fun getLastDeletionServerID(room: String, server: String): Long? + fun setLastDeletionServerID(room: String, server: String, newValue: Long) + fun removeLastDeletionServerID(room: String, server: String) // Message Handling fun isDuplicateMessage(timestamp: Long): Boolean fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) fun removeReceivedMessageTimestamps(timestamps: Set) - // Returns the IDs of the saved attachments. - fun persistAttachments(messageId: Long, attachments: List): List - fun getAttachmentsForMessage(messageId: Long): List - - fun getMessageIdInDatabase(timestamp: Long, author: String): Long? + /** + * Returns the IDs of the saved attachments. + */ + fun persistAttachments(messageID: Long, attachments: List): List + fun getAttachmentsForMessage(messageID: Long): List + fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name fun markAsSent(timestamp: Long, author: String) fun markUnidentified(timestamp: Long, author: String) fun setErrorMessage(timestamp: Long, author: String, error: Exception) @@ -107,11 +96,10 @@ interface StorageProtocol { fun createGroup(groupID: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) fun isGroupActive(groupPublicKey: String): Boolean fun setActive(groupID: String, value: Boolean) - fun getZombieMember(groupID: String): Set + fun getZombieMembers(groupID: String): Set fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) - fun updateZombieMembers(groupID: String, members: List
) - // Closed Group + fun setZombieMembers(groupID: String, members: List
) fun getAllClosedGroupPublicKeys(): Set fun getAllActiveClosedGroupPublicKeys(): Set fun addClosedGroupPublicKey(groupPublicKey: String) @@ -119,9 +107,9 @@ interface StorageProtocol { fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, - name: String, members: Collection, admins: Collection, sentTimestamp: Long) + name: String, members: Collection, admins: Collection, sentTimestamp: Long) fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, - members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) + members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) fun isClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? @@ -135,58 +123,29 @@ interface StorageProtocol { // Thread fun getOrCreateThreadIdFor(address: Address): 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 getLastUpdated(threadID: Long): Long - // Session Request - fun getSessionRequestSentTimestamp(publicKey: String): Long? - fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) - fun getSessionRequestProcessedTimestamp(publicKey: String): Long? - fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) - - // Session Contact (Loki User) + // Contacts fun getDisplayName(publicKey: String): String? fun getProfilePictureURL(publicKey: String): String? fun getContactWithSessionID(sessionID: String): Contact? fun getAllContacts(): Set fun setContact(contact: Contact) - - // Recipient fun getRecipientSettings(address: Address): RecipientSettings? fun addContacts(contacts: List) - // PartAuthority + // Attachments fun getAttachmentDataUri(attachmentId: AttachmentId): Uri fun getAttachmentThumbnailUri(attachmentId: AttachmentId): Uri // 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, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? - - // Data Extraction Notification fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long) - - // DEPRECATED - fun getAuthToken(server: String): String? - fun setAuthToken(server: String, newValue: String?) - fun removeAuthToken(server: String) - - fun getLastMessageServerID(group: Long, server: String): Long? - fun setLastMessageServerID(group: Long, server: String, newValue: Long) - fun removeLastMessageServerID(group: Long, server: String) - - fun getLastDeletionServerID(group: Long, server: String): Long? - fun setLastDeletionServerID(group: Long, server: String, newValue: Long) - fun removeLastDeletionServerID(group: Long, server: String) - - fun getOpenGroup(threadID: String): OpenGroup? - fun getAllOpenGroups(): Map - - 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? - } diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 20b8d49d14..03349369dc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -5,9 +5,9 @@ import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.StorageProtocol class MessagingModuleConfiguration( - val context: Context, - val storage: StorageProtocol, - val messageDataProvider: MessageDataProvider + val context: Context, + val storage: StorageProtocol, + val messageDataProvider: MessageDataProvider ) { companion object { diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPI.kt deleted file mode 100644 index aeca8b2562..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPI.kt +++ /dev/null @@ -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 { - 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 - } - } - } - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt index 57cfbb6534..c229aab4be 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt @@ -15,10 +15,18 @@ import org.session.libsignal.utilities.Log object FileServerAPIV2 { - private const val OLD_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - const val OLD_SERVER = "http://88.99.175.227" - private const val SERVER_PUBLIC_KEY = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" - const val SERVER = "http://filev2.getsession.org" + private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" + const val server = "http://filev2.getsession.org" + const 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. + */ + const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5? sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") @@ -44,9 +52,7 @@ object FileServerAPIV2 { return RequestBody.create(MediaType.get("application/json"), parametersAsJSON) } - private fun send(request: Request, useOldServer: Boolean): Promise, Exception> { - val server = if (useOldServer) OLD_SERVER else SERVER - val serverPublicKey = if (useOldServer) OLD_SERVER_PUBLIC_KEY else SERVER_PUBLIC_KEY + private fun send(request: Request): Promise, Exception> { val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val urlBuilder = HttpUrl.Builder() .scheme(url.scheme()) @@ -80,14 +86,14 @@ object FileServerAPIV2 { val base64EncodedFile = Base64.encodeBytes(file) val parameters = mapOf( "file" to base64EncodedFile ) val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters) - return send(request, false).map { json -> + return send(request).map { json -> json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed } } - fun download(file: Long, useOldServer: Boolean): Promise { + fun download(file: Long): Promise { 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 Base64.decode(base64EncodedFile) ?: throw Error.ParsingFailed } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 362b2e81ed..b0d923b0c3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -2,11 +2,9 @@ package org.session.libsession.messaging.jobs import okhttp3.HttpUrl 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.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.utilities.Data -import org.session.libsession.messaging.utilities.DotNetAPI import org.session.libsession.utilities.DownloadUtilities import org.session.libsignal.streams.AttachmentCipherInputStream import org.session.libsignal.utilities.Base64 @@ -42,11 +40,6 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) if (exception == Error.NoAttachment) { messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) 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 { this.handleFailure(exception) } @@ -57,9 +50,9 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) val tempFile = createTempFile() val threadID = storage.getThreadIdForMms(databaseMessageID) - val openGroupV2 = storage.getV2OpenGroup(threadID.toString()) + val openGroupV2 = storage.getV2OpenGroup(threadID) 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 if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { FileInputStream(tempFile) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 2928b3a9dc..4f7e7ea57c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -6,13 +6,12 @@ import com.esotericsoftware.kryo.io.Output import nl.komponents.kovenant.Promise import okio.Buffer 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.messages.Message import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender 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.messages.SignalServiceAttachmentStream 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 attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID) ?: return handleFailure(Error.NoAttachment) - val v2OpenGroup = storage.getV2OpenGroup(threadID) - val v1OpenGroup = storage.getOpenGroup(threadID) + val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong()) if (v2OpenGroup != null) { val keyAndResult = upload(attachment, v2OpenGroup.server, false) { OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server) } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) - } else if (v1OpenGroup == null) { - val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) { + } else { + val keyAndResult = upload(attachment, FileServerAPIV2.server, true) { FileServerAPIV2.upload(it) } 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) { if (e == Error.NoAttachment) { this.handlePermanentFailure(e) - } else if (e is DotNetAPI.Error && !e.isRetryable) { - this.handlePermanentFailure(e) } else { this.handleFailure(e) } } } - private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { // Key val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) // Length @@ -112,10 +102,10 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val id = upload(data).get() val digest = drb.transmittedDigest // 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.") delegate?.handleJobSucceeded(this) MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) @@ -158,7 +148,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess .putString(THREAD_ID_KEY, threadID) .putByteArray(MESSAGE_KEY, serializedMessage) .putString(MESSAGE_SEND_JOB_ID_KEY, messageSendJobID) - .build(); + .build() } override fun getFactoryKey(): String { @@ -172,7 +162,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess val kryo = Kryo() kryo.isRegistrationRequired = false val input = Input(serializedMessage) - val message: Message = kryo.readObject(input, Message::class.java) + val message = kryo.readObject(input, Message::class.java) input.close() return AttachmentUploadJob( data.getLong(ATTACHMENT_ID_KEY), diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 71a28ffdb4..87fead7c58 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -88,17 +88,16 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { override fun serialize(): Data { val kryo = Kryo() kryo.isRegistrationRequired = false - val output = Output(ByteArray(4096), MAX_BUFFER_SIZE) // Message - kryo.writeClassAndObject(output, message) - output.close() - val serializedMessage = output.toBytes() - output.clear() + val messageOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE) + kryo.writeClassAndObject(messageOutput, message) + messageOutput.close() + val serializedMessage = messageOutput.toBytes() // Destination - kryo.writeClassAndObject(output, destination) - output.close() - val serializedDestination = output.toBytes() - output.clear() + val destinationOutput = Output(ByteArray(4096), MAX_BUFFER_SIZE) + kryo.writeClassAndObject(destinationOutput, destination) + destinationOutput.close() + val serializedDestination = destinationOutput.toBytes() // Serialize return Data.Builder() .putByteArray(MESSAGE_KEY, serializedMessage) @@ -116,6 +115,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { val serializedMessage = data.getByteArray(MESSAGE_KEY) val serializedDestination = data.getByteArray(DESTINATION_KEY) val kryo = Kryo() + kryo.isRegistrationRequired = false // Message val messageInput = Input(serializedMessage) val message: Message diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index f11f194891..5b3225b117 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -67,7 +67,9 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { val output = Output(serializedMessage) kryo.writeObject(output, message) output.close() - return Data.Builder().putByteArray(MESSAGE_KEY, serializedMessage).build(); + return Data.Builder() + .putByteArray(MESSAGE_KEY, serializedMessage) + .build(); } override fun getFactoryKey(): String { @@ -81,7 +83,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { val kryo = Kryo() kryo.isRegistrationRequired = false val input = Input(serializedMessage) - val message: SnodeMessage = kryo.readObject(input, SnodeMessage::class.java) + val message = kryo.readObject(input, SnodeMessage::class.java) input.close() return NotifyPNServerJob(message) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt index 5ee6452112..d4b1b35c68 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/mentions/MentionsManager.kt @@ -30,14 +30,8 @@ class MentionsManager(private val userPublicKey: String, private val userDatabas // Prepare val cache = userPublicKeyCache[threadID] ?: return listOf() // Gather candidates - val publicChat = MessagingModuleConfiguration.shared.messageDataProvider.getOpenGroup(threadID) var candidates: List = cache.mapNotNull { publicKey -> - val displayName: String? - if (publicChat != null) { - displayName = userDatabase.getServerDisplayName(publicChat.id, publicKey) - } else { - displayName = userDatabase.getDisplayName(publicKey) - } + val displayName = userDatabase.getDisplayName(publicKey) if (displayName == null) { return@mapNotNull null } if (displayName.startsWith("Anonymous")) { return@mapNotNull null } Mention(publicKey, displayName) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index 9a65978afc..ac6d97874c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -2,7 +2,6 @@ package org.session.libsession.messaging.messages import org.session.libsession.messaging.MessagingModuleConfiguration 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.GroupUtil import org.session.libsignal.utilities.toHexString @@ -15,9 +14,6 @@ sealed class Destination { class ClosedGroup(var groupPublicKey: String) : Destination() { 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() { internal constructor(): this("", "") } @@ -36,10 +32,8 @@ sealed class Destination { } address.isOpenGroup -> { val storage = MessagingModuleConfiguration.shared.storage - val threadID = storage.getThreadID(address.contactIdentifier())!! - when (val openGroup = storage.getV2OpenGroup(threadID) ?: storage.getOpenGroup(threadID)) { - is org.session.libsession.messaging.open_groups.OpenGroup - -> Destination.OpenGroup(openGroup.channel, openGroup.server) + val threadID = storage.getThreadId(address)!! + when (val openGroup = storage.getV2OpenGroup(threadID)) { is org.session.libsession.messaging.open_groups.OpenGroupV2 -> Destination.OpenGroupV2(openGroup.room, openGroup.server) else -> throw Exception("Missing open group for thread with ID: $threadID.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt index 6eefd63c7b..cc49cec65b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt @@ -114,10 +114,9 @@ class ConfigurationMessage(var closedGroups: List, var openGroups: closedGroups.add(closedGroup) } if (group.isOpenGroup) { - val threadID = storage.getThreadID(group.encodedId) ?: continue - val openGroup = storage.getOpenGroup(threadID) + val threadID = storage.getThreadId(group.encodedId) ?: continue val openGroupV2 = storage.getV2OpenGroup(threadID) - val shareUrl = openGroup?.server ?: openGroupV2?.joinURL ?: continue + val shareUrl = openGroupV2?.joinURL ?: continue openGroups.add(shareUrl) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt deleted file mode 100644 index a39b22ad35..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt +++ /dev/null @@ -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 { - return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable ) - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt deleted file mode 100644 index fa3d32c504..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPI.kt +++ /dev/null @@ -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>> = 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 { - 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, Exception> { - Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.") - val storage = MessagingModuleConfiguration.shared.storage - val parameters = mutableMapOf( "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> - 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>).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>).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>).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, Exception> { - Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.") - val storage = MessagingModuleConfiguration.shared.storage - val parameters = mutableMapOf() - 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>).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 { - val deferred = deferred() - 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 { - 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, channel: Long, server: String, isSentByUser: Boolean): Promise, 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, Exception> { - return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then { json -> - try { - @Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List - 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 { - 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> - 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 { - 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 { - 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 { - 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, server: String): Promise, Exception> { - return getUserProfiles(publicKeys, server, false).map { json -> - val mapping = mutableMapOf() - 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 { - 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 { - return setProfilePicture(server, Base64.encodeBytes(profileKey), url) - } - - fun setProfilePicture(server: String, profileKey: String, url: String?): Promise { - 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 -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt index a18e25850d..12e5fc1364 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt @@ -31,8 +31,8 @@ object OpenGroupAPIV2 { private val curve = Curve25519.getInstance(Curve25519.BEST) val defaultRooms = MutableSharedFlow>(replay = 1) - private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - const val DEFAULT_SERVER = "http://116.203.70.33" + private const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" + const val defaultServer = "http://116.203.70.33" sealed class Error(message: String) : Exception(message) { object Generic : Error("An error occurred.") @@ -45,7 +45,7 @@ object OpenGroupAPIV2 { 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?) @@ -60,7 +60,7 @@ object OpenGroupAPIV2 { ) { companion object { - val EMPTY = MessageDeletion() + val empty = MessageDeletion() } } @@ -125,9 +125,7 @@ object OpenGroupAPIV2 { if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) { val storage = MessagingModuleConfiguration.shared.storage if (request.room != null) { - storage.removeAuthToken("${request.server}.${request.room}") - } else { - storage.removeAuthToken(request.server) + storage.removeAuthToken(request.room, request.server) } } } @@ -237,7 +235,7 @@ object OpenGroupAPIV2 { fun getMessages(room: String, server: String): Promise, Exception> { val storage = MessagingModuleConfiguration.shared.storage val queryParameters = mutableMapOf() - storage.getLastMessageServerId(room, server)?.let { lastId -> + storage.getLastMessageServerID(room, server)?.let { lastId -> queryParameters += "from_server_id" to lastId.toString() } 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>): List { val storage = MessagingModuleConfiguration.shared.storage - val lastMessageServerID = storage.getLastMessageServerId(room, server) ?: 0 + val lastMessageServerID = storage.getLastMessageServerID(room, server) ?: 0 var currentLastMessageServerID = lastMessageServerID val messages = rawMessages.mapNotNull { json -> json as Map @@ -274,7 +272,7 @@ object OpenGroupAPIV2 { null } } - storage.setLastMessageServerId(room, server, currentLastMessageServerID) + storage.setLastMessageServerID(room, server, currentLastMessageServerID) return messages } // endregion @@ -291,7 +289,7 @@ object OpenGroupAPIV2 { fun getDeletedMessages(room: String, server: String): Promise, Exception> { val storage = MessagingModuleConfiguration.shared.storage val queryParameters = mutableMapOf() - storage.getLastDeletionServerId(room, server)?.let { last -> + storage.getLastDeletionServerID(room, server)?.let { last -> queryParameters["from_server_id"] = last.toString() } 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 idsAsString = JsonUtil.toJson(json["ids"]) val serverIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.ParsingFailed - val lastMessageServerId = storage.getLastDeletionServerId(room, server) ?: 0 - val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.EMPTY + val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0 + val serverID = serverIDs.maxByOrNull {it.id } ?: MessageDeletion.empty if (serverID.id > lastMessageServerId) { - storage.setLastDeletionServerId(room, server, serverID.id) + storage.setLastDeletionServerID(room, server, serverID.id) } serverIDs } @@ -361,8 +359,8 @@ object OpenGroupAPIV2 { CompactPollRequest( roomID = room, authToken = authToken, - fromDeletionServerID = storage.getLastDeletionServerId(room, server), - fromMessageServerID = storage.getLastMessageServerId(room, server) + fromDeletionServerID = storage.getLastDeletionServerID(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 )) @@ -386,10 +384,10 @@ object OpenGroupAPIV2 { val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java) val idsAsString = JsonUtil.toJson(json["deletions"]) val deletedServerIDs = JsonUtil.fromJson>(idsAsString, type) ?: throw Error.ParsingFailed - val lastDeletionServerID = storage.getLastDeletionServerId(roomID, server) ?: 0 - val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.EMPTY + val lastDeletionServerID = storage.getLastDeletionServerID(roomID, server) ?: 0 + val serverID = deletedServerIDs.maxByOrNull { it.id } ?: MessageDeletion.empty if (serverID.id > lastDeletionServerID) { - storage.setLastDeletionServerId(roomID, server, serverID.id) + storage.setLastDeletionServerID(roomID, server, serverID.id) } // Messages val rawMessages = json["messages"] as? List> ?: return@mapNotNull null @@ -405,8 +403,8 @@ object OpenGroupAPIV2 { fun getDefaultRoomsIfNeeded(): Promise, Exception> { val storage = MessagingModuleConfiguration.shared.storage - storage.setOpenGroupPublicKey(DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY) - return getAllRooms(DEFAULT_SERVER).map { groups -> + storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey) + return getAllRooms(defaultServer).map { groups -> val earlyGroups = groups.map { group -> DefaultGroup(group.id, group.name, null) } @@ -417,7 +415,7 @@ object OpenGroupAPIV2 { } } val images = groups.map { group -> - group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER) + group.id to downloadOpenGroupProfilePicture(group.id, defaultServer) }.toMap() groups.map { group -> val image = try { diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt deleted file mode 100644 index bc4f465170..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt +++ /dev/null @@ -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, - 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) - : this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments as MutableList, 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 { - val value = mutableMapOf("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 -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index d08bcfcfad..23b0df99c0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -54,10 +54,11 @@ object MessageSender { // Convenience fun send(message: Message, destination: Destination): Promise { - if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) { + if (destination is Destination.OpenGroupV2) { return sendToOpenGroupDestination(destination, message) + } else { + return sendToSnodeDestination(destination, message) } - return sendToSnodeDestination(destination, message) } // One-on-One Chats & Closed Groups @@ -84,7 +85,7 @@ object MessageSender { when (destination) { is Destination.Contact -> message.recipient = destination.publicKey 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 if (!message.isValid()) { throw Error.InvalidMessage } @@ -122,7 +123,7 @@ object MessageSender { val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!! 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 val kind: SignalServiceProtos.Envelope.Type @@ -136,7 +137,7 @@ object MessageSender { kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE 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) // Send the result @@ -203,27 +204,6 @@ object MessageSender { try { when (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 -> { message.recipient = "${destination.server}.${destination.room}" val server = destination.server @@ -275,7 +255,7 @@ object MessageSender { // Track the open group server message ID if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) { 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) { storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index aceeda6635..6f8b0a6647 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences @@ -71,6 +72,8 @@ fun MessageSender.create(name: String, members: Collection): Promise> { - if (isPolling) { return listOf() } - isPolling = true - return poll() - } - - public fun stopIfNeeded() { - isPolling = false - handler.removeCallbacks(task) - } - // endregion - - // region Private API - private fun poll(): List> { - if (!isPolling) { return listOf() } - val storage = MessagingModuleConfiguration.shared.storage - val publicKeys = storage.getAllActiveClosedGroupPublicKeys() - return publicKeys.map { publicKey -> - val promise = SnodeAPI.getSwarm(publicKey).bind { swarm -> - val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure - if (!isPolling) { throw PollingCanceledException() } - SnodeAPI.getRawMessages(snode, publicKey).map {SnodeAPI.parseRawMessagesResponse(it, snode, publicKey) } - } - promise.successBackground { messages -> - if (!storage.isGroupActive(publicKey)) { return@successBackground } - messages.forEach { envelope -> - val job = MessageReceiveJob(envelope.toByteArray()) - JobQueue.shared.add(job) - } - } - promise.fail { - Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.") - } - promise.map { } - } - } - // endregion -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt new file mode 100644 index 0000000000..c0da73798c --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt @@ -0,0 +1,115 @@ +package org.session.libsession.messaging.sending_receiving.pollers + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.jobs.MessageReceiveJob +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.GroupUtil +import org.session.libsignal.crypto.getRandomElementOrNull +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.successBackground +import java.util.* +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.math.min + +class ClosedGroupPollerV2 { + private val executorService = Executors.newScheduledThreadPool(4) + private var isPolling = mutableMapOf() + private var futures = mutableMapOf>() + + private fun isPolling(groupPublicKey: String): Boolean { + return isPolling[groupPublicKey] ?: false + } + + companion object { + private val minPollInterval = 4 * 1000 + private val maxPollInterval = 2 * 60 * 1000 + + @JvmStatic + val shared = ClosedGroupPollerV2() + } + + class InsufficientSnodesException() : Exception("No snodes left to poll.") + class PollingCanceledException() : Exception("Polling canceled.") + + fun start() { + val storage = MessagingModuleConfiguration.shared.storage + val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + allGroupPublicKeys.forEach { startPolling(it) } + } + + fun startPolling(groupPublicKey: String) { + if (isPolling(groupPublicKey)) { return } + setUpPolling(groupPublicKey) + isPolling[groupPublicKey] = true + } + + fun stop() { + val storage = MessagingModuleConfiguration.shared.storage + val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys() + allGroupPublicKeys.forEach { stopPolling(it) } + } + + fun stopPolling(groupPublicKey: String) { + futures[groupPublicKey]?.cancel(false) + isPolling[groupPublicKey] = false + } + + private fun setUpPolling(groupPublicKey: String) { + poll(groupPublicKey).success { + pollRecursively(groupPublicKey) + }.fail { + // The error is logged in poll(_:) + pollRecursively(groupPublicKey) + } + } + + private fun pollRecursively(groupPublicKey: String) { + if (!isPolling(groupPublicKey)) { return } + // Get the received date of the last message in the thread. If we don't have any messages yet, pick some + // reasonable fake time interval to use instead. + val storage = MessagingModuleConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val threadID = storage.getThreadId(groupID) ?: return + val lastUpdated = storage.getLastUpdated(threadID) + val timeSinceLastMessage = if (lastUpdated != -1L) Date().time - lastUpdated else 5 * 60 * 1000 + val minPollInterval = Companion.minPollInterval + val limit: Long = 12 * 60 * 60 * 1000 + val a = (Companion.maxPollInterval - minPollInterval).toDouble() / limit.toDouble() + val nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval + Log.d("Loki", "Next poll interval for closed group with public key: $groupPublicKey is ${nextPollInterval / 1000} s.") + executorService?.schedule({ + poll(groupPublicKey).success { + pollRecursively(groupPublicKey) + }.fail { + // The error is logged in poll(_:) + pollRecursively(groupPublicKey) + } + }, nextPollInterval.toLong(), TimeUnit.MILLISECONDS) + } + + fun poll(groupPublicKey: String): Promise { + if (!isPolling(groupPublicKey)) { return Promise.of(Unit) } + val promise = SnodeAPI.getSwarm(groupPublicKey).bind { swarm -> + val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure + if (!isPolling(groupPublicKey)) { throw PollingCanceledException() } + SnodeAPI.getRawMessages(snode, groupPublicKey).map { SnodeAPI.parseRawMessagesResponse(it, snode, groupPublicKey) } + } + promise.success { envelopes -> + if (!isPolling(groupPublicKey)) { return@success } + envelopes.forEach { envelope -> + val job = MessageReceiveJob(envelope.toByteArray()) + JobQueue.shared.add(job) + } + } + promise.fail { + Log.d("Loki", "Polling failed for closed group with public key: $groupPublicKey due to error: $it.") + } + return promise.map { } + } +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt deleted file mode 100644 index d970a74c39..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ /dev/null @@ -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>() - - // region Convenience - private val userHexEncodedPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: "" - private var displayNameUpdates = setOf() - // 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 { - return pollForNewMessagesInternal(false) - } - - private fun pollForNewMessagesInternal(isBackgroundPoll: Boolean): Promise { - if (isPollOngoing) { return Promise.of(Unit) } - isPollOngoing = true - val deferred = deferred() - // 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 -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt index 320c3afd97..a6649c5150 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt @@ -77,7 +77,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService: val storage = MessagingModuleConfiguration.shared.storage val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider 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 messageID = dataProvider.getMessageID(serverID, threadID) if (messageID == null) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt deleted file mode 100644 index fc9751f998..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt +++ /dev/null @@ -1,269 +0,0 @@ -package org.session.libsession.messaging.utilities - -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.functional.map -import nl.komponents.kovenant.then -import okhttp3.* - -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.messaging.file_server.FileServerAPI - -import org.session.libsignal.crypto.DiffieHellman -import org.session.libsignal.streams.ProfileCipherOutputStream -import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException -import org.session.libsignal.exceptions.PushNetworkException -import org.session.libsignal.streams.StreamDetails -import org.session.libsignal.utilities.ProfileAvatarData -import org.session.libsignal.utilities.PushAttachmentData -import org.session.libsignal.streams.DigestingRequestBody -import org.session.libsignal.streams.ProfileCipherOutputStreamFactory -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.JsonUtil -import org.session.libsignal.utilities.HTTP -import org.session.libsignal.utilities.* -import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Log -import java.util.* - -/** - * Base class that provides utilities for .NET based APIs. - */ -open class DotNetAPI { - - internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH } - - // Error - internal sealed class Error(val description: String) : Exception(description) { - object Generic : Error("An error occurred.") - object InvalidURL : Error("Invalid URL.") - object ParsingFailed : Error("Invalid file server response.") - object SigningFailed : Error("Couldn't sign message.") - object EncryptionFailed : Error("Couldn't encrypt file.") - object DecryptionFailed : Error("Couldn't decrypt file.") - object MaxFileSizeExceeded : Error("Maximum file size exceeded.") - object TokenExpired: Error("Token expired.") // Session Android - - internal val isRetryable: Boolean = false - } - - companion object { - private val authTokenRequestCache = hashMapOf>() - } - - public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?) - - fun getAuthToken(server: String): Promise { - 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 { - Log.d("Loki", "Requesting auth token for server: $server.") - val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic - val parameters: Map = 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 { - 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 = mapOf(), isJSONRequired: Boolean = true): Promise, Exception> { - fun execute(token: String?): Promise, 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, server: String, includeAnnotations: Boolean): Promise>, 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> - 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, Exception> { - val annotation = mutableMapOf( "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 { - val promise: Promise, 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 } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index f3d062ab09..d2e33b64d9 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -6,7 +6,7 @@ import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map 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.libsignal.utilities.Log import org.session.libsignal.utilities.Base64 @@ -307,7 +307,7 @@ object OnionRequestAPI { val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2" val finalEncryptionResult = result.finalEncryptionResult 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.") } @Suppress("NAME_SHADOWING") val parameters = mapOf( diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index 9aee38647e..8ccb659dbb 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -1,15 +1,9 @@ package org.session.libsession.utilities 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.snode.OnionRequestAPI import org.session.libsignal.utilities.Log 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.* object DownloadUtilities { @@ -18,14 +12,14 @@ object DownloadUtilities { * Blocks the calling thread. */ @JvmStatic - fun downloadFile(destination: File, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { + fun downloadFile(destination: File, url: String) { val outputStream = FileOutputStream(destination) // Throws var remainingAttempts = 4 var exception: Exception? = null while (remainingAttempts > 0) { remainingAttempts -= 1 try { - downloadFile(outputStream, url, maxSize, listener) + downloadFile(outputStream, url) exception = null break } catch (e: Exception) { @@ -39,66 +33,16 @@ object DownloadUtilities { * Blocks the calling thread. */ @JvmStatic - fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { - - if (url.contains(FileServerAPIV2.SERVER) || url.contains(FileServerAPIV2.OLD_SERVER)) { - val httpUrl = HttpUrl.parse(url)!! - val fileId = httpUrl.pathSegments().last() - val useOldServer = url.contains(FileServerAPIV2.OLD_SERVER) - try { - FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let { - outputStream.write(it) - } - } catch (e: Exception) { - Log.e("Loki", "Couln't download attachment due to error",e) - throw e - } - } else { - // We need to throw a PushNetworkException or NonSuccessfulResponseCodeException - // because the underlying Signal logic requires these to work correctly - val oldPrefixedHost = "https://" + HttpUrl.get(url).host() - var newPrefixedHost = oldPrefixedHost - if (oldPrefixedHost.contains(FileServerAPI.fileStorageBucketURL)) { - newPrefixedHost = FileServerAPI.shared.server - } - // Edge case that needs to work: https://file-static.lokinet.org/i1pNmpInq3w9gF3TP8TFCa1rSo38J6UM - // → https://file.getsession.org/loki/v1/f/XLxogNXVEIWHk14NVCDeppzTujPHxu35 - val fileID = url.substringAfter(oldPrefixedHost).substringAfter("/f/") - val sanitizedURL = "$newPrefixedHost/loki/v1/f/$fileID" - val request = Request.Builder().url(sanitizedURL).get() - try { - val serverPublicKey = if (newPrefixedHost.contains(FileServerAPI.shared.server)) FileServerAPI.fileServerPublicKey - else FileServerAPI.shared.getPublicKeyForOpenGroupServer(newPrefixedHost).get() - val json = OnionRequestAPI.sendOnionRequest(request.build(), newPrefixedHost, serverPublicKey, isJSONRequired = false).get() - val result = json["result"] as? String - if (result == null) { - Log.d("Loki", "Couldn't parse attachment from: $json.") - throw PushNetworkException("Missing response body.") - } - val body = Base64.decode(result) - if (body.size > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") - } - body.inputStream().use { input -> - val buffer = ByteArray(32768) - var count = 0 - var bytes = input.read(buffer) - while (bytes >= 0) { - outputStream.write(buffer, 0, bytes) - count += bytes - if (count > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") - } - listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) - bytes = input.read(buffer) - } - } - } catch (e: Exception) { - Log.e("Loki", "Couldn't download attachment due to error", e) - throw if (e is NonSuccessfulResponseCodeException) e else PushNetworkException(e) + fun downloadFile(outputStream: OutputStream, urlAsString: String) { + val url = HttpUrl.parse(urlAsString)!! + val fileID = url.pathSegments().last() + try { + FileServerAPIV2.download(fileID.toLong()).get().let { + outputStream.write(it) } + } catch (e: Exception) { + Log.e("Loki", "Couldn't download attachment.", e) + throw e } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt index df9802417f..47223c8096 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt @@ -36,7 +36,7 @@ object ProfilePictureUtilities { deferred.reject(e) } TextSecurePreferences.setLastProfilePictureUpload(context, Date().time) - val url = "${FileServerAPIV2.SERVER}/files/$id" + val url = "${FileServerAPIV2.server}/files/$id" TextSecurePreferences.setProfilePictureURL(context, url) deferred.resolve(Unit) } diff --git a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt index 68c3308757..86af00a32d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/SSKEnvironment.kt @@ -34,7 +34,6 @@ class SSKEnvironment( fun setProfilePictureURL(context: Context, recipient: Recipient, profilePictureURL: String) fun setProfileKey(context: Context, recipient: Recipient, profileKey: ByteArray) fun setUnidentifiedAccessMode(context: Context, recipient: Recipient, unidentifiedAccessMode: Recipient.UnidentifiedAccessMode) - fun updateOpenGroupProfilePicturesIfNeeded(context: Context) fun getDisplayName(context: Context, recipient: Recipient): String? } diff --git a/libsession/src/main/java/org/session/libsession/utilities/UploadResult.kt b/libsession/src/main/java/org/session/libsession/utilities/UploadResult.kt new file mode 100644 index 0000000000..6be8b4437a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/UploadResult.kt @@ -0,0 +1,3 @@ +package org.session.libsession.utilities + +data class UploadResult(val id: Long, val url: String, val digest: ByteArray?) \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/database/LokiUserDatabaseProtocol.kt b/libsignal/src/main/java/org/session/libsignal/database/LokiUserDatabaseProtocol.kt index 1f57a6ff51..e22ecb1059 100644 --- a/libsignal/src/main/java/org/session/libsignal/database/LokiUserDatabaseProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/database/LokiUserDatabaseProtocol.kt @@ -3,6 +3,5 @@ package org.session.libsignal.database interface LokiUserDatabaseProtocol { fun getDisplayName(publicKey: String): String? - fun getServerDisplayName(serverID: String, publicKey: String): String? fun getProfilePictureURL(publicKey: String): String? }