mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into polling-limit-after-inactivity
This commit is contained in:
commit
becc3c7278
@ -143,8 +143,8 @@ dependencies {
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.2'
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 170
|
||||
def canonicalVersionName = "1.10.7"
|
||||
def canonicalVersionCode = 171
|
||||
def canonicalVersionName = "1.10.8"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['armeabi-v7a' : 1,
|
||||
|
@ -22,19 +22,15 @@ 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.ClosedGroupPollerV2;
|
||||
import org.session.libsession.messaging.sending_receiving.pollers.Poller;
|
||||
@ -174,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);
|
||||
}
|
||||
@ -401,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;
|
||||
@ -490,19 +471,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
});
|
||||
}
|
||||
|
||||
public void updateOpenGroupProfilePicturesIfNeeded() {
|
||||
AsyncTask.execute(() -> {
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||
String url = TextSecurePreferences.getProfilePictureURL(this);
|
||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||
for (String server : servers) {
|
||||
if (profileKey != null) {
|
||||
OpenGroupAPI.setProfilePicture(server, profileKey, url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void clearAllData(boolean isMigratingToV2KeyPair) {
|
||||
String token = TextSecurePreferences.getFCMToken(this);
|
||||
if (token != null && !token.isEmpty()) {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
@ -193,7 +190,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;
|
||||
@ -204,7 +200,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;
|
||||
|
||||
@ -377,12 +372,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")) {
|
||||
@ -1419,13 +1410,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())) {
|
||||
@ -2380,13 +2365,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");
|
||||
|
@ -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<String> 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<Long> serverIDs = new ArrayList<>();
|
||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||
ArrayList<Long> 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 -> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.MessagingModuleConfiguration
|
||||
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 getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? {
|
||||
@ -108,15 +106,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return registrationID
|
||||
}
|
||||
|
||||
override fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> {
|
||||
override fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long> {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
|
||||
return database.insertAttachments(messageId, databaseAttachments)
|
||||
return database.insertAttachments(messageID, databaseAttachments)
|
||||
}
|
||||
|
||||
override fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment> {
|
||||
override fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment> {
|
||||
val database = DatabaseFactory.getAttachmentDatabase(context)
|
||||
return database.getAttachmentsForMessage(messageId)
|
||||
return database.getAttachmentsForMessage(messageID)
|
||||
}
|
||||
|
||||
override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): Long? {
|
||||
@ -186,7 +184,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
return messageID
|
||||
}
|
||||
|
||||
// JOBS
|
||||
override fun persistJob(job: Job) {
|
||||
DatabaseFactory.getSessionJobDatabase(context).persistJob(job)
|
||||
}
|
||||
@ -220,20 +217,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)
|
||||
@ -249,30 +232,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)
|
||||
}
|
||||
@ -281,63 +249,31 @@ 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 null if limit is set on open groups polling
|
||||
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
||||
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 null if limit is set on open groups polling
|
||||
if (TextSecurePreferences.isOpenGroupPollingLimit(context)) return null
|
||||
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)
|
||||
}
|
||||
|
||||
@ -345,34 +281,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)
|
||||
}
|
||||
@ -399,15 +316,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
|
||||
@ -466,7 +374,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getGroupDatabase(context).setActive(groupID, value)
|
||||
}
|
||||
|
||||
override fun getZombieMember(groupID: String): Set<String> {
|
||||
override fun getZombieMembers(groupID: String): Set<String> {
|
||||
return DatabaseFactory.getGroupDatabase(context).getGroupZombieMembers(groupID).map { it.address.serialize() }.toHashSet()
|
||||
}
|
||||
|
||||
@ -478,7 +386,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
|
||||
}
|
||||
|
||||
override fun updateZombieMembers(groupID: String, members: List<Address>) {
|
||||
override fun setZombieMembers(groupID: String, members: List<Address>) {
|
||||
DatabaseFactory.getGroupDatabase(context).updateZombieMembers(groupID, members)
|
||||
}
|
||||
|
||||
@ -544,39 +452,18 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
|
||||
}
|
||||
|
||||
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
|
||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().mapValues { (_,chat)->
|
||||
OpenGroup(chat.channel, chat.server, chat.displayName, chat.isDeletable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
|
||||
}
|
||||
|
||||
override fun addOpenGroup(serverUrl: String, channel: Long) {
|
||||
val httpUrl = HttpUrl.parse(serverUrl) ?: return
|
||||
if (httpUrl.queryParameterNames().contains("public_key")) {
|
||||
// open group v2
|
||||
val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply {
|
||||
if (httpUrl.port() != 80 || httpUrl.port() != 443) {
|
||||
// non-standard port, add to server
|
||||
this.port(httpUrl.port())
|
||||
}
|
||||
}.build()
|
||||
val room = httpUrl.pathSegments().firstOrNull() ?: return
|
||||
val publicKey = httpUrl.queryParameter("public_key") ?: return
|
||||
|
||||
OpenGroupManager.add(server.toString().removeSuffix("/"), room, publicKey, context)
|
||||
} else {
|
||||
// TODO: No longer supported so let's remove this code
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAllGroups(): List<GroupRecord> {
|
||||
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)
|
||||
@ -601,15 +488,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
}
|
||||
}
|
||||
|
||||
override fun getThreadIdFor(address: Address): Long? {
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
||||
return if (threadID < 0) null else threadID
|
||||
override fun getThreadId(publicKeyOrOpenGroupID: String): Long? {
|
||||
val address = Address.fromSerialized(publicKeyOrOpenGroupID)
|
||||
return getThreadId(address)
|
||||
}
|
||||
|
||||
fun foo() {
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||
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
|
||||
}
|
||||
|
||||
override fun getThreadIdForMms(mmsId: Long): Long {
|
||||
@ -621,22 +512,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? {
|
||||
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
|
||||
}
|
||||
@ -645,10 +520,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(publicKey, newName)
|
||||
}
|
||||
|
||||
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
|
||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverID, publicKey)
|
||||
}
|
||||
|
||||
override fun getProfilePictureURL(publicKey: String): String? {
|
||||
return DatabaseFactory.getLokiUserDatabase(context).getProfilePictureURL(publicKey)
|
||||
}
|
||||
@ -696,7 +567,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)
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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<ActivityBackupRestoreBinding>(this, R.layout.activity_backup_restore)
|
||||
viewBinding.lifecycleOwner = this
|
||||
viewBinding.viewModel = viewModel
|
||||
// val viewBinding = DataBindingUtil.setContentView<ActivityBackupRestoreBinding>(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) {
|
||||
|
@ -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.messaging.sending_receiving.pollers.OpenGroupPollerV2
|
||||
import org.session.libsession.utilities.*
|
||||
@ -344,16 +343,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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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<Promise<*, Exception>>()
|
||||
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)
|
||||
|
@ -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
|
||||
@ -49,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
|
||||
@ -95,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)
|
||||
}
|
||||
}
|
@ -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,26 +56,8 @@ 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!!)
|
||||
@ -86,7 +67,5 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
||||
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,13 @@ package org.thoughtcrime.securesms.loki.database
|
||||
import android.content.ContentValues
|
||||
import android.content.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<Long, OpenGroup> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
var cursor: Cursor? = null
|
||||
val result = mutableMapOf<Long, OpenGroup>()
|
||||
try {
|
||||
cursor = database.rawQuery("select * from $publicChatTable", null)
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val threadID = cursor.getLong(threadID)
|
||||
val string = cursor.getString(publicChat)
|
||||
val publicChat = OpenGroup.fromJSON(string)
|
||||
if (publicChat != null) {
|
||||
result[threadID] = publicChat
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Do nothing
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
var cursor: Cursor? = null
|
||||
@ -79,20 +52,6 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
return result
|
||||
}
|
||||
|
||||
fun getAllPublicChatServers(): Set<String> {
|
||||
return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) }
|
||||
}
|
||||
|
||||
fun getPublicChat(threadID: Long): OpenGroup? {
|
||||
if (threadID < 0) { return null }
|
||||
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||
val publicChatAsJSON = cursor.getString(publicChat)
|
||||
OpenGroup.fromJSON(publicChatAsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
||||
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()))
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -27,15 +27,12 @@ object MentionUtilities {
|
||||
var matcher = pattern.matcher(text)
|
||||
val mentions = mutableListOf<Tuple2<Range<Int>, 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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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<Mention>()
|
||||
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<Mention>, 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
|
||||
|
@ -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
|
||||
|
@ -29,7 +29,7 @@ class ProfilePictureView : RelativeLayout {
|
||||
var additionalDisplayName: String? = null
|
||||
var isRSSFeed = false
|
||||
var isLarge = false
|
||||
private val imagesCached = mutableSetOf<String>()
|
||||
private val profilePicturesCached = mutableMapOf<String,String?>()
|
||||
|
||||
// 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
|
||||
}
|
@ -52,11 +52,7 @@ class UserView : LinearLayout {
|
||||
if (publicKey == null || publicKey.isBlank()) {
|
||||
return null
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,4 @@ class ProfileManager: SSKEnvironment.ProfileManagerProtocol {
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
database.setUnidentifiedAccessMode(recipient, unidentifiedAccessMode)
|
||||
}
|
||||
|
||||
override fun updateOpenGroupProfilePicturesIfNeeded(context: Context) {
|
||||
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
|
||||
}
|
||||
}
|
@ -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<Long, Boolean>?
|
||||
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<Long, Boolean>?
|
||||
fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List<Attachment>
|
||||
fun getMessageBodyFor(timestamp: Long, author: String): String
|
||||
|
||||
fun getAttachmentIDsFor(messageID: Long): List<Long>
|
||||
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
|
||||
|
||||
fun getOpenGroup(threadID: Long): OpenGroup?
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package org.session.libsession.database
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import org.session.libsession.messaging.jobs.AttachmentUploadJob
|
||||
@ -9,7 +8,6 @@ import org.session.libsession.messaging.jobs.MessageSendJob
|
||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||
import org.session.libsession.messaging.messages.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
|
||||
@ -18,6 +16,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||
import org.session.libsession.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
|
||||
@ -32,14 +31,12 @@ interface StorageProtocol {
|
||||
fun getUserDisplayName(): String?
|
||||
fun getUserProfileKey(): ByteArray?
|
||||
fun getUserProfilePictureURL(): String?
|
||||
fun setUserProfilePictureUrl(newProfilePicture: String)
|
||||
|
||||
fun setUserProfilePictureURL(newValue: String)
|
||||
fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray?
|
||||
fun getDisplayNameForRecipient(recipientPublicKey: String): String?
|
||||
fun setProfileKeyForRecipient(recipientPublicKey: String, profileKey: ByteArray)
|
||||
|
||||
// Signal Protocol
|
||||
|
||||
// Signal
|
||||
fun getOrGenerateRegistrationID(): Int
|
||||
|
||||
// Jobs
|
||||
@ -59,48 +56,40 @@ interface StorageProtocol {
|
||||
|
||||
// Open Groups
|
||||
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
||||
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<Long>
|
||||
fun addReceivedMessageTimestamp(timestamp: Long)
|
||||
fun removeReceivedMessageTimestamps(timestamps: Set<Long>)
|
||||
// Returns the IDs of the saved attachments.
|
||||
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageId: Long): List<DatabaseAttachment>
|
||||
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
|
||||
/**
|
||||
* Returns the IDs of the saved attachments.
|
||||
*/
|
||||
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
|
||||
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
|
||||
fun getMessageIdInDatabase(timestamp: Long, author: String): Long? // TODO: This is a weird name
|
||||
fun markAsSent(timestamp: Long, author: String)
|
||||
fun markUnidentified(timestamp: Long, author: String)
|
||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||
@ -110,11 +99,10 @@ interface StorageProtocol {
|
||||
fun createGroup(groupID: String, title: String?, members: List<Address>, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List<Address>, formationTimestamp: Long)
|
||||
fun isGroupActive(groupPublicKey: String): Boolean
|
||||
fun setActive(groupID: String, value: Boolean)
|
||||
fun getZombieMember(groupID: String): Set<String>
|
||||
fun getZombieMembers(groupID: String): Set<String>
|
||||
fun removeMember(groupID: String, member: Address)
|
||||
fun updateMembers(groupID: String, members: List<Address>)
|
||||
fun updateZombieMembers(groupID: String, members: List<Address>)
|
||||
// Closed Group
|
||||
fun setZombieMembers(groupID: String, members: List<Address>)
|
||||
fun getAllClosedGroupPublicKeys(): Set<String>
|
||||
fun getAllActiveClosedGroupPublicKeys(): Set<String>
|
||||
fun addClosedGroupPublicKey(groupPublicKey: String)
|
||||
@ -138,58 +126,27 @@ 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)
|
||||
|
||||
// Loki User
|
||||
// Contacts
|
||||
fun getDisplayName(publicKey: String): String?
|
||||
fun setDisplayName(publicKey: String, newName: String)
|
||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
||||
fun getProfilePictureURL(publicKey: String): String?
|
||||
|
||||
// Recipient
|
||||
fun getRecipientSettings(address: Address): RecipientSettings?
|
||||
fun addContacts(contacts: List<ConfigurationMessage.Contact>)
|
||||
|
||||
// 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<LinkPreview?>, groupPublicKey: String?, openGroupID: String?, attachments: List<Attachment>): 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<Long, OpenGroup>
|
||||
|
||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||
|
||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
||||
|
||||
}
|
||||
|
@ -1,75 +0,0 @@
|
||||
package org.session.libsession.messaging.file_server
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import okhttp3.Request
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.database.LokiAPIDatabaseProtocol
|
||||
import org.session.libsignal.utilities.*
|
||||
import java.net.URL
|
||||
|
||||
class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : DotNetAPI() {
|
||||
|
||||
companion object {
|
||||
internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
||||
internal val maxRetryCount = 4
|
||||
|
||||
public val maxFileSize = 10_000_000 // 10 MB
|
||||
/**
|
||||
* The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
* is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
* request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
* be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
* uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
*/
|
||||
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
|
||||
public val fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
lateinit var shared: FileServerAPI
|
||||
|
||||
/**
|
||||
* Must be called before `LokiAPI` is used.
|
||||
*/
|
||||
fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) {
|
||||
if (Companion::shared.isInitialized) { return }
|
||||
val server = "https://file.getsession.org"
|
||||
shared = FileServerAPI(server, userPublicKey, userPrivateKey, database)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Open Group Server Public Key
|
||||
fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise<String, Exception> {
|
||||
val publicKey = database.getOpenGroupPublicKey(openGroupServer)
|
||||
if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) {
|
||||
return Promise.of(publicKey)
|
||||
} else {
|
||||
val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}"
|
||||
val request = Request.Builder().url(url)
|
||||
request.addHeader("Content-Type", "application/json")
|
||||
request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token
|
||||
return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json ->
|
||||
try {
|
||||
val bodyAsString = json["data"] as String
|
||||
val body = JsonUtil.fromJson(bodyAsString)
|
||||
val base64EncodedPublicKey = body.get("data").asText()
|
||||
val prefixedPublicKey = Base64.decode(base64EncodedPublicKey)
|
||||
val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
||||
val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
||||
database.setOpenGroupPublicKey(openGroupServer, result)
|
||||
result
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse open group public key from: $json.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,10 +15,18 @@ import org.session.libsignal.utilities.Log
|
||||
|
||||
object FileServerAPIV2 {
|
||||
|
||||
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<Map<*, *>, 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<Map<*, *>, 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<ByteArray, Exception> {
|
||||
fun download(file: Long): Promise<ByteArray, Exception> {
|
||||
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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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<Long, Exception>): Pair<ByteArray, DotNetAPI.UploadResult> {
|
||||
private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise<Long, Exception>): Pair<ByteArray, UploadResult> {
|
||||
// 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)
|
||||
|
@ -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<Mention> = 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)
|
||||
|
@ -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.")
|
||||
|
@ -114,10 +114,9 @@ class ConfigurationMessage(var closedGroups: List<ClosedGroup>, 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)
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
|
||||
data class OpenGroup(
|
||||
val channel: Long,
|
||||
private val serverURL: String,
|
||||
val displayName: String,
|
||||
val isDeletable: Boolean
|
||||
) {
|
||||
val server get() = serverURL.toLowerCase()
|
||||
val id get() = getId(channel, server)
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic fun getId(channel: Long, server: String): String {
|
||||
return "$server.$channel"
|
||||
}
|
||||
|
||||
@JvmStatic fun fromJSON(jsonAsString: String): OpenGroup? {
|
||||
try {
|
||||
val json = JsonUtil.fromJson(jsonAsString)
|
||||
val channel = json.get("channel").asLong()
|
||||
val server = json.get("server").asText().toLowerCase()
|
||||
val displayName = json.get("displayName").asText()
|
||||
val isDeletable = json.get("isDeletable").asBoolean()
|
||||
return OpenGroup(channel, server, displayName, isDeletable)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toJSON(): Map<String, Any> {
|
||||
return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable )
|
||||
}
|
||||
}
|
@ -1,394 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||
import org.session.libsession.utilities.DownloadUtilities
|
||||
import org.session.libsignal.utilities.retryIfNeeded
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object OpenGroupAPI: DotNetAPI() {
|
||||
|
||||
private val moderators: HashMap<String, HashMap<Long, Set<String>>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
||||
|
||||
// region Settings
|
||||
private val fallbackBatchCount = 64
|
||||
private val maxRetryCount = 8
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private val channelInfoType = "net.patter-app.settings"
|
||||
private val attachmentType = "net.app.core.oembed"
|
||||
@JvmStatic
|
||||
val openGroupMessageType = "network.loki.messenger.publicChat"
|
||||
@JvmStatic
|
||||
val profilePictureType = "network.loki.messenger.avatar"
|
||||
|
||||
fun getDefaultChats(): List<OpenGroup> {
|
||||
return listOf() // Don't auto-join any open groups right now
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
|
||||
if (moderators[server] != null && moderators[server]!![channel] != null) {
|
||||
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
|
||||
}
|
||||
return false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
fun getMessages(channel: Long, server: String): Promise<List<OpenGroupMessage>, Exception> {
|
||||
Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.")
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>( "include_annotations" to 1 )
|
||||
val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||
if (lastMessageServerID != null) {
|
||||
parameters["since_id"] = lastMessageServerID
|
||||
} else {
|
||||
parameters["count"] = fallbackBatchCount
|
||||
parameters["include_deleted"] = 0
|
||||
}
|
||||
return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as List<Map<*, *>>
|
||||
val messages = data.mapNotNull { message ->
|
||||
try {
|
||||
val isDeleted = message["is_deleted"] as? Boolean ?: false
|
||||
if (isDeleted) { return@mapNotNull null }
|
||||
// Ignore messages without annotations
|
||||
if (message["annotations"] == null) { return@mapNotNull null }
|
||||
val annotation = (message["annotations"] as List<Map<*, *>>).find {
|
||||
((it["type"] as? String ?: "") == openGroupMessageType) && it["value"] != null
|
||||
} ?: return@mapNotNull null
|
||||
val value = annotation["value"] as Map<*, *>
|
||||
val serverID = message["id"] as? Long ?: (message["id"] as? Int)?.toLong() ?: (message["id"] as String).toLong()
|
||||
val user = message["user"] as Map<*, *>
|
||||
val publicKey = user["username"] as String
|
||||
val displayName = user["name"] as? String ?: "Anonymous"
|
||||
var profilePicture: OpenGroupMessage.ProfilePicture? = null
|
||||
if (user["annotations"] != null) {
|
||||
val profilePictureAnnotation = (user["annotations"] as List<Map< *, *>>).find {
|
||||
((it["type"] as? String ?: "") == profilePictureType) && it["value"] != null
|
||||
}
|
||||
val profilePictureAnnotationValue = profilePictureAnnotation?.get("value") as? Map<*, *>
|
||||
if (profilePictureAnnotationValue != null && profilePictureAnnotationValue["profileKey"] != null && profilePictureAnnotationValue["url"] != null) {
|
||||
try {
|
||||
val profileKey = Base64.decode(profilePictureAnnotationValue["profileKey"] as String)
|
||||
val url = profilePictureAnnotationValue["url"] as String
|
||||
profilePicture = OpenGroupMessage.ProfilePicture(profileKey, url)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
@Suppress("NAME_SHADOWING") val body = message["text"] as String
|
||||
val timestamp = value["timestamp"] as? Long ?: (value["timestamp"] as? Int)?.toLong() ?: (value["timestamp"] as String).toLong()
|
||||
var quote: OpenGroupMessage.Quote? = null
|
||||
if (value["quote"] != null) {
|
||||
val replyTo = message["reply_to"] as? Long ?: (message["reply_to"] as? Int)?.toLong() ?: (message["reply_to"] as String).toLong()
|
||||
val quoteAnnotation = value["quote"] as? Map<*, *>
|
||||
val quoteTimestamp = quoteAnnotation?.get("id") as? Long ?: (quoteAnnotation?.get("id") as? Int)?.toLong() ?: (quoteAnnotation?.get("id") as? String)?.toLong() ?: 0L
|
||||
val author = quoteAnnotation?.get("author") as? String
|
||||
val text = quoteAnnotation?.get("text") as? String
|
||||
quote = if (quoteTimestamp > 0L && author != null && text != null) OpenGroupMessage.Quote(quoteTimestamp, author, text, replyTo) else null
|
||||
}
|
||||
val attachmentsAsJSON = (message["annotations"] as List<Map<*, *>>).filter {
|
||||
((it["type"] as? String ?: "") == attachmentType) && it["value"] != null
|
||||
}
|
||||
val attachments = attachmentsAsJSON.mapNotNull { it["value"] as? Map<*, *> }.mapNotNull { attachmentAsJSON ->
|
||||
try {
|
||||
val kindAsString = attachmentAsJSON["lokiType"] as String
|
||||
val kind = OpenGroupMessage.Attachment.Kind.values().first { it.rawValue == kindAsString }
|
||||
val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong()
|
||||
val contentType = attachmentAsJSON["contentType"] as String
|
||||
val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt()
|
||||
val fileName = attachmentAsJSON["fileName"] as? String
|
||||
val flags = 0
|
||||
val url = attachmentAsJSON["url"] as String
|
||||
val caption = attachmentAsJSON["caption"] as? String
|
||||
val linkPreviewURL = attachmentAsJSON["linkPreviewUrl"] as? String
|
||||
val linkPreviewTitle = attachmentAsJSON["linkPreviewTitle"] as? String
|
||||
if (kind == OpenGroupMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) {
|
||||
null
|
||||
} else {
|
||||
OpenGroupMessage.Attachment(kind, server, id, contentType, size, fileName, flags, 0, 0, caption, url, linkPreviewURL, linkPreviewTitle)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki","Couldn't parse attachment due to error: $e.")
|
||||
null
|
||||
}
|
||||
}
|
||||
// Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over
|
||||
@Suppress("NAME_SHADOWING") val lastMessageServerID = storage.getLastMessageServerID(channel, server)
|
||||
if (serverID > lastMessageServerID ?: 0) { storage.setLastMessageServerID(channel, server, serverID) }
|
||||
val hexEncodedSignature = value["sig"] as String
|
||||
val signatureVersion = value["sigver"] as? Long ?: (value["sigver"] as? Int)?.toLong() ?: (value["sigver"] as String).toLong()
|
||||
val signature = OpenGroupMessage.Signature(Hex.fromStringCondensed(hexEncodedSignature), signatureVersion)
|
||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val dateAsString = message["created_at"] as String
|
||||
val serverTimestamp = format.parse(dateAsString).time
|
||||
// Verify the message
|
||||
val groupMessage = OpenGroupMessage(serverID, publicKey, displayName, body, timestamp, openGroupMessageType, quote, attachments.toMutableList(), profilePicture, signature, serverTimestamp)
|
||||
if (groupMessage.hasValidSignature()) groupMessage else null
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)}. Exception: ${exception.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
}.sortedBy { it.serverTimestamp }
|
||||
messages
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse messages for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDeletedMessageServerIDs(channel: Long, server: String): Promise<List<Long>, Exception> {
|
||||
Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.")
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val parameters = mutableMapOf<String, Any>()
|
||||
val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||
if (lastDeletionServerID != null) {
|
||||
parameters["since_id"] = lastDeletionServerID
|
||||
} else {
|
||||
parameters["count"] = fallbackBatchCount
|
||||
}
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then { json ->
|
||||
try {
|
||||
val deletedMessageServerIDs = (json["data"] as List<Map<*, *>>).mapNotNull { deletion ->
|
||||
try {
|
||||
val serverID = deletion["id"] as? Long ?: (deletion["id"] as? Int)?.toLong() ?: (deletion["id"] as String).toLong()
|
||||
val messageServerID = deletion["message_id"] as? Long ?: (deletion["message_id"] as? Int)?.toLong() ?: (deletion["message_id"] as String).toLong()
|
||||
@Suppress("NAME_SHADOWING") val lastDeletionServerID = storage.getLastDeletionServerID(channel, server)
|
||||
if (serverID > (lastDeletionServerID ?: 0)) { storage.setLastDeletionServerID(channel, server, serverID) }
|
||||
messageServerID
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse deleted message for open group with ID: $channel on server: $server. Exception: ${exception.message}")
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
deletedMessageServerIDs
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse deleted messages for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise<OpenGroupMessage, Exception> {
|
||||
val deferred = deferred<OpenGroupMessage, Exception>()
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic
|
||||
ThreadUtils.queue {
|
||||
val signedMessage = message.sign(userKeyPair.second)
|
||||
if (signedMessage == null) {
|
||||
deferred.reject(Error.SigningFailed)
|
||||
} else {
|
||||
retryIfNeeded(maxRetryCount) {
|
||||
Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.")
|
||||
val parameters = signedMessage.toJSON()
|
||||
execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as Map<*, *>
|
||||
val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong()
|
||||
val text = data["text"] as String
|
||||
val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
|
||||
format.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val dateAsString = data["created_at"] as String
|
||||
val timestamp = format.parse(dateAsString).time
|
||||
OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}.success {
|
||||
deferred.resolve(it)
|
||||
}.fail {
|
||||
deferred.reject(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun deleteMessage(messageServerID: Long, channel: Long, server: String, isSentByUser: Boolean): Promise<Long, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
Log.d("Loki", "Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
||||
val endpoint = if (isSentByUser) "channels/$channel/messages/$messageServerID" else "loki/v1/moderation/message/$messageServerID"
|
||||
execute(HTTPVerb.DELETE, server, endpoint, isJSONRequired = false).then {
|
||||
Log.d("Loki", "Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server.")
|
||||
messageServerID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val isModerationRequest = !isSentByUser
|
||||
val parameters = mapOf( "ids" to messageServerIDs.joinToString(",") )
|
||||
Log.d("Loki", "Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).")
|
||||
val endpoint = if (isSentByUser) "loki/v1/messages" else "loki/v1/moderation/messages"
|
||||
execute(HTTPVerb.DELETE, server, endpoint, parameters = parameters, isJSONRequired = false).then { json ->
|
||||
Log.d("Loki", "Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server.")
|
||||
messageServerIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getModerators(channel: Long, server: String): Promise<Set<String>, Exception> {
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then { json ->
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List<String>
|
||||
val moderatorsAsSet = moderators.orEmpty().toSet()
|
||||
if (this.moderators[server] != null) {
|
||||
this.moderators[server]!![channel] = moderatorsAsSet
|
||||
} else {
|
||||
this.moderators[server] = hashMapOf( channel to moderatorsAsSet )
|
||||
}
|
||||
moderatorsAsSet
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse moderators for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getChannelInfo(channel: Long, server: String): Promise<OpenGroupInfo, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
val parameters = mapOf( "include_annotations" to 1 )
|
||||
execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then { json ->
|
||||
try {
|
||||
val data = json["data"] as Map<*, *>
|
||||
val annotations = data["annotations"] as List<Map<*, *>>
|
||||
val annotation = annotations.find { (it["type"] as? String ?: "") == channelInfoType } ?: throw Error.ParsingFailed
|
||||
val info = annotation["value"] as Map<*, *>
|
||||
val displayName = info["name"] as String
|
||||
val countInfo = data["counts"] as Map<*, *>
|
||||
val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt()
|
||||
val profilePictureURL = info["avatar"] as String
|
||||
val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount)
|
||||
MessagingModuleConfiguration.shared.storage.setUserCount(channel, server, memberCount)
|
||||
publicChatInfo
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.setUserCount(channel, server, info.memberCount)
|
||||
storage.updateTitle(groupID, info.displayName)
|
||||
// Download and update profile picture if needed
|
||||
val oldProfilePictureURL = storage.getOpenGroupProfilePictureURL(channel, server)
|
||||
if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) {
|
||||
val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return
|
||||
storage.updateProfilePicture(groupID, profilePictureAsByteArray)
|
||||
storage.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? {
|
||||
val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}"
|
||||
Log.d("Loki", "Downloading open group profile picture from \"$url\".")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
try {
|
||||
DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null)
|
||||
Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"")
|
||||
return outputStream.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to download open group profile picture from \"$url\" due to error: $e.")
|
||||
return null
|
||||
} finally {
|
||||
outputStream.close()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun join(channel: Long, server: String): Promise<Unit, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then {
|
||||
Log.d("Loki", "Joined channel with ID: $channel on server: $server.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun leave(channel: Long, server: String): Promise<Unit, Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then {
|
||||
Log.d("Loki", "Left channel with ID: $channel on server: $server.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
|
||||
return retryIfNeeded(maxRetryCount) {
|
||||
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {
|
||||
Log.d("Loki", "Banned user with ID: $publicKey from $server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDisplayNames(publicKeys: Set<String>, server: String): Promise<Map<String, String>, Exception> {
|
||||
return getUserProfiles(publicKeys, server, false).map { json ->
|
||||
val mapping = mutableMapOf<String, String>()
|
||||
for (user in json) {
|
||||
if (user["username"] != null) {
|
||||
val publicKey = user["username"] as String
|
||||
val displayName = user["name"] as? String ?: "Anonymous"
|
||||
mapping[publicKey] = displayName
|
||||
}
|
||||
}
|
||||
mapping
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setDisplayName(newDisplayName: String?, server: String): Promise<Unit, Exception> {
|
||||
Log.d("Loki", "Updating display name on server: $server.")
|
||||
val parameters = mapOf( "name" to (newDisplayName ?: "") )
|
||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise<Unit, Exception> {
|
||||
return setProfilePicture(server, Base64.encodeBytes(profileKey), url)
|
||||
}
|
||||
|
||||
fun setProfilePicture(server: String, profileKey: String, url: String?): Promise<Unit, Exception> {
|
||||
Log.d("Loki", "Updating profile picture on server: $server.")
|
||||
val value = when (url) {
|
||||
null -> null
|
||||
else -> mapOf( "profileKey" to profileKey, "url" to url )
|
||||
}
|
||||
// TODO: This may actually completely replace the annotations, have to double check it
|
||||
return setSelfAnnotation(server, profilePictureType, value).map { Unit }.fail {
|
||||
Log.d("Loki", "Failed to update profile picture due to error: $it.")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -31,8 +31,8 @@ object OpenGroupAPIV2 {
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(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<List<OpenGroupMessageV2>, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
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<Map<*, *>>): List<OpenGroupMessageV2> {
|
||||
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<String, Any>
|
||||
@ -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<List<MessageDeletion>, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val queryParameters = mutableMapOf<String, String>()
|
||||
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<List<MessageDeletion>>(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<List<MessageDeletion>>(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<Map<String, Any>> ?: return@mapNotNull null
|
||||
@ -405,8 +403,8 @@ object OpenGroupAPIV2 {
|
||||
|
||||
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, 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 {
|
||||
|
@ -1,247 +0,0 @@
|
||||
package org.session.libsession.messaging.open_groups
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||
import org.session.libsignal.utilities.removing05PrefixIfNeeded
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
|
||||
data class OpenGroupMessage(
|
||||
val serverID: Long?,
|
||||
val senderPublicKey: String,
|
||||
val displayName: String,
|
||||
val body: String,
|
||||
val timestamp: Long,
|
||||
val type: String,
|
||||
val quote: Quote?,
|
||||
val attachments: MutableList<Attachment>,
|
||||
val profilePicture: ProfilePicture?,
|
||||
val signature: Signature?,
|
||||
val serverTimestamp: Long,
|
||||
) {
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val userPublicKey = storage.getUserPublicKey() ?: return null
|
||||
val attachmentIDs = message.attachmentIDs
|
||||
// Validation
|
||||
if (!message.isValid()) { return null } // Should be valid at this point
|
||||
// Quote
|
||||
val quote: Quote? = {
|
||||
val quote = message.quote
|
||||
if (quote != null && quote.isValid()) {
|
||||
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
|
||||
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
|
||||
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}()
|
||||
// Message
|
||||
val displayname = storage.getUserDisplayName() ?: "Anonymous"
|
||||
val text = message.text
|
||||
val body = if (text.isNullOrEmpty()) message.sentTimestamp.toString() else text // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
val result = OpenGroupMessage(null, userPublicKey, displayname, body, message.sentTimestamp!!, OpenGroupAPI.openGroupMessageType, quote, mutableListOf(), null, null, 0)
|
||||
// Link preview
|
||||
val linkPreview = message.linkPreview
|
||||
linkPreview?.let {
|
||||
if (!linkPreview.isValid()) { return@let }
|
||||
val attachmentID = linkPreview.attachmentID ?: return@let
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
|
||||
val openGroupLinkPreview = Attachment(
|
||||
Attachment.Kind.LinkPreview,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.get(),
|
||||
attachment.fileName.orNull(),
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
linkPreview.url,
|
||||
linkPreview.title)
|
||||
result.attachments.add(openGroupLinkPreview)
|
||||
}
|
||||
// Attachments
|
||||
val attachments = message.attachmentIDs.mapNotNull {
|
||||
val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null
|
||||
return@mapNotNull Attachment(
|
||||
Attachment.Kind.Attachment,
|
||||
server,
|
||||
attachment.id,
|
||||
attachment.contentType!!,
|
||||
attachment.size.orNull(),
|
||||
attachment.fileName.orNull() ?: "",
|
||||
0,
|
||||
attachment.width,
|
||||
attachment.height,
|
||||
attachment.caption.orNull(),
|
||||
attachment.url,
|
||||
null,
|
||||
null)
|
||||
}
|
||||
result.attachments.addAll(attachments)
|
||||
// Return
|
||||
return result
|
||||
}
|
||||
|
||||
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||
private val signatureVersion: Long = 1
|
||||
private val attachmentType = "net.app.core.oembed"
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Types
|
||||
data class ProfilePicture(
|
||||
val profileKey: ByteArray,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
data class Quote(
|
||||
val quotedMessageTimestamp: Long,
|
||||
val quoteePublicKey: String,
|
||||
val quotedMessageBody: String,
|
||||
val quotedMessageServerID: Long? = null,
|
||||
)
|
||||
|
||||
data class Signature(
|
||||
val data: ByteArray,
|
||||
val version: Long,
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val kind: Kind,
|
||||
val server: String,
|
||||
val serverID: Long,
|
||||
val contentType: String,
|
||||
val size: Int,
|
||||
val fileName: String?,
|
||||
val flags: Int,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val caption: String?,
|
||||
val url: String,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
val linkPreviewURL: String?,
|
||||
/**
|
||||
Guaranteed to be non-`nil` if `kind` is `LinkPreview`.
|
||||
*/
|
||||
val linkPreviewTitle: String?,
|
||||
) {
|
||||
val dotNetAPIType = when {
|
||||
contentType.startsWith("image") -> "photo"
|
||||
contentType.startsWith("video") -> "video"
|
||||
contentType.startsWith("audio") -> "audio"
|
||||
else -> "other"
|
||||
}
|
||||
|
||||
enum class Kind(val rawValue: String) {
|
||||
Attachment("attachment"), LinkPreview("preview")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Initialization
|
||||
constructor(hexEncodedPublicKey: String, displayName: String, body: String, timestamp: Long, type: String, quote: Quote?, attachments: List<Attachment>)
|
||||
: this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments as MutableList<Attachment>, null, null, 0)
|
||||
// endregion
|
||||
|
||||
// region Crypto
|
||||
internal fun sign(privateKey: ByteArray): OpenGroupMessage? {
|
||||
val data = getValidationData(signatureVersion)
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Failed to sign public chat message.")
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val signatureData = curve.calculateSignature(privateKey, data)
|
||||
val signature = Signature(signatureData, signatureVersion)
|
||||
return copy(signature = signature)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to sign public chat message due to error: ${e.message}.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasValidSignature(): Boolean {
|
||||
if (signature == null) { return false }
|
||||
val data = getValidationData(signature.version) ?: return false
|
||||
val publicKey = Hex.fromStringCondensed(senderPublicKey.removing05PrefixIfNeeded())
|
||||
try {
|
||||
return curve.verifySignature(publicKey, data, signature.data)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to verify public chat message due to error: ${e.message}.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Parsing
|
||||
internal fun toJSON(): Map<String, Any> {
|
||||
val value = mutableMapOf<String, Any>("timestamp" to timestamp)
|
||||
if (quote != null) {
|
||||
value["quote"] = mapOf("id" to quote.quotedMessageTimestamp, "author" to quote.quoteePublicKey, "text" to quote.quotedMessageBody)
|
||||
}
|
||||
if (signature != null) {
|
||||
value["sig"] = Hex.toStringCondensed(signature.data)
|
||||
value["sigver"] = signature.version
|
||||
}
|
||||
val annotation = mapOf("type" to type, "value" to value)
|
||||
val annotations = mutableListOf(annotation)
|
||||
attachments.forEach { attachment ->
|
||||
val attachmentValue = mutableMapOf(
|
||||
// Fields required by the .NET API
|
||||
"version" to 1,
|
||||
"type" to attachment.dotNetAPIType,
|
||||
// Custom fields
|
||||
"lokiType" to attachment.kind.rawValue,
|
||||
"server" to attachment.server,
|
||||
"id" to attachment.serverID,
|
||||
"contentType" to attachment.contentType,
|
||||
"size" to attachment.size,
|
||||
"fileName" to attachment.fileName,
|
||||
"flags" to attachment.flags,
|
||||
"width" to attachment.width,
|
||||
"height" to attachment.height,
|
||||
"url" to attachment.url
|
||||
)
|
||||
if (attachment.caption != null) { attachmentValue["caption"] = attachment.caption }
|
||||
if (attachment.linkPreviewURL != null) { attachmentValue["linkPreviewUrl"] = attachment.linkPreviewURL }
|
||||
if (attachment.linkPreviewTitle != null) { attachmentValue["linkPreviewTitle"] = attachment.linkPreviewTitle }
|
||||
val attachmentAnnotation = mapOf("type" to attachmentType, "value" to attachmentValue)
|
||||
annotations.add(attachmentAnnotation)
|
||||
}
|
||||
val result = mutableMapOf("text" to body, "annotations" to annotations)
|
||||
if (quote?.quotedMessageServerID != null) {
|
||||
result["reply_to"] = quote.quotedMessageServerID
|
||||
}
|
||||
return result
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private fun getValidationData(signatureVersion: Long): ByteArray? {
|
||||
var string = "${body.trim()}$timestamp"
|
||||
if (quote != null) {
|
||||
string += "${quote.quotedMessageTimestamp}${quote.quoteePublicKey}${quote.quotedMessageBody.trim()}"
|
||||
if (quote.quotedMessageServerID != null) {
|
||||
string += "${quote.quotedMessageServerID}"
|
||||
}
|
||||
}
|
||||
string += attachments.sortedBy { it.serverID }.map { it.serverID }.joinToString("")
|
||||
string += "$signatureVersion"
|
||||
try {
|
||||
return string.toByteArray(Charsets.UTF_8)
|
||||
} catch (exception: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -54,11 +54,12 @@ object MessageSender {
|
||||
|
||||
// Convenience
|
||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||
if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) {
|
||||
if (destination is Destination.OpenGroupV2) {
|
||||
return sendToOpenGroupDestination(destination, message)
|
||||
}
|
||||
} else {
|
||||
return sendToSnodeDestination(destination, message)
|
||||
}
|
||||
}
|
||||
|
||||
// One-on-One Chats & Closed Groups
|
||||
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
|
||||
@ -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
|
||||
@ -162,9 +163,11 @@ object MessageSender {
|
||||
}
|
||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
||||
/*
|
||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||
shouldNotify = true
|
||||
}
|
||||
*/
|
||||
if (shouldNotify) {
|
||||
val notifyPNServerJob = NotifyPNServerJob(snodeMessage)
|
||||
JobQueue.shared.add(notifyPNServerJob)
|
||||
@ -203,27 +206,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 +257,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())
|
||||
}
|
||||
|
@ -192,8 +192,8 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
|
||||
// Save the new group members
|
||||
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
|
||||
// Update the zombie list
|
||||
val oldZombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
val oldZombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, oldZombies.minus(membersToRemove).map { Address.fromSerialized(it) })
|
||||
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
|
||||
val name = group.title
|
||||
// Send the update to the group
|
||||
|
@ -69,21 +69,21 @@ private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
|
||||
fun MessageReceiver.showTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStartedMessage(context, threadID, address, 1)
|
||||
}
|
||||
|
||||
fun MessageReceiver.hideTypingIndicatorIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveTypingStoppedMessage(context, threadID, address, 1, false)
|
||||
}
|
||||
|
||||
fun MessageReceiver.cancelTypingIndicatorsIfNeeded(senderPublicKey: String) {
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val address = Address.fromSerialized(senderPublicKey)
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadIdFor(address) ?: return
|
||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) ?: return
|
||||
SSKEnvironment.shared.typingIndicators.didReceiveIncomingMessage(context, threadID, address, 1)
|
||||
}
|
||||
|
||||
@ -123,11 +123,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name,
|
||||
closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!)
|
||||
}
|
||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL }
|
||||
for (openGroup in message.openGroups) {
|
||||
if (allOpenGroups.contains(openGroup) || allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup, 1)
|
||||
if (allV2OpenGroups.contains(openGroup)) continue
|
||||
storage.addOpenGroup(openGroup)
|
||||
}
|
||||
if (message.displayName.isNotEmpty()) {
|
||||
TextSecurePreferences.setProfileName(context, message.displayName)
|
||||
@ -138,7 +137,7 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
||||
val profileKey = Base64.encodeBytes(message.profileKey)
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, profileKey)
|
||||
storage.setProfileKeyForRecipient(userPublicKey, message.profileKey)
|
||||
storage.setUserProfilePictureUrl(message.profilePicture!!)
|
||||
storage.setUserProfilePictureURL(message.profilePicture!!)
|
||||
}
|
||||
storage.addContacts(message.contacts)
|
||||
}
|
||||
@ -158,12 +157,9 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
||||
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
|
||||
throw MessageReceiver.Error.NoThread
|
||||
}
|
||||
val openGroup = threadID.let {
|
||||
storage.getOpenGroup(it.toString())
|
||||
}
|
||||
// Update profile if needed
|
||||
val profile = message.profile
|
||||
if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups
|
||||
if (profile != null && userPublicKey != message.sender) {
|
||||
val profileManager = SSKEnvironment.shared.profileManager
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||
val displayName = profile.displayName!!
|
||||
@ -475,8 +471,8 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
|
||||
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) })
|
||||
val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED
|
||||
// Notify the user
|
||||
// We don't display zombie members in the notification as users have already been notified when those members left
|
||||
@ -528,8 +524,8 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
|
||||
} else {
|
||||
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
|
||||
// Update zombie members
|
||||
val zombies = storage.getZombieMember(groupID)
|
||||
storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
val zombies = storage.getZombieMembers(groupID)
|
||||
storage.setZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) })
|
||||
}
|
||||
// Notify the user
|
||||
if (userLeft) {
|
||||
|
@ -28,7 +28,7 @@ class ClosedGroupPollerV2 {
|
||||
|
||||
companion object {
|
||||
private val minPollInterval = 4 * 1000
|
||||
private val maxPollInterval = 2 * 60 * 1000
|
||||
private val maxPollInterval = 4 * 60 * 1000
|
||||
|
||||
@JvmStatic
|
||||
val shared = ClosedGroupPollerV2()
|
||||
@ -75,7 +75,7 @@ class ClosedGroupPollerV2 {
|
||||
// reasonable fake time interval to use instead.
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
|
||||
val threadID = storage.getThreadID(groupID)?.toLongOrNull() ?: return
|
||||
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
|
||||
|
@ -1,232 +0,0 @@
|
||||
package org.session.libsession.messaging.sending_receiving.pollers
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.jobs.JobQueue
|
||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsignal.protos.SignalServiceProtos.*
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.successBackground
|
||||
import java.util.*
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class OpenGroupPoller(private val openGroup: OpenGroup, private val executorService: ScheduledExecutorService? = null) {
|
||||
|
||||
private var hasStarted = false
|
||||
@Volatile private var isPollOngoing = false
|
||||
var isCaughtUp = false
|
||||
|
||||
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||
|
||||
// region Convenience
|
||||
private val userHexEncodedPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: ""
|
||||
private var displayNameUpdates = setOf<String>()
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||
private val pollForDeletedMessagesInterval: Long = 60 * 1000
|
||||
private val pollForModeratorsInterval: Long = 10 * 60 * 1000
|
||||
private val pollForDisplayNamesInterval: Long = 60 * 1000
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
fun startIfNeeded() {
|
||||
if (hasStarted || executorService == null) return
|
||||
cancellableFutures += listOf(
|
||||
executorService.scheduleAtFixedRate(::pollForNewMessages,0, pollForNewMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDeletedMessages,0, pollForDeletedMessagesInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForModerators,0, pollForModeratorsInterval, TimeUnit.MILLISECONDS),
|
||||
executorService.scheduleAtFixedRate(::pollForDisplayNames,0, pollForDisplayNamesInterval, TimeUnit.MILLISECONDS)
|
||||
)
|
||||
hasStarted = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
cancellableFutures.forEach { future ->
|
||||
future.cancel(false)
|
||||
}
|
||||
cancellableFutures.clear()
|
||||
hasStarted = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Polling
|
||||
fun pollForNewMessages(): Promise<Unit, Exception> {
|
||||
return pollForNewMessagesInternal(false)
|
||||
}
|
||||
|
||||
private fun pollForNewMessagesInternal(isBackgroundPoll: Boolean): Promise<Unit, Exception> {
|
||||
if (isPollOngoing) { return Promise.of(Unit) }
|
||||
isPollOngoing = true
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
||||
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
||||
// Process messages in the background
|
||||
messages.forEach { message ->
|
||||
try {
|
||||
val senderPublicKey = message.senderPublicKey
|
||||
fun generateDisplayName(rawDisplayName: String): String {
|
||||
return "$rawDisplayName (...${senderPublicKey.takeLast(8)})"
|
||||
}
|
||||
val senderDisplayName = MessagingModuleConfiguration.shared.storage.getOpenGroupDisplayName(senderPublicKey, openGroup.channel, openGroup.server) ?: generateDisplayName(message.displayName)
|
||||
val id = openGroup.id.toByteArray()
|
||||
// Main message
|
||||
val dataMessageProto = DataMessage.newBuilder()
|
||||
val body = if (message.body == message.timestamp.toString()) { "" } else { message.body }
|
||||
dataMessageProto.setBody(body)
|
||||
dataMessageProto.setTimestamp(message.timestamp)
|
||||
// Attachments
|
||||
val attachmentProtos = message.attachments.mapNotNull { attachment ->
|
||||
try {
|
||||
if (attachment.kind != OpenGroupMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
|
||||
val attachmentProto = AttachmentPointer.newBuilder()
|
||||
attachmentProto.setId(attachment.serverID)
|
||||
attachmentProto.setContentType(attachment.contentType)
|
||||
attachmentProto.setSize(attachment.size)
|
||||
attachmentProto.setFileName(attachment.fileName)
|
||||
attachmentProto.setFlags(attachment.flags)
|
||||
attachmentProto.setWidth(attachment.width)
|
||||
attachmentProto.setHeight(attachment.height)
|
||||
attachment.caption?.let { attachmentProto.setCaption(it) }
|
||||
attachmentProto.setUrl(attachment.url)
|
||||
attachmentProto.build()
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki","Failed to parse attachment as proto",e)
|
||||
null
|
||||
}
|
||||
}
|
||||
dataMessageProto.addAllAttachments(attachmentProtos)
|
||||
// Link preview
|
||||
val linkPreview = message.attachments.firstOrNull { it.kind == OpenGroupMessage.Attachment.Kind.LinkPreview }
|
||||
if (linkPreview != null) {
|
||||
val linkPreviewProto = DataMessage.Preview.newBuilder()
|
||||
linkPreviewProto.setUrl(linkPreview.linkPreviewURL!!)
|
||||
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!!)
|
||||
val attachmentProto = AttachmentPointer.newBuilder()
|
||||
attachmentProto.setId(linkPreview.serverID)
|
||||
attachmentProto.setContentType(linkPreview.contentType)
|
||||
attachmentProto.setSize(linkPreview.size)
|
||||
attachmentProto.setFileName(linkPreview.fileName)
|
||||
attachmentProto.setFlags(linkPreview.flags)
|
||||
attachmentProto.setWidth(linkPreview.width)
|
||||
attachmentProto.setHeight(linkPreview.height)
|
||||
linkPreview.caption?.let { attachmentProto.setCaption(it) }
|
||||
attachmentProto.setUrl(linkPreview.url)
|
||||
linkPreviewProto.setImage(attachmentProto.build())
|
||||
dataMessageProto.addPreview(linkPreviewProto.build())
|
||||
}
|
||||
// Quote
|
||||
val quote = message.quote
|
||||
if (quote != null) {
|
||||
val quoteProto = DataMessage.Quote.newBuilder()
|
||||
quoteProto.setId(quote.quotedMessageTimestamp)
|
||||
quoteProto.setAuthor(quote.quoteePublicKey)
|
||||
if (quote.quotedMessageBody != quote.quotedMessageTimestamp.toString()) { quoteProto.setText(quote.quotedMessageBody) }
|
||||
dataMessageProto.setQuote(quoteProto.build())
|
||||
}
|
||||
val messageServerID = message.serverID
|
||||
// Profile
|
||||
val profileProto = DataMessage.LokiProfile.newBuilder()
|
||||
profileProto.setDisplayName(senderDisplayName)
|
||||
val profilePicture = message.profilePicture
|
||||
if (profilePicture != null) {
|
||||
profileProto.setProfilePicture(profilePicture.url)
|
||||
dataMessageProto.setProfileKey(ByteString.copyFrom(profilePicture.profileKey))
|
||||
}
|
||||
dataMessageProto.setProfile(profileProto.build())
|
||||
/* TODO: the signal service proto needs to be synced with iOS
|
||||
// Open group info
|
||||
if (messageServerID != null) {
|
||||
val openGroupProto = PublicChatInfo.newBuilder()
|
||||
openGroupProto.setServerID(messageServerID)
|
||||
dataMessageProto.setPublicChatInfo(openGroupProto.build())
|
||||
}
|
||||
*/
|
||||
// Signal group context
|
||||
val groupProto = GroupContext.newBuilder()
|
||||
groupProto.setId(ByteString.copyFrom(id))
|
||||
groupProto.setType(GroupContext.Type.DELIVER)
|
||||
groupProto.setName(openGroup.displayName)
|
||||
dataMessageProto.setGroup(groupProto.build())
|
||||
// Content
|
||||
val content = Content.newBuilder()
|
||||
content.setDataMessage(dataMessageProto.build())
|
||||
// Envelope
|
||||
val builder = Envelope.newBuilder()
|
||||
builder.type = Envelope.Type.SESSION_MESSAGE
|
||||
builder.source = senderPublicKey
|
||||
builder.sourceDevice = 1
|
||||
builder.setContent(content.build().toByteString())
|
||||
builder.timestamp = message.timestamp
|
||||
builder.serverTimestamp = message.serverTimestamp
|
||||
val envelope = builder.build()
|
||||
val job = MessageReceiveJob(envelope.toByteArray(), messageServerID, openGroup.id)
|
||||
Log.d("Loki", "Scheduling Job $job")
|
||||
if (isBackgroundPoll) {
|
||||
job.executeAsync().always { deferred.resolve(Unit) }
|
||||
// The promise is just used to keep track of when we're done
|
||||
} else {
|
||||
JobQueue.shared.add(job)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Exception parsing message", e)
|
||||
}
|
||||
}
|
||||
displayNameUpdates = displayNameUpdates + messages.map { it.senderPublicKey }.toSet() - userHexEncodedPublicKey
|
||||
executorService?.schedule(::pollForDisplayNames, 0, TimeUnit.MILLISECONDS)
|
||||
isCaughtUp = true
|
||||
isPollOngoing = false
|
||||
deferred.resolve(Unit)
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||
isPollOngoing = false
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
private fun pollForDisplayNames() {
|
||||
if (displayNameUpdates.isEmpty()) { return }
|
||||
val hexEncodedPublicKeys = displayNameUpdates
|
||||
displayNameUpdates = setOf()
|
||||
OpenGroupAPI.getDisplayNames(hexEncodedPublicKeys, openGroup.server).successBackground { mapping ->
|
||||
for (pair in mapping.entries) {
|
||||
if (pair.key == userHexEncodedPublicKey) continue
|
||||
val senderDisplayName = "${pair.value} (...${pair.key.substring(pair.key.count() - 8)})"
|
||||
MessagingModuleConfiguration.shared.storage.setOpenGroupDisplayName(pair.key, openGroup.channel, openGroup.server, senderDisplayName)
|
||||
}
|
||||
}.fail {
|
||||
displayNameUpdates = displayNameUpdates.union(hexEncodedPublicKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForDeletedMessages() {
|
||||
val messagingModule = MessagingModuleConfiguration.shared
|
||||
val address = GroupUtil.getEncodedOpenGroupID(openGroup.id.toByteArray())
|
||||
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { messagingModule.messageDataProvider.getMessageID(it, threadId) }
|
||||
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun pollForModerators() {
|
||||
OpenGroupAPI.getModerators(openGroup.channel, openGroup.server)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -78,7 +78,7 @@ class OpenGroupPollerV2(private val server: String, private val executorService:
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val 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) {
|
||||
|
@ -1,269 +0,0 @@
|
||||
package org.session.libsession.messaging.utilities
|
||||
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import okhttp3.*
|
||||
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.snode.OnionRequestAPI
|
||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||
|
||||
import org.session.libsignal.crypto.DiffieHellman
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
||||
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException
|
||||
import org.session.libsignal.exceptions.PushNetworkException
|
||||
import org.session.libsignal.streams.StreamDetails
|
||||
import org.session.libsignal.utilities.ProfileAvatarData
|
||||
import org.session.libsignal.utilities.PushAttachmentData
|
||||
import org.session.libsignal.streams.DigestingRequestBody
|
||||
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.JsonUtil
|
||||
import org.session.libsignal.utilities.HTTP
|
||||
import org.session.libsignal.utilities.*
|
||||
import org.session.libsignal.utilities.Base64
|
||||
import org.session.libsignal.utilities.Log
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Base class that provides utilities for .NET based APIs.
|
||||
*/
|
||||
open class DotNetAPI {
|
||||
|
||||
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
|
||||
|
||||
// Error
|
||||
internal sealed class Error(val description: String) : Exception(description) {
|
||||
object Generic : Error("An error occurred.")
|
||||
object InvalidURL : Error("Invalid URL.")
|
||||
object ParsingFailed : Error("Invalid file server response.")
|
||||
object SigningFailed : Error("Couldn't sign message.")
|
||||
object EncryptionFailed : Error("Couldn't encrypt file.")
|
||||
object DecryptionFailed : Error("Couldn't decrypt file.")
|
||||
object MaxFileSizeExceeded : Error("Maximum file size exceeded.")
|
||||
object TokenExpired: Error("Token expired.") // Session Android
|
||||
|
||||
internal val isRetryable: Boolean = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val authTokenRequestCache = hashMapOf<String, Promise<String, Exception>>()
|
||||
}
|
||||
|
||||
public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
||||
|
||||
fun getAuthToken(server: String): Promise<String, Exception> {
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
val token = storage.getAuthToken(server)
|
||||
if (token != null) { return Promise.of(token) }
|
||||
// Avoid multiple token requests to the server by caching
|
||||
var promise = authTokenRequestCache[server]
|
||||
if (promise == null) {
|
||||
promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken ->
|
||||
storage.setAuthToken(server, newToken)
|
||||
newToken
|
||||
}.always {
|
||||
authTokenRequestCache.remove(server)
|
||||
}
|
||||
authTokenRequestCache[server] = promise
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
private fun requestNewAuthToken(server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Requesting auth token for server: $server.")
|
||||
val userKeyPair = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: throw Error.Generic
|
||||
val parameters: Map<String, Any> = mapOf( "pubKey" to userKeyPair.first )
|
||||
return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map { json ->
|
||||
try {
|
||||
val base64EncodedChallenge = json["cipherText64"] as String
|
||||
val challenge = Base64.decode(base64EncodedChallenge)
|
||||
val base64EncodedServerPublicKey = json["serverPubKey64"] as String
|
||||
var serverPublicKey = Base64.decode(base64EncodedServerPublicKey)
|
||||
// Discard the "05" prefix if needed
|
||||
if (serverPublicKey.count() == 33) {
|
||||
val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey)
|
||||
serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded())
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userKeyPair.second)
|
||||
val token = tokenAsData.toString(Charsets.UTF_8)
|
||||
token
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't parse auth token for server: $server.")
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitAuthToken(token: String, server: String): Promise<String, Exception> {
|
||||
Log.d("Loki", "Submitting auth token for server: $server.")
|
||||
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() ?: throw Error.Generic
|
||||
val parameters = mapOf( "pubKey" to userPublicKey, "token" to token )
|
||||
return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token }
|
||||
}
|
||||
|
||||
internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map<String, Any> = mapOf(), isJSONRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||
val sanitizedEndpoint = endpoint.removePrefix("/")
|
||||
var url = "$server/$sanitizedEndpoint"
|
||||
if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) {
|
||||
val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&")
|
||||
if (queryParameters.isNotEmpty()) { url += "?$queryParameters" }
|
||||
}
|
||||
var request = Request.Builder().url(url)
|
||||
if (isAuthRequired) {
|
||||
if (token == null) { throw IllegalStateException() }
|
||||
request = request.header("Authorization", "Bearer $token")
|
||||
}
|
||||
when (verb) {
|
||||
HTTPVerb.GET -> request = request.get()
|
||||
HTTPVerb.DELETE -> request = request.delete()
|
||||
else -> {
|
||||
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||
val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||
when (verb) {
|
||||
HTTPVerb.PUT -> request = request.put(body)
|
||||
HTTPVerb.POST -> request = request.post(body)
|
||||
HTTPVerb.PATCH -> request = request.patch(body)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey)
|
||||
else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server)
|
||||
return serverPublicKeyPromise.bind { serverPublicKey ->
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
||||
throw Error.TokenExpired
|
||||
}
|
||||
}
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (isAuthRequired) {
|
||||
getAuthToken(server).bind { execute(it) }
|
||||
} else {
|
||||
execute(null)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getUserProfiles(publicKeys: Set<String>, server: String, includeAnnotations: Boolean): Promise<List<Map<*, *>>, Exception> {
|
||||
val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } )
|
||||
return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json ->
|
||||
val data = json["data"] as? List<Map<*, *>>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
data!! // For some reason the compiler can't infer that this can't be null at this point
|
||||
}
|
||||
}
|
||||
|
||||
internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise<Map<*, *>, Exception> {
|
||||
val annotation = mutableMapOf<String, Any>( "type" to type )
|
||||
if (newValue != null) { annotation["value"] = newValue }
|
||||
val parameters = mapOf( "annotations" to listOf( annotation ) )
|
||||
return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters)
|
||||
}
|
||||
|
||||
// UPLOAD
|
||||
|
||||
// TODO: migrate to v2 file server
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult {
|
||||
// This function mimics what Signal does in PushServiceSocket
|
||||
val contentType = "application/octet-stream"
|
||||
val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener)
|
||||
Log.d("Loki", "File size: ${attachment.dataSize} bytes.")
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", contentType)
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.e("Loki", "Couldn't parse attachment from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.e("Loki", "Couldn't parse upload from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult {
|
||||
val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key))
|
||||
val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory,
|
||||
profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null)
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", "network.loki")
|
||||
.addFormDataPart("Content-Type", "application/octet-stream")
|
||||
.addFormDataPart("content", UUID.randomUUID().toString(), file)
|
||||
.build()
|
||||
val request = Request.Builder().url("$server/files").post(body)
|
||||
return retryIfNeeded(4) {
|
||||
upload(server, request) { json ->
|
||||
val data = json["data"] as? Map<*, *>
|
||||
if (data == null) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||
val url = data["url"] as? String
|
||||
if (id == null || url == null || url.isEmpty()) {
|
||||
Log.d("Loki", "Couldn't parse profile picture from: $json.")
|
||||
throw Error.ParsingFailed
|
||||
}
|
||||
setLastProfilePictureUpload()
|
||||
UploadResult(id, url, file.transmittedDigest)
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
|
||||
@Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class)
|
||||
private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise<UploadResult, Exception> {
|
||||
val promise: Promise<Map<*, *>, Exception>
|
||||
if (server == FileServerAPI.shared.server) {
|
||||
request.addHeader("Authorization", "Bearer loki")
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token
|
||||
promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey)
|
||||
} else {
|
||||
promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey ->
|
||||
getAuthToken(server).bind { token ->
|
||||
request.addHeader("Authorization", "Bearer $token")
|
||||
OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return promise.map { json ->
|
||||
parse(json)
|
||||
}.recover { exception ->
|
||||
if (exception is HTTP.HTTPRequestFailedException) {
|
||||
val statusCode = exception.statusCode
|
||||
if (statusCode == 401 || statusCode == 403) {
|
||||
MessagingModuleConfiguration.shared.storage.setAuthToken(server, null)
|
||||
}
|
||||
throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.")
|
||||
}
|
||||
throw PushNetworkException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Boolean.toInt(): Int { return if (this) 1 else 0 }
|
@ -6,7 +6,7 @@ import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.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(
|
||||
|
@ -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)
|
||||
fun downloadFile(outputStream: OutputStream, urlAsString: String) {
|
||||
val url = HttpUrl.parse(urlAsString)!!
|
||||
val fileID = url.pathSegments().last()
|
||||
try {
|
||||
FileServerAPIV2.download(fileId.toLong(), useOldServer).get().let {
|
||||
FileServerAPIV2.download(fileID.toLong()).get().let {
|
||||
outputStream.write(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couln't download attachment due to error",e)
|
||||
Log.e("Loki", "Couldn't download attachment.", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -33,7 +33,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)
|
||||
}
|
||||
|
||||
interface MessageExpirationManagerProtocol {
|
||||
|
@ -0,0 +1,3 @@
|
||||
package org.session.libsession.utilities
|
||||
|
||||
data class UploadResult(val id: Long, val url: String, val digest: ByteArray?)
|
@ -3,6 +3,5 @@ package org.session.libsignal.database
|
||||
interface LokiUserDatabaseProtocol {
|
||||
|
||||
fun getDisplayName(publicKey: String): String?
|
||||
fun getServerDisplayName(serverID: String, publicKey: String): String?
|
||||
fun getProfilePictureURL(publicKey: String): String?
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user