mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
commit
facd3616fb
@ -46,11 +46,12 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||||
implementation 'androidx.activity:activity-ktx:1.1.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
|
implementation 'androidx.activity:activity-ktx:1.2.2'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.3.2'
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
||||||
|
|
||||||
|
@ -137,9 +137,20 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
return openGroupMessagingDatabase.getMessageID(serverID)
|
return openGroupMessagingDatabase.getMessageID(serverID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessage(messageID: Long) {
|
override fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>? {
|
||||||
val messagingDatabase = DatabaseFactory.getSmsDatabase(context)
|
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
messagingDatabase.deleteMessage(messageID)
|
return messageDB.getMessageID(serverId, threadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
|
if (isSms) {
|
||||||
|
val db = DatabaseFactory.getSmsDatabase(context)
|
||||||
|
db.deleteMessage(messageID)
|
||||||
|
} else {
|
||||||
|
val db = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
db.delete(messageID)
|
||||||
|
}
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).deleteMessage(messageID, isSms)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
||||||
|
@ -83,21 +83,42 @@ import com.annimon.stream.Stream;
|
|||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager;
|
import org.session.libsession.messaging.mentions.MentionsManager;
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.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;
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
||||||
|
import org.session.libsession.messaging.threads.Address;
|
||||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||||
|
import org.session.libsession.messaging.threads.GroupRecord;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.RecipientFormattingException;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
||||||
|
import org.session.libsession.utilities.ExpirationUtil;
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.session.libsession.utilities.MediaTypes;
|
import org.session.libsession.utilities.MediaTypes;
|
||||||
|
import org.session.libsession.utilities.ServiceUtil;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
|
||||||
|
import org.session.libsession.utilities.views.Stub;
|
||||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
import org.session.libsignal.libsignal.InvalidMessageException;
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
import org.session.libsignal.service.loki.Mention;
|
import org.session.libsignal.service.loki.Mention;
|
||||||
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
import org.session.libsignal.service.loki.utilities.HexEncodingKt;
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
|
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
|
||||||
|
import org.session.libsignal.utilities.concurrent.ListenableFuture;
|
||||||
|
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||||
import org.thoughtcrime.securesms.MediaOverviewActivity;
|
import org.thoughtcrime.securesms.MediaOverviewActivity;
|
||||||
@ -121,7 +142,6 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
|||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
import org.thoughtcrime.securesms.database.DraftDatabase.Draft;
|
||||||
@ -135,7 +155,6 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
|||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
|
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
||||||
@ -158,8 +177,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.mms.QuoteId;
|
import org.thoughtcrime.securesms.mms.QuoteId;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
@ -168,31 +185,12 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
|
|||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientFormattingException;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
|
||||||
import org.session.libsession.utilities.ServiceUtil;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
|
||||||
import org.session.libsession.messaging.threads.GroupRecord;
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil;
|
|
||||||
import org.session.libsession.utilities.views.Stub;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.concurrent.AssertedSuccessListener;
|
|
||||||
import org.session.libsignal.utilities.concurrent.ListenableFuture;
|
|
||||||
import org.session.libsignal.utilities.concurrent.SettableFuture;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
@ -377,9 +375,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
||||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
PublicChatInfoUpdateWorker.scheduleInstant(this, openGroupV2.getServer(), openGroupV2.getRoom());
|
||||||
}
|
}
|
||||||
|
|
||||||
View rootView = findViewById(R.id.rootView);
|
View rootView = findViewById(R.id.rootView);
|
||||||
@ -1400,11 +1401,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null &&
|
if (publicChat != null &&
|
||||||
publicChat.getChannel() == event.getChannel() &&
|
publicChat.getChannel() == event.getChannel() &&
|
||||||
publicChat.getServer().equals(event.getUrl())) {
|
publicChat.getServer().equals(event.getUrl())) {
|
||||||
this.updateSubtitleTextView();
|
this.updateSubtitleTextView();
|
||||||
}
|
}
|
||||||
|
if (openGroup != null &&
|
||||||
|
openGroup.getRoom().equals(event.getRoom()) &&
|
||||||
|
openGroup.getServer().equals(event.getUrl())) {
|
||||||
|
this.updateSubtitleTextView();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//////// Helper Methods
|
//////// Helper Methods
|
||||||
@ -1721,7 +1728,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
boolean initiating = threadId == -1;
|
boolean initiating = threadId == -1;
|
||||||
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
|
||||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||||
recipient.isGroupRecipient() ||
|
// recipient.isGroupRecipient() ||
|
||||||
inputPanel.getQuote().isPresent() ||
|
inputPanel.getQuote().isPresent() ||
|
||||||
linkPreviewViewModel.hasLinkPreview() ||
|
linkPreviewViewModel.hasLinkPreview() ||
|
||||||
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
||||||
@ -2338,10 +2345,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
|
||||||
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadId);
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
|
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
|
||||||
if (userCount == null) { userCount = 0; }
|
if (userCount == null) { userCount = 0; }
|
||||||
subtitleTextView.setText(userCount + " members");
|
subtitleTextView.setText(userCount + " members");
|
||||||
|
} else if (openGroup != null) {
|
||||||
|
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(openGroup.getRoom(),openGroup.getServer());
|
||||||
|
if (userCount == null) { userCount = 0; }
|
||||||
|
subtitleTextView.setText(userCount + " members");
|
||||||
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
|
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
|
||||||
subtitleTextView.setText(recipient.getAddress().toString());
|
subtitleTextView.setText(recipient.getAddress().toString());
|
||||||
} else {
|
} else {
|
||||||
|
@ -57,49 +57,51 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration;
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
||||||
import org.session.libsession.messaging.messages.visible.Quote;
|
import org.session.libsession.messaging.messages.visible.Quote;
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
import org.session.libsession.messaging.messages.visible.VisibleMessage;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
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;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||||
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
|
import org.session.libsession.messaging.threads.Address;
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient;
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences;
|
||||||
|
import org.session.libsession.utilities.Util;
|
||||||
|
import org.session.libsession.utilities.ViewUtil;
|
||||||
|
import org.session.libsession.utilities.concurrent.SimpleTask;
|
||||||
|
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
||||||
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
import org.thoughtcrime.securesms.ShareActivity;
|
import org.thoughtcrime.securesms.ShareActivity;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
|
||||||
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
import org.session.libsession.utilities.ViewUtil;
|
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@ -397,7 +399,8 @@ public class ConversationFragment extends Fragment
|
|||||||
|
|
||||||
if (isGroupChat) {
|
if (isGroupChat) {
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
boolean isPublicChat = (publicChat != null);
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
boolean isPublicChat = (publicChat != null || openGroupChat != null);
|
||||||
int selectedMessageCount = messageRecords.size();
|
int selectedMessageCount = messageRecords.size();
|
||||||
boolean areAllSentByUser = true;
|
boolean areAllSentByUser = true;
|
||||||
Set<String> uniqueUserSet = new HashSet<>();
|
Set<String> uniqueUserSet = new HashSet<>();
|
||||||
@ -407,8 +410,12 @@ public class ConversationFragment extends Fragment
|
|||||||
}
|
}
|
||||||
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
|
||||||
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
|
||||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext());
|
||||||
boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
|
boolean userCanModerate =
|
||||||
|
(isPublicChat &&
|
||||||
|
((publicChat != null && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()))
|
||||||
|
|| (openGroupChat != null && OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())))
|
||||||
|
);
|
||||||
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
|
||||||
// allow banning if moderating a public chat and only one user's messages are selected
|
// allow banning if moderating a public chat and only one user's messages are selected
|
||||||
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
|
||||||
@ -509,6 +516,7 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
|
OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -519,14 +527,14 @@ public class ConversationFragment extends Fragment
|
|||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(MessageRecord... messageRecords) {
|
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||||
if (publicChat != null) {
|
if (publicChat != null || openGroupChat != null) {
|
||||||
ArrayList<Long> serverIDs = new ArrayList<>();
|
ArrayList<Long> serverIDs = new ArrayList<>();
|
||||||
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
ArrayList<Long> ignoredMessages = new ArrayList<>();
|
||||||
ArrayList<Long> failedMessages = new ArrayList<>();
|
ArrayList<Long> failedMessages = new ArrayList<>();
|
||||||
boolean isSentByUser = true;
|
boolean isSentByUser = true;
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
isSentByUser = isSentByUser && messageRecord.isOutgoing();
|
||||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
if (serverID != null) {
|
if (serverID != null) {
|
||||||
serverIDs.add(serverID);
|
serverIDs.add(serverID);
|
||||||
} else {
|
} else {
|
||||||
@ -538,7 +546,7 @@ public class ConversationFragment extends Fragment
|
|||||||
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
|
||||||
.success(l -> {
|
.success(l -> {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
if (l.contains(serverID)) {
|
if (l.contains(serverID)) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
|
||||||
@ -555,7 +563,25 @@ public class ConversationFragment extends Fragment
|
|||||||
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
|
Log.w("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
} else if (openGroupChat != null) {
|
||||||
|
for (Long serverId : serverIDs) {
|
||||||
|
OpenGroupAPIV2
|
||||||
|
.deleteMessage(serverId, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
|
.success(l -> {
|
||||||
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
|
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id, !messageRecord.isMms());
|
||||||
|
if (serverID != null && serverID.equals(serverId)) {
|
||||||
|
MessagingModuleConfiguration.shared.getMessageDataProvider().deleteMessage(messageRecord.id, !messageRecord.isMms());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).fail(e->{
|
||||||
|
Log.e("Loki", "Couldn't delete message due to error",e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
if (messageRecord.isMms()) {
|
if (messageRecord.isMms()) {
|
||||||
@ -591,7 +617,8 @@ public class ConversationFragment extends Fragment
|
|||||||
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
builder.setTitle(R.string.ConversationFragment_ban_selected_user);
|
||||||
builder.setCancelable(true);
|
builder.setCancelable(true);
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
final OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
|
||||||
|
final OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
builder.setPositiveButton(R.string.ban, (dialog, which) -> {
|
||||||
ConversationAdapter chatAdapter = getListAdapter();
|
ConversationAdapter chatAdapter = getListAdapter();
|
||||||
@ -610,9 +637,19 @@ public class ConversationFragment extends Fragment
|
|||||||
Log.d("Loki", "User banned");
|
Log.d("Loki", "User banned");
|
||||||
return Unit.INSTANCE;
|
return Unit.INSTANCE;
|
||||||
}).fail(e -> {
|
}).fail(e -> {
|
||||||
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
|
Log.e("Loki", "Couldn't ban user due to error",e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
} else if (openGroupChat != null) {
|
||||||
|
OpenGroupAPIV2
|
||||||
|
.ban(userPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())
|
||||||
|
.success(l -> {
|
||||||
|
Log.d("Loki", "User banned");
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}).fail(e -> {
|
||||||
|
Log.e("Loki", "Failed to ban user",e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
Log.d("Loki", "Tried to ban user from a non-public chat");
|
Log.d("Loki", "Tried to ban user from a non-public chat");
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,8 @@ import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
|
|||||||
import org.session.libsession.messaging.jobs.JobQueue;
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup;
|
import org.session.libsession.messaging.open_groups.OpenGroup;
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI;
|
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;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
|
||||||
@ -88,6 +90,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.Quote;
|
import org.thoughtcrime.securesms.database.model.Quote;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||||
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
|
||||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
@ -724,9 +727,9 @@ public class ConversationItem extends LinearLayout
|
|||||||
String publicKey = recipient.getAddress().toString();
|
String publicKey = recipient.getAddress().toString();
|
||||||
profilePictureView.setPublicKey(publicKey);
|
profilePictureView.setPublicKey(publicKey);
|
||||||
String displayName = recipient.getName();
|
String displayName = recipient.getName();
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
OpenGroup openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
||||||
if (displayName == null && publicChat != null) {
|
if (displayName == null && openGroup != null) {
|
||||||
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.getId(), publicKey);
|
displayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(openGroup.getId(), publicKey);
|
||||||
}
|
}
|
||||||
profilePictureView.setDisplayName(displayName);
|
profilePictureView.setDisplayName(displayName);
|
||||||
profilePictureView.setAdditionalPublicKey(null);
|
profilePictureView.setAdditionalPublicKey(null);
|
||||||
@ -867,7 +870,12 @@ public class ConversationItem extends LinearLayout
|
|||||||
try {
|
try {
|
||||||
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
|
String serverId = GroupUtil.getDecodedGroupID(conversationRecipient.getAddress().serialize());
|
||||||
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
|
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize());
|
||||||
if (senderDisplayName != null) { displayName = senderDisplayName; }
|
if (senderDisplayName != null) {
|
||||||
|
displayName = senderDisplayName;
|
||||||
|
} else {
|
||||||
|
// opengroupv2 format
|
||||||
|
displayName = OpenGroupUtilities.getDisplayName(recipient);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@ -912,9 +920,13 @@ public class ConversationItem extends LinearLayout
|
|||||||
int visibility = View.GONE;
|
int visibility = View.GONE;
|
||||||
|
|
||||||
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
OpenGroup publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
|
||||||
|
OpenGroupV2 openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(messageRecord.getThreadId());
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
|
boolean isModerator = OpenGroupAPI.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
|
||||||
visibility = isModerator ? View.VISIBLE : View.GONE;
|
visibility = isModerator ? View.VISIBLE : View.GONE;
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
boolean isModerator = OpenGroupAPIV2.isUserModerator(current.getRecipient().getAddress().toString(), openGroupV2.getRoom(), openGroupV2.getServer());
|
||||||
|
visibility = isModerator ? View.VISIBLE : View.GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
moderatorIconImageView.setVisibility(visibility);
|
moderatorIconImageView.setVisibility(visibility);
|
||||||
|
@ -13,6 +13,7 @@ import org.session.libsession.messaging.messages.signal.IncomingTextMessage
|
|||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.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.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
@ -226,6 +227,21 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(server, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAuthToken(room: String, server: String): String? {
|
||||||
|
val id = "$server.$room"
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getAuthToken(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAuthToken(room: String, server: String, newValue: String) {
|
||||||
|
val id = "$server.$room"
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAuthToken(room: String, server: String) {
|
||||||
|
val id = "$server.$room"
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setAuthToken(id, null)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getOpenGroup(threadID: String): OpenGroup? {
|
override fun getOpenGroup(threadID: String): OpenGroup? {
|
||||||
if (threadID.toInt() < 0) { return null }
|
if (threadID.toInt() < 0) { return null }
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
@ -235,6 +251,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getV2OpenGroup(threadId: String): OpenGroupV2? {
|
||||||
|
if (threadId.toInt() < 0) { return null }
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf(threadId)) { cursor ->
|
||||||
|
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
|
||||||
|
OpenGroupV2.fromJson(publicChatAsJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getThreadID(openGroupID: String): String {
|
override fun getThreadID(openGroupID: String): String {
|
||||||
val address = Address.fromSerialized(openGroupID)
|
val address = Address.fromSerialized(openGroupID)
|
||||||
val recipient = Recipient.from(context, address, false)
|
val recipient = Recipient.from(context, address, false)
|
||||||
@ -254,11 +279,33 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(groupID, publicKey, displayName)
|
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? {
|
override fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? {
|
||||||
val groupID = "$server.$channel"
|
val groupID = "$server.$channel"
|
||||||
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String? {
|
||||||
|
val groupID = "$server.$room"
|
||||||
|
return DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(groupID, publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLastMessageServerId(room: String, server: String): Long? {
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLastMessageServerId(room: String, server: String, newValue: Long) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setLastMessageServerID(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeLastMessageServerId(room: String, server: String) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastMessageServerID(group, server)
|
||||||
}
|
}
|
||||||
@ -271,6 +318,22 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(group, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastDeletionServerId(room: String, server: String): Long? {
|
||||||
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLastDeletionServerId(room: String, server: String, newValue: Long) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setLastDeletionServerID(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeLastDeletionServerId(room: String, server: String) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(room, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setUserCount(room: String, server: String, newValue: Int) {
|
||||||
|
DatabaseFactory.getLokiAPIDatabase(context).setUserCount(room, server, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
|
return DatabaseFactory.getLokiAPIDatabase(context).getLastDeletionServerID(group, server)
|
||||||
}
|
}
|
||||||
@ -325,8 +388,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return database.getMessageFor(timestamp, address)?.getId()
|
return database.getMessageFor(timestamp, address)?.getId()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long) {
|
override fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) {
|
||||||
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
|
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID, isSms)
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setOriginalThreadID(messageID, serverID, threadID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
|
||||||
@ -475,6 +539,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
|
return DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups()
|
||||||
|
}
|
||||||
|
|
||||||
override fun addOpenGroup(server: String, channel: Long) {
|
override fun addOpenGroup(server: String, channel: Long) {
|
||||||
OpenGroupUtilities.addGroup(context, server, channel)
|
OpenGroupUtilities.addGroup(context, server, channel)
|
||||||
}
|
}
|
||||||
@ -513,6 +581,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
return if (threadID < 0) null else threadID
|
return if (threadID < 0) null else threadID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getThreadIdForMms(mmsId: Long): Long {
|
||||||
|
val mmsDb = DatabaseFactory.getMmsDatabase(context)
|
||||||
|
val cursor = mmsDb.getMessage(mmsId)
|
||||||
|
val reader = mmsDb.readerFor(cursor)
|
||||||
|
val threadId = reader.next.threadId
|
||||||
|
cursor.close()
|
||||||
|
return threadId
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||||
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
|
return DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestSentTimestamp(publicKey)
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||||
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
|
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
|
||||||
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
|
||||||
|
|
||||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||||
@ -275,6 +277,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
if (oldVersion < lokiV23) {
|
if (oldVersion < lokiV23) {
|
||||||
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
|
db.execSQL("ALTER TABLE groups ADD COLUMN zombie_members TEXT");
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageIDTableForType());
|
||||||
|
db.execSQL(LokiMessageDatabase.getUpdateMessageMappingTable());
|
||||||
}
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
|
@ -353,6 +353,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||||
|
val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||||
if (publicChat != null) {
|
if (publicChat != null) {
|
||||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
@ -364,6 +365,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
ApplicationContext.getInstance(context).publicChatManager
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
.removeChat(publicChat.server, publicChat.channel)
|
.removeChat(publicChat.server, publicChat.channel)
|
||||||
|
} else if (openGroupV2 != null) {
|
||||||
|
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||||
|
apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server)
|
||||||
|
apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server)
|
||||||
|
|
||||||
|
ApplicationContext.getInstance(context).publicChatManager
|
||||||
|
.removeChat(openGroupV2.server, openGroupV2.room)
|
||||||
} else {
|
} else {
|
||||||
threadDB.deleteConversation(threadID)
|
threadDB.deleteConversation(threadID)
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,49 @@ package org.thoughtcrime.securesms.loki.activities
|
|||||||
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.AnimatorListenerAdapter
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Patterns
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.activity.viewModels
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.messaging.threads.DistributionTypes
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||||
|
import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel
|
||||||
|
import org.thoughtcrime.securesms.loki.viewmodel.State
|
||||||
|
|
||||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||||
|
|
||||||
|
private val viewModel by viewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||||
|
|
||||||
// region Lifecycle
|
// region Lifecycle
|
||||||
@ -65,16 +83,43 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun joinPublicChatIfPossible(url: String) {
|
fun joinPublicChatIfPossible(url: String) {
|
||||||
if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) {
|
// add http if just an IP style / host style URL is entered but leave it if scheme is included
|
||||||
return Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
val properString = if (!url.startsWith("http")) "http://$url" else url
|
||||||
}
|
val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
val room = httpUrl.pathSegments().firstOrNull()
|
||||||
|
val publicKey = httpUrl.queryParameter("public_key")
|
||||||
|
val isV2OpenGroup = !room.isNullOrEmpty()
|
||||||
showLoader()
|
showLoader()
|
||||||
val channel: Long = 1
|
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
|
val (threadID, groupID) = if (isV2OpenGroup) {
|
||||||
|
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 group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!)
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||||
|
threadID to groupID
|
||||||
|
} else {
|
||||||
|
val channel: Long = 1
|
||||||
|
val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel)
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity)
|
||||||
|
val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray())
|
||||||
|
threadID to groupID
|
||||||
|
}
|
||||||
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
// go to the new conversation and finish this one
|
||||||
|
openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@ -83,10 +128,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
|||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) { finish() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region Convenience
|
||||||
|
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||||
|
val intent = Intent(context, ConversationActivity::class.java)
|
||||||
|
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||||
|
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
|
||||||
|
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Adapter
|
// region Adapter
|
||||||
@ -109,7 +163,7 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(index: Int): CharSequence? {
|
override fun getPageTitle(index: Int): CharSequence {
|
||||||
return when (index) {
|
return when (index) {
|
||||||
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
|
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
|
||||||
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
||||||
@ -122,24 +176,63 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity
|
|||||||
// region Enter Chat URL Fragment
|
// region Enter Chat URL Fragment
|
||||||
class EnterChatURLFragment : Fragment() {
|
class EnterChatURLFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||||
|
defaultRoomsGridLayout.removeAllViews()
|
||||||
|
groups.forEach { defaultGroup ->
|
||||||
|
val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip
|
||||||
|
val drawable = defaultGroup.image?.let { bytes ->
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size)
|
||||||
|
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||||
|
isCircular = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chip.chipIcon = drawable
|
||||||
|
chip.text = defaultGroup.name
|
||||||
|
chip.setOnClickListener {
|
||||||
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.toJoinUrl())
|
||||||
|
}
|
||||||
|
defaultRoomsGridLayout.addView(chip)
|
||||||
|
}
|
||||||
|
if (groups.size and 1 != 0) {
|
||||||
|
// add a filler weight 1 view
|
||||||
|
layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||||
|
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||||
|
defaultRoomsParent.isVisible = state is State.Success
|
||||||
|
defaultRoomsLoader.isVisible = state is State.Loading
|
||||||
|
when (state) {
|
||||||
|
State.Loading -> {
|
||||||
|
// show a loader here probs
|
||||||
|
}
|
||||||
|
is State.Error -> {
|
||||||
|
// hide the loader and the
|
||||||
|
}
|
||||||
|
is State.Success -> {
|
||||||
|
populateDefaultGroups(state.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Convenience
|
||||||
private fun joinPublicChatIfPossible() {
|
private fun joinPublicChatIfPossible() {
|
||||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||||
var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
|
val chatURL = chatURLEditText.text.trim().toString().toLowerCase()
|
||||||
if (!chatURL.toLowerCase().startsWith("https")) {
|
|
||||||
chatURL = "https://$chatURL"
|
|
||||||
}
|
|
||||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
@ -9,8 +9,10 @@ import nl.komponents.kovenant.all
|
|||||||
import nl.komponents.kovenant.functional.map
|
import nl.komponents.kovenant.functional.map
|
||||||
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
import org.session.libsession.messaging.jobs.MessageReceiveJob
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
@ -90,6 +92,14 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
promises.add(poller.pollForNewMessages())
|
promises.add(poller.pollForNewMessages())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val openGroupsV2 = DatabaseFactory.getLokiThreadDatabase(context).getAllV2OpenGroups().values.groupBy(OpenGroupV2::server)
|
||||||
|
|
||||||
|
openGroupsV2.values.map { groups ->
|
||||||
|
OpenGroupV2Poller(groups)
|
||||||
|
}.forEach { poller ->
|
||||||
|
promises.add(poller.compactPoll(true).map{ /*Unit*/ })
|
||||||
|
}
|
||||||
|
|
||||||
// Wait till all the promises get resolved
|
// Wait till all the promises get resolved
|
||||||
all(promises).get()
|
all(promises).get()
|
||||||
|
|
||||||
|
@ -16,6 +16,25 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
|||||||
|
|
||||||
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
||||||
private const val DATA_KEY_CHANNEL = "channel"
|
private const val DATA_KEY_CHANNEL = "channel"
|
||||||
|
private const val DATA_KEY_ROOM = "room"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun scheduleInstant(context: Context, serverUrl: String, room :String) {
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
|
||||||
|
.setConstraints(Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setInputData(workDataOf(
|
||||||
|
DATA_KEY_SERVER_URL to serverUrl,
|
||||||
|
DATA_KEY_ROOM to room
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager
|
||||||
|
.getInstance(context)
|
||||||
|
.enqueue(workRequest)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
||||||
@ -39,17 +58,35 @@ class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters)
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
||||||
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
||||||
|
val room = inputData.getString(DATA_KEY_ROOM)
|
||||||
|
|
||||||
val publicChatId = OpenGroup.getId(channel, serverUrl)
|
val isOpenGroupV2 = !room.isNullOrEmpty() && channel == -1L
|
||||||
|
|
||||||
|
if (!isOpenGroupV2) {
|
||||||
|
val publicChatId = OpenGroup.getId(channel, serverUrl)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.v(TAG, "Updating open group info for $publicChatId.")
|
||||||
|
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
|
||||||
|
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val openGroupId = "$serverUrl.$room"
|
||||||
|
|
||||||
|
return try {
|
||||||
|
Log.v(TAG, "Updating open group info for $openGroupId.")
|
||||||
|
OpenGroupUtilities.updateGroupInfo(context, serverUrl, room!!)
|
||||||
|
Log.v(TAG, "Open group info was successfully updated for $openGroupId.")
|
||||||
|
Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to update open group info for $openGroupId", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,9 @@ import android.graphics.Bitmap
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupInfo
|
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
|
||||||
|
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupV2Poller
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.Util
|
import org.session.libsession.utilities.Util
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
@ -20,15 +19,17 @@ import java.util.concurrent.Executors
|
|||||||
|
|
||||||
class PublicChatManager(private val context: Context) {
|
class PublicChatManager(private val context: Context) {
|
||||||
private var chats = mutableMapOf<Long, OpenGroup>()
|
private var chats = mutableMapOf<Long, OpenGroup>()
|
||||||
|
private var v2Chats = mutableMapOf<Long, OpenGroupV2>()
|
||||||
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
|
private val pollers = mutableMapOf<Long, OpenGroupPoller>()
|
||||||
|
private val v2Pollers = mutableMapOf<String, OpenGroupV2Poller>()
|
||||||
private val observers = mutableMapOf<Long, ContentObserver>()
|
private val observers = mutableMapOf<Long, ContentObserver>()
|
||||||
private var isPolling = false
|
private var isPolling = false
|
||||||
private val executorService = Executors.newScheduledThreadPool(16)
|
private val executorService = Executors.newScheduledThreadPool(4)
|
||||||
|
|
||||||
public fun areAllCaughtUp(): Boolean {
|
public fun areAllCaughtUp(): Boolean {
|
||||||
var areAllCaughtUp = true
|
var areAllCaughtUp = true
|
||||||
refreshChatsAndPollers()
|
refreshChatsAndPollers()
|
||||||
for ((threadID, chat) in chats) {
|
for ((threadID, _) in chats) {
|
||||||
val poller = pollers[threadID]
|
val poller = pollers[threadID]
|
||||||
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true
|
areAllCaughtUp = if (poller != null) areAllCaughtUp && poller.isCaughtUp else true
|
||||||
}
|
}
|
||||||
@ -52,6 +53,17 @@ class PublicChatManager(private val context: Context) {
|
|||||||
listenToThreadDeletion(threadId)
|
listenToThreadDeletion(threadId)
|
||||||
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
|
||||||
}
|
}
|
||||||
|
v2Pollers.values.forEach { it.stop() }
|
||||||
|
v2Pollers.clear()
|
||||||
|
v2Chats.entries.groupBy { (_, group) -> group.server }.forEach { (server, threadedRooms) ->
|
||||||
|
val poller = OpenGroupV2Poller(threadedRooms.map { it.value }, executorService)
|
||||||
|
poller.startIfNeeded()
|
||||||
|
threadedRooms.forEach { (thread, _) ->
|
||||||
|
listenToThreadDeletion(thread)
|
||||||
|
}
|
||||||
|
v2Pollers[server] = poller
|
||||||
|
}
|
||||||
|
|
||||||
isPolling = true
|
isPolling = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +110,26 @@ class PublicChatManager(private val context: Context) {
|
|||||||
return chat
|
return chat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun addChat(server: String, room: String, info: OpenGroupAPIV2.Info, publicKey: String): OpenGroupV2 {
|
||||||
|
val chat = OpenGroupV2(server, room, info.name, publicKey)
|
||||||
|
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||||
|
val profilePicture: Bitmap?
|
||||||
|
if (threadID < 0) {
|
||||||
|
val profilePictureAsByteArray = try {
|
||||||
|
OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id,server).get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray)
|
||||||
|
val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name)
|
||||||
|
threadID = result.threadId
|
||||||
|
}
|
||||||
|
DatabaseFactory.getLokiThreadDatabase(context).setOpenGroupChat(chat, threadID)
|
||||||
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
return chat
|
||||||
|
}
|
||||||
|
|
||||||
public fun removeChat(server: String, channel: Long) {
|
public fun removeChat(server: String, channel: Long) {
|
||||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
val groupId = OpenGroup.getId(channel, server)
|
val groupId = OpenGroup.getId(channel, server)
|
||||||
@ -108,14 +140,26 @@ class PublicChatManager(private val context: Context) {
|
|||||||
Util.runOnMain { startPollersIfNeeded() }
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeChat(server: String, room: String) {
|
||||||
|
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||||
|
val groupId = "$server.$room"
|
||||||
|
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||||
|
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
||||||
|
GroupManager.deleteGroup(groupAddress, context)
|
||||||
|
|
||||||
|
Util.runOnMain { startPollersIfNeeded() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshChatsAndPollers() {
|
private fun refreshChatsAndPollers() {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val chatsInDB = storage.getAllOpenGroups()
|
val chatsInDB = storage.getAllOpenGroups()
|
||||||
|
val v2ChatsInDB = storage.getAllV2OpenGroups()
|
||||||
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
|
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
|
||||||
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
|
removedChatThreadIds.forEach { pollers.remove(it)?.stop() }
|
||||||
|
|
||||||
// Only append to chats if we have a thread for the chat
|
// Only append to chats if we have a thread for the chat
|
||||||
chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
chats = chatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
||||||
|
v2Chats = v2ChatsInDB.filter { GroupManager.getOpenGroupThreadID(it.value.id, context) > -1 }.toMutableMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenToThreadDeletion(threadID: Long) {
|
private fun listenToThreadDeletion(threadID: Long) {
|
||||||
@ -132,6 +176,8 @@ class PublicChatManager(private val context: Context) {
|
|||||||
|
|
||||||
DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID)
|
DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID)
|
||||||
pollers.remove(threadID)?.stop()
|
pollers.remove(threadID)?.stop()
|
||||||
|
v2Pollers.values.forEach { it.stop() }
|
||||||
|
v2Pollers.clear()
|
||||||
observers.remove(threadID)
|
observers.remove(threadID)
|
||||||
startPollersIfNeeded()
|
startPollersIfNeeded()
|
||||||
}
|
}
|
||||||
|
@ -286,6 +286,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastMessageServerID(room: String, server: String): Long? {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(lastMessageServerID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -293,12 +301,25 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setLastMessageServerID(room: String, server: String, newValue: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() ))
|
||||||
|
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLastMessageServerID(group: Long, server: String) {
|
fun removeLastMessageServerID(group: Long, server: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
|
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeLastMessageServerID(room: String, server:String) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
database.delete(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -307,6 +328,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLastDeletionServerID(room: String, server: String): Long? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(lastDeletionServerID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -314,6 +343,19 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setLastDeletionServerID(room: String, server: String, newValue: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf(lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString()))
|
||||||
|
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLastDeletionServerID(room: String, server: String) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLastDeletionServerID(group: Long, server: String) {
|
fun removeLastDeletionServerID(group: Long, server: String) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -328,6 +370,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
}?.toInt()
|
}?.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserCount(room: String, server: String): Int? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor ->
|
||||||
|
cursor.getInt(userCount)
|
||||||
|
}?.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val index = "$server.$group"
|
val index = "$server.$group"
|
||||||
@ -335,6 +385,13 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
|||||||
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setUserCount(room: String, server: String, newValue: Int) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val index = "$server.$room"
|
||||||
|
val row = wrap(mapOf( publicChatID to index, userCount to newValue.toString() ))
|
||||||
|
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
||||||
|
@ -5,10 +5,7 @@ import android.content.Context
|
|||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.get
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getInt
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.getString
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
|
|
||||||
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
|
import org.session.libsignal.service.loki.LokiMessageDatabaseProtocol
|
||||||
|
|
||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
||||||
@ -22,56 +19,111 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
private val friendRequestStatus = "friend_request_status"
|
private val friendRequestStatus = "friend_request_status"
|
||||||
private val threadID = "thread_id"
|
private val threadID = "thread_id"
|
||||||
private val errorMessage = "error_message"
|
private val errorMessage = "error_message"
|
||||||
@JvmStatic val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
private val messageType = "message_type"
|
||||||
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
@JvmStatic
|
||||||
@JvmStatic val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
|
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
||||||
|
@JvmStatic
|
||||||
|
val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
||||||
|
@JvmStatic
|
||||||
|
val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
|
||||||
|
@JvmStatic
|
||||||
|
val updateMessageIDTableForType = "ALTER TABLE $messageIDTable ADD COLUMN $messageType INTEGER DEFAULT 0; ALTER TABLE $messageIDTable ADD CONSTRAINT PK_$messageIDTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
|
@JvmStatic
|
||||||
|
val updateMessageMappingTable = "ALTER TABLE $messageThreadMappingTable ADD COLUMN $serverID INTEGER DEFAULT 0; ALTER TABLE $messageThreadMappingTable ADD CONSTRAINT PK_$messageThreadMappingTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
|
|
||||||
|
const val SMS_TYPE = 0
|
||||||
|
const val MMS_TYPE = 1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
|
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
|
||||||
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
|
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
|
||||||
return if (message != null) getServerID(message.getId()) else null
|
return if (message != null) getServerID(message.getId(), !message.isMms) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getServerID(messageID: Long): Long? {
|
fun getServerID(messageID: Long): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
|
cursor.getInt(serverID)
|
||||||
|
}?.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerID(messageID: Long, isSms: Boolean): Long? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(messageIDTable, "${Companion.messageID} = ? AND $messageType = ?", arrayOf(messageID.toString(), if (isSms) SMS_TYPE.toString() else MMS_TYPE.toString())) { cursor ->
|
||||||
cursor.getInt(serverID)
|
cursor.getInt(serverID)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMessageID(serverID: Long): Long? {
|
fun getMessageID(serverID: Long): Long? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
|
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf(serverID.toString())) { cursor ->
|
||||||
cursor.getInt(messageID)
|
cursor.getInt(messageID)
|
||||||
}?.toLong()
|
}?.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setServerID(messageID: Long, serverID: Long) {
|
fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
|
||||||
|
val serverID = database.get(messageIDTable,
|
||||||
|
"${Companion.messageID} = ? AND ${Companion.messageType} = ?",
|
||||||
|
arrayOf(messageID.toString(), (if (isSms) SMS_TYPE else MMS_TYPE).toString())) { cursor ->
|
||||||
|
cursor.getInt(Companion.serverID).toLong()
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
database.beginTransaction()
|
||||||
|
|
||||||
|
database.delete(messageIDTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
|
database.delete(messageThreadMappingTable, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageID(serverID: Long, threadID: Long): Pair<Long, Boolean>? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
val mappingResult = database.get(messageThreadMappingTable, "${Companion.serverID} = ? AND ${Companion.threadID} = ?",
|
||||||
|
arrayOf(serverID.toString(), threadID.toString())) { cursor ->
|
||||||
|
cursor.getInt(messageID) to cursor.getInt(Companion.serverID)
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
val (mappedID, mappedServerID) = mappingResult
|
||||||
|
|
||||||
|
return database.get(messageIDTable,
|
||||||
|
"$messageID = ? AND ${Companion.serverID} = ?",
|
||||||
|
arrayOf(mappedID.toString(), mappedServerID.toString())) { cursor ->
|
||||||
|
cursor.getInt(Companion.messageID).toLong() to (cursor.getInt(messageType) == SMS_TYPE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setServerID(messageID: Long, serverID: Long, isSms: Boolean) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.serverID, serverID)
|
contentValues.put(Companion.serverID, serverID)
|
||||||
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
contentValues.put(messageType, if (isSms) SMS_TYPE else MMS_TYPE)
|
||||||
|
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOriginalThreadID(messageID: Long): Long {
|
fun getOriginalThreadID(messageID: Long): Long {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
cursor.getInt(threadID)
|
cursor.getInt(threadID)
|
||||||
}?.toLong() ?: -1L
|
}?.toLong() ?: -1L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOriginalThreadID(messageID: Long, threadID: Long) {
|
fun setOriginalThreadID(messageID: Long, serverID: Long, threadID: Long) {
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
|
contentValues.put(Companion.serverID, serverID)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
contentValues.put(Companion.threadID, threadID)
|
||||||
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ? AND ${Companion.serverID} = ?", arrayOf(messageID.toString(), serverID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getErrorMessage(messageID: Long): String? {
|
fun getErrorMessage(messageID: Long): String? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
|
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
cursor.getString(errorMessage)
|
cursor.getString(errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,6 +133,6 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.messageID, messageID)
|
contentValues.put(Companion.messageID, messageID)
|
||||||
contentValues.put(Companion.errorMessage, errorMessage)
|
contentValues.put(Companion.errorMessage, errorMessage)
|
||||||
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
|
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,12 +10,11 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
|||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.loki.utilities.*
|
import org.thoughtcrime.securesms.loki.utilities.*
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
|
|
||||||
import org.session.libsignal.utilities.JsonUtil
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation
|
|
||||||
|
|
||||||
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
|
||||||
|
|
||||||
@ -26,8 +25,10 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
private val friendRequestStatus = "friend_request_status"
|
private val friendRequestStatus = "friend_request_status"
|
||||||
private val sessionResetStatus = "session_reset_status"
|
private val sessionResetStatus = "session_reset_status"
|
||||||
val publicChat = "public_chat"
|
val publicChat = "public_chat"
|
||||||
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
|
@JvmStatic
|
||||||
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
|
val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
|
||||||
|
@JvmStatic
|
||||||
|
val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThreadID(hexEncodedPublicKey: String): Long {
|
fun getThreadID(hexEncodedPublicKey: String): Long {
|
||||||
@ -46,11 +47,33 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
val threadID = cursor.getLong(threadID)
|
val threadID = cursor.getLong(threadID)
|
||||||
val string = cursor.getString(publicChat)
|
val string = cursor.getString(publicChat)
|
||||||
val publicChat = OpenGroup.fromJSON(string)
|
val publicChat = OpenGroup.fromJSON(string)
|
||||||
if (publicChat != null) { result[threadID] = publicChat }
|
if (publicChat != null) {
|
||||||
|
result[threadID] = publicChat
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
} finally {
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
var cursor: Cursor? = null
|
||||||
|
val result = mutableMapOf<Long, OpenGroupV2>()
|
||||||
|
try {
|
||||||
|
cursor = database.rawQuery("select * from $publicChatTable", null)
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
val threadID = cursor.getLong(threadID)
|
||||||
|
val string = cursor.getString(publicChat)
|
||||||
|
val openGroup = OpenGroupV2.fromJson(string)
|
||||||
|
if (openGroup != null) result[threadID] = openGroup
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
cursor?.close()
|
cursor?.close()
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -62,23 +85,48 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
|
|
||||||
fun getPublicChat(threadID: Long): OpenGroup? {
|
fun getPublicChat(threadID: Long): OpenGroup? {
|
||||||
if (threadID < 0) { return null }
|
if (threadID < 0) { return null }
|
||||||
|
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
|
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||||
val publicChatAsJSON = cursor.getString(publicChat)
|
val publicChatAsJSON = cursor.getString(publicChat)
|
||||||
OpenGroup.fromJSON(publicChatAsJSON)
|
OpenGroup.fromJSON(publicChatAsJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
|
||||||
|
if (threadID < 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
|
||||||
|
val json = cursor.getString(publicChat)
|
||||||
|
OpenGroupV2.fromJson(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOpenGroupChat(openGroupV2: OpenGroupV2, threadID: Long) {
|
||||||
|
if (threadID < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val contentValues = ContentValues(2)
|
||||||
|
contentValues.put(Companion.threadID, threadID)
|
||||||
|
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
|
||||||
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
|
fun setPublicChat(publicChat: OpenGroup, threadID: Long) {
|
||||||
if (threadID < 0) { return }
|
if (threadID < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val database = databaseHelper.writableDatabase
|
val database = databaseHelper.writableDatabase
|
||||||
val contentValues = ContentValues(2)
|
val contentValues = ContentValues(2)
|
||||||
contentValues.put(Companion.threadID, threadID)
|
contentValues.put(Companion.threadID, threadID)
|
||||||
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
|
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
|
||||||
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
|
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removePublicChat(threadID: Long) {
|
fun removePublicChat(threadID: Long) {
|
||||||
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
|
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.loki.utilities
|
package org.thoughtcrime.securesms.loki.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
|
|
||||||
object MentionManagerUtilities {
|
object MentionManagerUtilities {
|
||||||
|
|
||||||
|
@ -3,20 +3,47 @@ package org.thoughtcrime.securesms.loki.utilities
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
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.threads.recipients.Recipient
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
import org.session.libsession.utilities.preferences.ProfileKeyUtil
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
||||||
object OpenGroupUtilities {
|
object OpenGroupUtilities {
|
||||||
|
|
||||||
private const val TAG = "OpenGroupUtilities"
|
private const val TAG = "OpenGroupUtilities"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@WorkerThread
|
||||||
|
fun addGroup(context: Context, server: String, room: String, publicKey: String): OpenGroupV2 {
|
||||||
|
val groupId = "$server.$room"
|
||||||
|
val threadID = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||||
|
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID)
|
||||||
|
if (openGroup != null) return openGroup
|
||||||
|
|
||||||
|
MessagingModuleConfiguration.shared.storage.setOpenGroupPublicKey(server,publicKey)
|
||||||
|
OpenGroupAPIV2.getAuthToken(room, server).get()
|
||||||
|
val groupInfo = OpenGroupAPIV2.getInfo(room,server).get()
|
||||||
|
val application = ApplicationContext.getInstance(context)
|
||||||
|
|
||||||
|
val group = application.publicChatManager.addChat(server, room, groupInfo, publicKey)
|
||||||
|
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.removeLastDeletionServerId(room, server)
|
||||||
|
storage.removeLastMessageServerId(room, server)
|
||||||
|
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -67,5 +94,30 @@ object OpenGroupUtilities {
|
|||||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
data class GroupInfoUpdatedEvent(val url: String, val channel: Long)
|
@JvmStatic
|
||||||
|
@WorkerThread
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateGroupInfo(context: Context, server: String, room: String) {
|
||||||
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
|
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
||||||
|
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again?
|
||||||
|
OpenGroupAPIV2.getMemberCount(room, server).get()
|
||||||
|
|
||||||
|
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a generated name for users in the style of `$name (...$hex.takeLast(8))` for public groups
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getDisplayName(recipient: Recipient): String {
|
||||||
|
return String.format(Locale.ROOT, PUBLIC_GROUP_STRING_FORMAT, recipient.name, recipient.address.serialize().takeLast(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
const val PUBLIC_GROUP_STRING_FORMAT = "%s (...%s)"
|
||||||
|
|
||||||
|
data class GroupInfoUpdatedEvent(val url: String, val channel: Long = -1, val room: String = "")
|
||||||
}
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
|
|
||||||
|
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup>
|
||||||
|
typealias GroupState = State<DefaultGroups>
|
||||||
|
|
||||||
|
class DefaultGroupsViewModel : ViewModel() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultRooms = OpenGroupAPIV2.defaultRooms.map<DefaultGroups, GroupState> {
|
||||||
|
State.Success(it)
|
||||||
|
}.onStart {
|
||||||
|
emit(State.Loading)
|
||||||
|
}.asLiveData()
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki.viewmodel
|
||||||
|
|
||||||
|
sealed class State<out T> {
|
||||||
|
object Loading : State<Nothing>()
|
||||||
|
data class Success<T>(val value: T): State<T>()
|
||||||
|
data class Error(val error: Exception): State<Nothing>()
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/contentView"
|
android:id="@+id/contentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -22,7 +21,36 @@
|
|||||||
android:layout_marginTop="@dimen/large_spacing"
|
android:layout_marginTop="@dimen/large_spacing"
|
||||||
android:layout_marginRight="@dimen/large_spacing"
|
android:layout_marginRight="@dimen/large_spacing"
|
||||||
android:inputType="textWebEmailAddress"
|
android:inputType="textWebEmailAddress"
|
||||||
android:hint="Enter an open group URL" />
|
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||||
|
|
||||||
|
<com.github.ybq.android.spinkit.SpinKitView
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/defaultRoomsLoader"
|
||||||
|
style="@style/SpinKitView.Small.WanderingCubes"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:id="@+id/defaultRoomsParent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
|
android:columnCount="2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
14
app/src/main/res/layout/default_group_chip.xml
Normal file
14
app/src/main/res/layout/default_group_chip.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:theme="@style/Theme.MaterialComponents.DayNight"
|
||||||
|
style="?attr/chipStyle"
|
||||||
|
app:chipStartPadding="6dp"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
android:layout_marginHorizontal="2dp"
|
||||||
|
tools:text="Main Group"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:layout_width="wrap_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="52dp" />
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/contentView"
|
android:id="@+id/contentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -24,6 +23,35 @@
|
|||||||
android:inputType="textWebEmailAddress"
|
android:inputType="textWebEmailAddress"
|
||||||
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||||
|
|
||||||
|
<com.github.ybq.android.spinkit.SpinKitView
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/defaultRoomsLoader"
|
||||||
|
style="@style/SpinKitView.Small.WanderingCubes"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:id="@+id/defaultRoomsParent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/activity_join_public_chat_join_rooms"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/defaultRoomsGridLayout"
|
||||||
|
android:columnCount="2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
5
app/src/main/res/layout/grid_layout_filler.xml
Normal file
5
app/src/main/res/layout/grid_layout_filler.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
android:layout_height="0dp"/>
|
@ -1898,5 +1898,6 @@
|
|||||||
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
<string name="activity_backup_restore_passphrase">30-digit passphrase</string>
|
||||||
<!-- LinkDeviceActivity -->
|
<!-- LinkDeviceActivity -->
|
||||||
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
<string name="activity_link_device_skip_prompt">This is taking a while, would you like to skip?</string>
|
||||||
|
<string name="activity_join_public_chat_join_rooms">Or join one of these...</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -11,7 +11,8 @@ import java.io.InputStream
|
|||||||
interface MessageDataProvider {
|
interface MessageDataProvider {
|
||||||
|
|
||||||
fun getMessageID(serverID: Long): Long?
|
fun getMessageID(serverID: Long): Long?
|
||||||
fun deleteMessage(messageID: Long)
|
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||||
|
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||||
|
|
||||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
|||||||
import org.session.libsession.messaging.messages.visible.Attachment
|
import org.session.libsession.messaging.messages.visible.Attachment
|
||||||
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
import org.session.libsession.messaging.messages.visible.VisibleMessage
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.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.AttachmentId
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
@ -52,16 +53,18 @@ interface StorageProtocol {
|
|||||||
fun isJobCanceled(job: Job): Boolean
|
fun isJobCanceled(job: Job): Boolean
|
||||||
|
|
||||||
// Authorization
|
// Authorization
|
||||||
fun getAuthToken(server: String): String?
|
fun getAuthToken(room: String, server: String): String?
|
||||||
fun setAuthToken(server: String, newValue: String?)
|
fun setAuthToken(room: String, server: String, newValue: String)
|
||||||
fun removeAuthToken(server: String)
|
fun removeAuthToken(room: String, server: String)
|
||||||
|
|
||||||
|
// Open Groups
|
||||||
|
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2>
|
||||||
|
fun getV2OpenGroup(threadId: String): OpenGroupV2?
|
||||||
|
|
||||||
// Open Groups
|
// Open Groups
|
||||||
fun getOpenGroup(threadID: String): OpenGroup?
|
|
||||||
fun getThreadID(openGroupID: String): String?
|
fun getThreadID(openGroupID: String): String?
|
||||||
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
|
||||||
fun addOpenGroup(server: String, channel: Long)
|
fun addOpenGroup(server: String, channel: Long)
|
||||||
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
|
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)
|
||||||
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
|
||||||
|
|
||||||
// Open Group Public Keys
|
// Open Group Public Keys
|
||||||
@ -69,25 +72,24 @@ interface StorageProtocol {
|
|||||||
fun setOpenGroupPublicKey(server: String, newValue: String)
|
fun setOpenGroupPublicKey(server: String, newValue: String)
|
||||||
|
|
||||||
// Open Group User Info
|
// Open Group User Info
|
||||||
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
fun setOpenGroupDisplayName(publicKey: String, room: String, server: String, displayName: String)
|
||||||
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
fun getOpenGroupDisplayName(publicKey: String, room: String, server: String): String?
|
||||||
|
|
||||||
// Open Group Metadata
|
// Open Group Metadata
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
|
||||||
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
|
||||||
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
|
||||||
fun updateTitle(groupID: String, newValue: String)
|
fun updateTitle(groupID: String, newValue: String)
|
||||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||||
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
|
|
||||||
// Last Message Server ID
|
// Last Message Server ID
|
||||||
fun getLastMessageServerID(group: Long, server: String): Long?
|
fun getLastMessageServerId(room: String, server: String): Long?
|
||||||
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
fun setLastMessageServerId(room: String, server: String, newValue: Long)
|
||||||
fun removeLastMessageServerID(group: Long, server: String)
|
fun removeLastMessageServerId(room: String, server: String)
|
||||||
|
|
||||||
// Last Deletion Server ID
|
// Last Deletion Server ID
|
||||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
fun getLastDeletionServerId(room: String, server: String): Long?
|
||||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
fun setLastDeletionServerId(room: String, server: String, newValue: Long)
|
||||||
fun removeLastDeletionServerID(group: Long, server: String)
|
fun removeLastDeletionServerId(room: String, server: String)
|
||||||
|
|
||||||
// Message Handling
|
// Message Handling
|
||||||
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
fun isMessageDuplicated(timestamp: Long, sender: String): Boolean
|
||||||
@ -137,6 +139,7 @@ interface StorageProtocol {
|
|||||||
fun getOrCreateThreadIdFor(address: Address): Long
|
fun getOrCreateThreadIdFor(address: Address): Long
|
||||||
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long
|
||||||
fun getThreadIdFor(address: Address): Long?
|
fun getThreadIdFor(address: Address): Long?
|
||||||
|
fun getThreadIdForMms(mmsId: Long): Long
|
||||||
|
|
||||||
// Session Request
|
// Session Request
|
||||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||||
@ -164,4 +167,28 @@ interface StorageProtocol {
|
|||||||
|
|
||||||
// Data Extraction Notification
|
// Data Extraction Notification
|
||||||
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
fun insertDataExtractionNotificationMessage(senderPublicKey: String, message: DataExtractionNotificationInfoMessage, sentTimestamp: Long)
|
||||||
|
|
||||||
|
// DEPRECATED
|
||||||
|
fun getAuthToken(server: String): String?
|
||||||
|
fun setAuthToken(server: String, newValue: String?)
|
||||||
|
fun removeAuthToken(server: String)
|
||||||
|
|
||||||
|
fun getLastMessageServerID(group: Long, server: String): Long?
|
||||||
|
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
|
||||||
|
fun removeLastMessageServerID(group: Long, server: String)
|
||||||
|
|
||||||
|
fun getLastDeletionServerID(group: Long, server: String): Long?
|
||||||
|
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||||
|
fun removeLastDeletionServerID(group: Long, server: String)
|
||||||
|
|
||||||
|
fun getOpenGroup(threadID: String): OpenGroup?
|
||||||
|
fun getAllOpenGroups(): Map<Long, OpenGroup>
|
||||||
|
|
||||||
|
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||||
|
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
|
||||||
|
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
|
||||||
|
|
||||||
|
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
|
||||||
|
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package org.session.libsession.messaging.jobs
|
package org.session.libsession.messaging.jobs
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.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.sending_receiving.attachments.AttachmentState
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream
|
||||||
@ -10,7 +12,7 @@ import org.session.libsignal.utilities.logging.Log
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
|
||||||
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long): Job {
|
class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job {
|
||||||
override var delegate: JobDelegate? = null
|
override var delegate: JobDelegate? = null
|
||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
@ -22,6 +24,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
override val maxFailureCount: Int = 20
|
override val maxFailureCount: Int = 20
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val KEY: String = "AttachmentDownloadJob"
|
val KEY: String = "AttachmentDownloadJob"
|
||||||
|
|
||||||
@ -46,18 +49,31 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment)
|
val attachment = messageDataProvider.getDatabaseAttachment(attachmentID)
|
||||||
|
?: return handleFailure(Error.NoAttachment)
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID)
|
||||||
val tempFile = createTempFile()
|
val tempFile = createTempFile()
|
||||||
|
|
||||||
FileServerAPI.shared.downloadFile(tempFile, attachment.url, null)
|
val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID)
|
||||||
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString())
|
||||||
|
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
val stream = if (openGroupV2 == null) {
|
||||||
val stream = if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) FileInputStream(tempFile)
|
FileServerAPI.shared.downloadFile(tempFile, attachment.url, null)
|
||||||
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
else AttachmentCipherInputStream.createForAttachment(tempFile, attachment.size, Base64.decode(attachment.key), attachment.digest)
|
||||||
|
} else {
|
||||||
|
val url = HttpUrl.parse(attachment.url)!!
|
||||||
|
val fileId = url.pathSegments().last()
|
||||||
|
OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let {
|
||||||
|
tempFile.writeBytes(it)
|
||||||
|
}
|
||||||
|
FileInputStream(tempFile)
|
||||||
|
}
|
||||||
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream)
|
||||||
|
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -94,8 +110,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
return KEY
|
return KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory: Job.Factory<AttachmentDownloadJob> {
|
class Factory : Job.Factory<AttachmentDownloadJob> {
|
||||||
|
|
||||||
override fun create(data: Data): AttachmentDownloadJob {
|
override fun create(data: Data): AttachmentDownloadJob {
|
||||||
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
|
return AttachmentDownloadJob(data.getLong(KEY_ATTACHMENT_ID), data.getLong(KEY_TS_INCOMING_MESSAGE_ID))
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import com.esotericsoftware.kryo.io.Output
|
|||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||||
import org.session.libsession.messaging.messages.Message
|
import org.session.libsession.messaging.messages.Message
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.messaging.utilities.DotNetAPI
|
import org.session.libsession.messaging.utilities.DotNetAPI
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
|
||||||
@ -46,9 +47,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(Error.NoAttachment)
|
||||||
|
|
||||||
val usePadding = false
|
val usePadding = false
|
||||||
|
val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID)
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)
|
||||||
val server = if (openGroup != null) openGroup.server else FileServerAPI.shared.server
|
val server = openGroup?.let {
|
||||||
val shouldEncrypt = (openGroup == null) // Encrypt if this isn't an open group
|
it.server
|
||||||
|
} ?: openGroupV2?.let {
|
||||||
|
it.server
|
||||||
|
} ?: FileServerAPI.shared.server
|
||||||
|
val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group
|
||||||
|
|
||||||
val attachmentKey = Util.getSecretBytes(64)
|
val attachmentKey = Util.getSecretBytes(64)
|
||||||
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
|
||||||
@ -58,7 +64,11 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
|
||||||
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
|
||||||
|
|
||||||
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
|
val uploadResult = if (openGroupV2 == null) FileServerAPI.shared.uploadAttachment(server, attachmentData) else {
|
||||||
|
val dataBytes = attachmentData.data.readBytes()
|
||||||
|
val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get()
|
||||||
|
DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf())
|
||||||
|
}
|
||||||
handleSuccess(attachment, attachmentKey, uploadResult)
|
handleSuccess(attachment, attachmentKey, uploadResult)
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
if (e == Error.NoAttachment) {
|
if (e == Error.NoAttachment) {
|
||||||
|
@ -18,6 +18,7 @@ class JobQueue : JobDelegate {
|
|||||||
private var hasResumedPendingJobs = false // Just for debugging
|
private var hasResumedPendingJobs = false // Just for debugging
|
||||||
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
private val multiDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
|
||||||
private val scope = GlobalScope + SupervisorJob()
|
private val scope = GlobalScope + SupervisorJob()
|
||||||
private val queue = Channel<Job>(UNLIMITED)
|
private val queue = Channel<Job>(UNLIMITED)
|
||||||
|
|
||||||
@ -28,8 +29,15 @@ class JobQueue : JobDelegate {
|
|||||||
scope.launch(dispatcher) {
|
scope.launch(dispatcher) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
queue.receive().let { job ->
|
queue.receive().let { job ->
|
||||||
job.delegate = this@JobQueue
|
if (job.canExecuteParallel()) {
|
||||||
job.execute()
|
launch(multiDispatcher) {
|
||||||
|
job.delegate = this@JobQueue
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job.delegate = this@JobQueue
|
||||||
|
job.execute()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,6 +48,13 @@ class JobQueue : JobDelegate {
|
|||||||
val shared: JobQueue by lazy { JobQueue() }
|
val shared: JobQueue by lazy { JobQueue() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Job.canExecuteParallel(): Boolean {
|
||||||
|
return this.javaClass in arrayOf(
|
||||||
|
AttachmentUploadJob::class.java,
|
||||||
|
AttachmentDownloadJob::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun add(job: Job) {
|
fun add(job: Job) {
|
||||||
addWithoutExecuting(job)
|
addWithoutExecuting(job)
|
||||||
queue.offer(job) // offer always called on unlimited capacity
|
queue.offer(job) // offer always called on unlimited capacity
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
package org.session.libsession.messaging.messages
|
package org.session.libsession.messaging.messages
|
||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.service.loki.utilities.toHexString
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
|
||||||
|
typealias OpenGroupModel = OpenGroup
|
||||||
|
typealias OpenGroupV2Model = OpenGroupV2
|
||||||
|
|
||||||
sealed class Destination {
|
sealed class Destination {
|
||||||
|
|
||||||
class Contact(var publicKey: String) : Destination() {
|
class Contact(var publicKey: String) : Destination() {
|
||||||
@ -16,6 +21,9 @@ sealed class Destination {
|
|||||||
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
class OpenGroup(var channel: Long, var server: String) : Destination() {
|
||||||
internal constructor(): this(0, "")
|
internal constructor(): this(0, "")
|
||||||
}
|
}
|
||||||
|
class OpenGroupV2(var room: String, var server: String): Destination() {
|
||||||
|
internal constructor(): this("", "")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(address: Address): Destination {
|
fun from(address: Address): Destination {
|
||||||
@ -29,9 +37,13 @@ sealed class Destination {
|
|||||||
ClosedGroup(groupPublicKey)
|
ClosedGroup(groupPublicKey)
|
||||||
}
|
}
|
||||||
address.isOpenGroup -> {
|
address.isOpenGroup -> {
|
||||||
val threadID = MessagingModuleConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID)!!
|
val threadID = storage.getThreadID(address.contactIdentifier())!!
|
||||||
OpenGroup(openGroup.channel, openGroup.server)
|
when (val openGroup = storage.getOpenGroup(threadID) ?: storage.getV2OpenGroup(threadID)) {
|
||||||
|
is OpenGroupModel -> OpenGroup(openGroup.channel, openGroup.server)
|
||||||
|
is OpenGroupV2Model -> OpenGroupV2(openGroup.room, openGroup.server)
|
||||||
|
else -> throw Exception("Invalid OpenGroup $openGroup")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw Exception("TODO: Handle legacy closed groups.")
|
throw Exception("TODO: Handle legacy closed groups.")
|
||||||
|
@ -0,0 +1,505 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonNaming
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import nl.komponents.kovenant.Kovenant
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.functional.bind
|
||||||
|
import nl.komponents.kovenant.functional.map
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.Error
|
||||||
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
|
import org.session.libsession.utilities.AESGCM
|
||||||
|
import org.session.libsignal.service.loki.HTTP
|
||||||
|
import org.session.libsignal.service.loki.HTTP.Verb.*
|
||||||
|
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import org.session.libsignal.service.loki.utilities.toHexString
|
||||||
|
import org.session.libsignal.utilities.Base64.*
|
||||||
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object OpenGroupAPIV2 {
|
||||||
|
|
||||||
|
private val moderators: HashMap<String, Set<String>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs)
|
||||||
|
const val DEFAULT_SERVER = "http://116.203.70.33"
|
||||||
|
private const val DEFAULT_SERVER_PUBLIC_KEY = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
|
||||||
|
|
||||||
|
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
|
||||||
|
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
|
||||||
|
sealed class Error : Exception() {
|
||||||
|
object GENERIC : Error()
|
||||||
|
object PARSING_FAILED : Error()
|
||||||
|
object DECRYPTION_FAILED : Error()
|
||||||
|
object SIGNING_FAILED : Error()
|
||||||
|
object INVALID_URL : Error()
|
||||||
|
object NO_PUBLIC_KEY : Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DefaultGroup(val id: String,
|
||||||
|
val name: String,
|
||||||
|
val image: ByteArray?) {
|
||||||
|
fun toJoinUrl(): String = "$DEFAULT_SERVER/$id?public_key=$DEFAULT_SERVER_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Info(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val imageID: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class CompactPollRequest(val roomId: String,
|
||||||
|
val authToken: String,
|
||||||
|
val fromDeletionServerId: Long?,
|
||||||
|
val fromMessageServerId: Long?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CompactPollResult(val messages: List<OpenGroupMessageV2>,
|
||||||
|
val deletions: List<Long>,
|
||||||
|
val moderators: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
|
||||||
|
data class MessageDeletion @JvmOverloads constructor(val id: Long = 0,
|
||||||
|
val deletedMessageId: Long = 0
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY = MessageDeletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Request(
|
||||||
|
val verb: HTTP.Verb,
|
||||||
|
val room: String?,
|
||||||
|
val server: String,
|
||||||
|
val endpoint: String,
|
||||||
|
val queryParameters: Map<String, String> = mapOf(),
|
||||||
|
val parameters: Any? = null,
|
||||||
|
val headers: Map<String, String> = mapOf(),
|
||||||
|
val isAuthRequired: Boolean = true,
|
||||||
|
// Always `true` under normal circumstances. You might want to disable
|
||||||
|
// this when running over Lokinet.
|
||||||
|
val useOnionRouting: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createBody(parameters: Any?): RequestBody? {
|
||||||
|
if (parameters == null) return null
|
||||||
|
|
||||||
|
val parametersAsJSON = JsonUtil.toJson(parameters)
|
||||||
|
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send(request: Request, isJsonRequired: Boolean = true): Promise<Map<*, *>, Exception> {
|
||||||
|
val parsed = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.INVALID_URL)
|
||||||
|
val urlBuilder = HttpUrl.Builder()
|
||||||
|
.scheme(parsed.scheme())
|
||||||
|
.host(parsed.host())
|
||||||
|
.port(parsed.port())
|
||||||
|
.addPathSegments(request.endpoint)
|
||||||
|
|
||||||
|
if (request.verb == GET) {
|
||||||
|
for ((key, value) in request.queryParameters) {
|
||||||
|
urlBuilder.addQueryParameter(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun execute(token: String?): Promise<Map<*, *>, Exception> {
|
||||||
|
val requestBuilder = okhttp3.Request.Builder()
|
||||||
|
.url(urlBuilder.build())
|
||||||
|
.headers(Headers.of(request.headers))
|
||||||
|
if (request.isAuthRequired) {
|
||||||
|
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request")
|
||||||
|
requestBuilder.header("Authorization", token)
|
||||||
|
}
|
||||||
|
when (request.verb) {
|
||||||
|
GET -> requestBuilder.get()
|
||||||
|
PUT -> requestBuilder.put(createBody(request.parameters)!!)
|
||||||
|
POST -> requestBuilder.post(createBody(request.parameters)!!)
|
||||||
|
DELETE -> requestBuilder.delete(createBody(request.parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.room.isNullOrEmpty()) {
|
||||||
|
requestBuilder.header("Room", request.room)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.useOnionRouting) {
|
||||||
|
val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
|
||||||
|
?: return Promise.ofFail(Error.NO_PUBLIC_KEY)
|
||||||
|
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, publicKey, isJSONRequired = isJsonRequired)
|
||||||
|
.fail { e ->
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (request.isAuthRequired) {
|
||||||
|
getAuthToken(request.room!!, request.server).bind { execute(it) }
|
||||||
|
} else {
|
||||||
|
execute(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadOpenGroupProfilePicture(roomID: String, server: String): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(verb = GET, room = roomID, server = server, endpoint = "rooms/$roomID/image", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val result = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
decode(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
return storage.getAuthToken(room, server)?.let {
|
||||||
|
Promise.of(it)
|
||||||
|
} ?: run {
|
||||||
|
requestNewAuthToken(room, server)
|
||||||
|
.bind { claimAuthToken(it, room, server) }
|
||||||
|
.success { authToken ->
|
||||||
|
storage.setAuthToken(room, server, authToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
|
||||||
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair()
|
||||||
|
?: return Promise.ofFail(Error.GENERIC)
|
||||||
|
val queryParameters = mutableMapOf("public_key" to publicKey)
|
||||||
|
val request = Request(GET, room, server, "auth_token_challenge", queryParameters, isAuthRequired = false, parameters = null)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||||
|
val base64EncodedCiphertext = challenge["ciphertext"] as? String
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val ciphertext = decode(base64EncodedCiphertext)
|
||||||
|
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
|
||||||
|
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
|
||||||
|
val tokenAsData = try {
|
||||||
|
AESGCM.decrypt(ciphertext, symmetricKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Error.DECRYPTION_FAILED
|
||||||
|
}
|
||||||
|
tokenAsData.toHexString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun claimAuthToken(authToken: String, room: String, server: String): Promise<String, Exception> {
|
||||||
|
val parameters = mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
|
||||||
|
val headers = mapOf("Authorization" to authToken)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "claim_auth_token",
|
||||||
|
parameters = parameters, headers = headers, isAuthRequired = false)
|
||||||
|
return send(request).map { authToken }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAuthToken(room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "auth_token")
|
||||||
|
return send(request).map {
|
||||||
|
MessagingModuleConfiguration.shared.storage.removeAuthToken(room, server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Sending
|
||||||
|
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
|
||||||
|
val base64EncodedFile = encodeBytes(file)
|
||||||
|
val parameters = mapOf("file" to base64EncodedFile)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "files", parameters = parameters)
|
||||||
|
return send(request).map { json ->
|
||||||
|
json["result"] as? Long ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
|
||||||
|
return send(request).map { json ->
|
||||||
|
val base64EncodedFile = json["result"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
decode(base64EncodedFile) ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun send(message: OpenGroupMessageV2, room: String, server: String): Promise<OpenGroupMessageV2, Exception> {
|
||||||
|
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SIGNING_FAILED)
|
||||||
|
val jsonMessage = signedMessage.toJSON()
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "messages", parameters = jsonMessage)
|
||||||
|
return send(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.PARSING_FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Messages
|
||||||
|
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 ->
|
||||||
|
queryParameters += "from_server_id" to lastId.toString()
|
||||||
|
}
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "messages", queryParameters = queryParameters)
|
||||||
|
return send(request).map { jsonList ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val rawMessages = jsonList["messages"] as? List<Map<String, Any>>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val lastMessageServerId = storage.getLastMessageServerId(room, server) ?: 0
|
||||||
|
|
||||||
|
var currentMax = lastMessageServerId
|
||||||
|
val messages = rawMessages.mapNotNull { json ->
|
||||||
|
try {
|
||||||
|
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
|
||||||
|
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
|
||||||
|
val sender = message.sender
|
||||||
|
val data = decode(message.base64EncodedData)
|
||||||
|
val signature = decode(message.base64EncodedSignature)
|
||||||
|
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
|
||||||
|
val isValid = curve.verifySignature(publicKey, data, signature)
|
||||||
|
if (!isValid) {
|
||||||
|
Log.d("Loki", "Ignoring message with invalid signature")
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
if (message.serverID > lastMessageServerId) {
|
||||||
|
currentMax = message.serverID
|
||||||
|
}
|
||||||
|
message
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage.setLastMessageServerId(room, server, currentMax)
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Message Deletion
|
||||||
|
@JvmStatic
|
||||||
|
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Deleted server message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
queryParameters["from_server_id"] = last.toString()
|
||||||
|
}
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "deleted_messages", queryParameters = queryParameters)
|
||||||
|
return send(request).map { json ->
|
||||||
|
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.PARSING_FAILED
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
serverIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Moderation
|
||||||
|
private fun handleModerators(serverRoomId: String, moderatorList: List<String>) {
|
||||||
|
moderators[serverRoomId] = moderatorList.toMutableSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
|
||||||
|
return send(request).map { json ->
|
||||||
|
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
|
||||||
|
?: throw Error.PARSING_FAILED
|
||||||
|
val id = "$server.$room"
|
||||||
|
handleModerators(id, moderatorsJson)
|
||||||
|
moderatorsJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val parameters = mapOf("public_key" to publicKey)
|
||||||
|
val request = Request(verb = POST, room = room, server = server, endpoint = "block_list", parameters = parameters)
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Banned user $publicKey from $server.$room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
|
||||||
|
val request = Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
|
||||||
|
return send(request).map {
|
||||||
|
Log.d("Loki", "Unbanned user $publicKey from $server.$room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
|
||||||
|
moderators["$server.$room"]?.contains(publicKey) ?: false
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region General
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun getCompactPoll(rooms: List<String>, server: String): Promise<Map<String, CompactPollResult>, Exception> {
|
||||||
|
val requestAuths = rooms.associateWith { room -> getAuthToken(room, server) }
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val requests = rooms.mapNotNull { room ->
|
||||||
|
val authToken = try {
|
||||||
|
requestAuths[room]?.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Failed to get auth token for $room", e)
|
||||||
|
null
|
||||||
|
} ?: return@mapNotNull null
|
||||||
|
|
||||||
|
CompactPollRequest(roomId = room,
|
||||||
|
authToken = authToken,
|
||||||
|
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))
|
||||||
|
// build a request for all rooms
|
||||||
|
return send(request = request).map { json ->
|
||||||
|
val results = json["results"] as? List<*> ?: throw Error.PARSING_FAILED
|
||||||
|
|
||||||
|
results.mapNotNull { roomJson ->
|
||||||
|
if (roomJson !is Map<*,*>) return@mapNotNull null
|
||||||
|
val roomId = roomJson["room_id"] as? String ?: return@mapNotNull null
|
||||||
|
|
||||||
|
// check the status was fine
|
||||||
|
val statusCode = roomJson["status_code"] as? Int ?: return@mapNotNull null
|
||||||
|
if (statusCode == 401) {
|
||||||
|
// delete auth token and return null
|
||||||
|
storage.removeAuthToken(roomId, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check and store mods
|
||||||
|
val moderators = roomJson["moderators"] as? List<String> ?: return@mapNotNull null
|
||||||
|
handleModerators("$server.$roomId", moderators)
|
||||||
|
|
||||||
|
// get deletions
|
||||||
|
val type = TypeFactory.defaultInstance().constructCollectionType(List::class.java, MessageDeletion::class.java)
|
||||||
|
val idsAsString = JsonUtil.toJson(roomJson["deletions"])
|
||||||
|
val deletedServerIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type) ?: throw Error.PARSING_FAILED
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get messages
|
||||||
|
val rawMessages = roomJson["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null // parsing failed
|
||||||
|
|
||||||
|
val lastMessageServerId = storage.getLastMessageServerId(roomId, server) ?: 0
|
||||||
|
var currentMax = lastMessageServerId
|
||||||
|
val messages = rawMessages.mapNotNull { rawMessage ->
|
||||||
|
val message = OpenGroupMessageV2.fromJSON(rawMessage)?.apply {
|
||||||
|
currentMax = maxOf(currentMax,this.serverID ?: 0)
|
||||||
|
}
|
||||||
|
message
|
||||||
|
}
|
||||||
|
storage.setLastMessageServerId(roomId, server, currentMax)
|
||||||
|
roomId to CompactPollResult(
|
||||||
|
messages = messages,
|
||||||
|
deletions = deletedServerIDs.map { it.deletedMessageId },
|
||||||
|
moderators = moderators
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
val earlyGroups = groups.map { group ->
|
||||||
|
DefaultGroup(group.id, group.name, null)
|
||||||
|
}
|
||||||
|
// see if we have any cached rooms, and if they already have images, don't overwrite with early non-image results
|
||||||
|
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
|
||||||
|
if (replayed.none { it.image?.isNotEmpty() == true}) {
|
||||||
|
defaultRooms.tryEmit(earlyGroups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val images = groups.map { group ->
|
||||||
|
group.id to downloadOpenGroupProfilePicture(group.id, DEFAULT_SERVER)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
groups.map { group ->
|
||||||
|
val image = try {
|
||||||
|
images[group.id]!!.get()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// no image or image failed to download
|
||||||
|
null
|
||||||
|
}
|
||||||
|
DefaultGroup(group.id, group.name, image)
|
||||||
|
}
|
||||||
|
}.success { new ->
|
||||||
|
defaultRooms.tryEmit(new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInfo(room: String, server: String): Promise<Info, Exception> {
|
||||||
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms/$room", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.PARSING_FAILED
|
||||||
|
val id = rawRoom["id"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
val name = rawRoom["name"] as? String ?: throw Error.PARSING_FAILED
|
||||||
|
val imageID = rawRoom["image_id"] as? String
|
||||||
|
Info(id = id, name = name, imageID = imageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
|
||||||
|
val request = Request(verb = GET, room = null, server = server, endpoint = "rooms", isAuthRequired = false)
|
||||||
|
return send(request).map { json ->
|
||||||
|
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.PARSING_FAILED
|
||||||
|
rawRooms.mapNotNull {
|
||||||
|
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
|
||||||
|
val id = roomJson["id"] as? String ?: return@mapNotNull null
|
||||||
|
val name = roomJson["name"] as? String ?: return@mapNotNull null
|
||||||
|
val imageId = roomJson["image_id"] as? String
|
||||||
|
Info(id, name, imageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
|
||||||
|
val request = Request(verb = GET, room = room, server = server, endpoint = "member_count")
|
||||||
|
return send(request).map { json ->
|
||||||
|
val memberCount = json["member_count"] as? Int ?: throw Error.PARSING_FAILED
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
storage.setUserCount(room, server, memberCount)
|
||||||
|
memberCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Error.errorDescription() = when (this) {
|
||||||
|
Error.GENERIC -> "An error occurred."
|
||||||
|
Error.PARSING_FAILED -> "Invalid response."
|
||||||
|
Error.DECRYPTION_FAILED -> "Couldn't decrypt response."
|
||||||
|
Error.SIGNING_FAILED -> "Couldn't sign message."
|
||||||
|
Error.INVALID_URL -> "Invalid URL."
|
||||||
|
Error.NO_PUBLIC_KEY -> "Couldn't find server public key."
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.Base64.decode
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
|
|
||||||
|
data class OpenGroupMessageV2(
|
||||||
|
val serverID: Long? = null,
|
||||||
|
val sender: String?,
|
||||||
|
val sentTimestamp: Long,
|
||||||
|
// The serialized protobuf in base64 encoding
|
||||||
|
val base64EncodedData: String,
|
||||||
|
// When sending a message, the sender signs the serialized protobuf with their private key so that
|
||||||
|
// a receiving user can verify that the message wasn't tampered with.
|
||||||
|
val base64EncodedSignature: String? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val curve = Curve25519.getInstance(Curve25519.BEST)
|
||||||
|
|
||||||
|
fun fromJSON(json: Map<String, Any>): OpenGroupMessageV2? {
|
||||||
|
val base64EncodedData = json["data"] as? String ?: return null
|
||||||
|
val sentTimestamp = json["timestamp"] as? Long ?: return null
|
||||||
|
val serverID = json["server_id"] as? Int
|
||||||
|
val sender = json["public_key"] as? String
|
||||||
|
val base64EncodedSignature = json["signature"] as? String
|
||||||
|
return OpenGroupMessageV2(serverID = serverID?.toLong(),
|
||||||
|
sender = sender,
|
||||||
|
sentTimestamp = sentTimestamp,
|
||||||
|
base64EncodedData = base64EncodedData,
|
||||||
|
base64EncodedSignature = base64EncodedSignature
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sign(): OpenGroupMessageV2? {
|
||||||
|
if (base64EncodedData.isEmpty()) return null
|
||||||
|
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserKeyPair() ?: return null
|
||||||
|
|
||||||
|
if (sender != publicKey) return null // only sign our own messages?
|
||||||
|
|
||||||
|
val signature = try {
|
||||||
|
curve.calculateSignature(privateKey, decode(base64EncodedData))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Couldn't sign OpenGroupV2Message", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(base64EncodedSignature = Base64.encodeBytes(signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): Map<String, Any> {
|
||||||
|
val jsonMap = mutableMapOf("data" to base64EncodedData, "timestamp" to sentTimestamp)
|
||||||
|
serverID?.let { jsonMap["server_id"] = serverID }
|
||||||
|
sender?.let { jsonMap["public_key"] = sender }
|
||||||
|
base64EncodedSignature?.let { jsonMap["signature"] = base64EncodedSignature }
|
||||||
|
return jsonMap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toProto(): SignalServiceProtos.Content = decode(base64EncodedData).let(PushTransportDetails::getStrippedPaddingMessageBody).let { bytes ->
|
||||||
|
SignalServiceProtos.Content.parseFrom(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package org.session.libsession.messaging.open_groups
|
||||||
|
|
||||||
|
import org.session.libsignal.utilities.JsonUtil
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class OpenGroupV2(
|
||||||
|
val server: String,
|
||||||
|
val room: String,
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val publicKey: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
constructor(server: String, room: String, name: String, publicKey: String) : this(
|
||||||
|
server = server,
|
||||||
|
room = room,
|
||||||
|
id = "$server.$room",
|
||||||
|
name = name,
|
||||||
|
publicKey = publicKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun fromJson(jsonAsString: String): OpenGroupV2? {
|
||||||
|
return try {
|
||||||
|
val json = JsonUtil.fromJson(jsonAsString)
|
||||||
|
if (!json.has("room")) return null
|
||||||
|
|
||||||
|
val room = json.get("room").asText().toLowerCase(Locale.getDefault())
|
||||||
|
val server = json.get("server").asText().toLowerCase(Locale.getDefault())
|
||||||
|
val displayName = json.get("displayName").asText()
|
||||||
|
val publicKey = json.get("publicKey").asText()
|
||||||
|
|
||||||
|
OpenGroupV2(server, room, displayName, publicKey)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJson(): Map<String,String> = mapOf(
|
||||||
|
"room" to room,
|
||||||
|
"server" to server,
|
||||||
|
"displayName" to name,
|
||||||
|
"publicKey" to publicKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -7,7 +7,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
|
|
||||||
object MessageReceiver {
|
object MessageReceiver {
|
||||||
|
|
||||||
@ -37,6 +36,7 @@ object MessageReceiver {
|
|||||||
is UnknownEnvelopeType -> false
|
is UnknownEnvelopeType -> false
|
||||||
is InvalidSignature -> false
|
is InvalidSignature -> false
|
||||||
is NoData -> false
|
is NoData -> false
|
||||||
|
is NoThread -> false
|
||||||
is SenderBlocked -> false
|
is SenderBlocked -> false
|
||||||
is SelfSend -> false
|
is SelfSend -> false
|
||||||
else -> true
|
else -> true
|
||||||
@ -122,7 +122,6 @@ object MessageReceiver {
|
|||||||
message.recipient = userPublicKey
|
message.recipient = userPublicKey
|
||||||
message.sentTimestamp = envelope.timestamp
|
message.sentTimestamp = envelope.timestamp
|
||||||
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
message.receivedTimestamp = if (envelope.hasServerTimestamp()) envelope.serverTimestamp else System.currentTimeMillis()
|
||||||
Log.d("Loki", "time: ${envelope.timestamp}, sent: ${envelope.serverTimestamp}")
|
|
||||||
message.groupPublicKey = groupPublicKey
|
message.groupPublicKey = groupPublicKey
|
||||||
message.openGroupServerMessageID = openGroupServerID
|
message.openGroupServerMessageID = openGroupServerID
|
||||||
// Validate
|
// Validate
|
||||||
|
@ -12,14 +12,15 @@ import org.session.libsession.messaging.messages.control.ClosedGroupControlMessa
|
|||||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||||
import org.session.libsession.messaging.messages.visible.*
|
import org.session.libsession.messaging.messages.visible.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
|
||||||
import org.session.libsession.messaging.threads.Address
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.messaging.threads.recipients.Recipient
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
import org.session.libsession.snode.RawResponsePromise
|
import org.session.libsession.snode.RawResponsePromise
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.snode.SnodeModule
|
import org.session.libsession.snode.SnodeModule
|
||||||
import org.session.libsession.snode.SnodeMessage
|
import org.session.libsession.snode.SnodeMessage
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.SSKEnvironment
|
import org.session.libsession.utilities.SSKEnvironment
|
||||||
import org.session.libsignal.service.internal.push.PushTransportDetails
|
import org.session.libsignal.service.internal.push.PushTransportDetails
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
@ -62,7 +63,7 @@ object MessageSender {
|
|||||||
|
|
||||||
// Convenience
|
// Convenience
|
||||||
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
|
||||||
if (destination is Destination.OpenGroup) {
|
if (destination is Destination.OpenGroup || destination is Destination.OpenGroupV2) {
|
||||||
return sendToOpenGroupDestination(destination, message)
|
return sendToOpenGroupDestination(destination, message)
|
||||||
}
|
}
|
||||||
return sendToSnodeDestination(destination, message)
|
return sendToSnodeDestination(destination, message)
|
||||||
@ -90,7 +91,8 @@ object MessageSender {
|
|||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> message.recipient = destination.publicKey
|
is Destination.Contact -> message.recipient = destination.publicKey
|
||||||
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Validate the message
|
// Validate the message
|
||||||
if (!message.isValid()) { throw Error.InvalidMessage }
|
if (!message.isValid()) { throw Error.InvalidMessage }
|
||||||
@ -109,9 +111,9 @@ object MessageSender {
|
|||||||
if (message is VisibleMessage) {
|
if (message is VisibleMessage) {
|
||||||
val displayName = storage.getUserDisplayName()!!
|
val displayName = storage.getUserDisplayName()!!
|
||||||
val profileKey = storage.getUserProfileKey()
|
val profileKey = storage.getUserProfileKey()
|
||||||
val profilePrictureUrl = storage.getUserProfilePictureURL()
|
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||||
if (profileKey != null && profilePrictureUrl != null) {
|
if (profileKey != null && profilePictureUrl != null) {
|
||||||
message.profile = Profile(displayName, profileKey, profilePrictureUrl)
|
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||||
} else {
|
} else {
|
||||||
message.profile = Profile(displayName)
|
message.profile = Profile(displayName)
|
||||||
}
|
}
|
||||||
@ -128,7 +130,8 @@ object MessageSender {
|
|||||||
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
|
||||||
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
// Wrap the result
|
// Wrap the result
|
||||||
val kind: SignalServiceProtos.Envelope.Type
|
val kind: SignalServiceProtos.Envelope.Type
|
||||||
@ -142,7 +145,8 @@ object MessageSender {
|
|||||||
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE
|
||||||
senderPublicKey = destination.groupPublicKey
|
senderPublicKey = destination.groupPublicKey
|
||||||
}
|
}
|
||||||
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
is Destination.OpenGroup,
|
||||||
|
is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!")
|
||||||
}
|
}
|
||||||
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
|
||||||
// Send the result
|
// Send the result
|
||||||
@ -150,6 +154,7 @@ object MessageSender {
|
|||||||
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
val base64EncodedData = Base64.encodeBytes(wrappedMessage)
|
||||||
|
// Send the result
|
||||||
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
|
val snodeMessage = SnodeMessage(message.recipient!!, base64EncodedData, message.ttl, message.sentTimestamp!!)
|
||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeModule.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
|
||||||
@ -204,32 +209,71 @@ object MessageSender {
|
|||||||
deferred.reject(error)
|
deferred.reject(error)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val server: String
|
|
||||||
val channel: Long
|
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
|
||||||
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
|
||||||
is Destination.OpenGroup -> {
|
is Destination.OpenGroup -> {
|
||||||
message.recipient = "${destination.server}.${destination.channel}"
|
message.recipient = "${destination.server}.${destination.channel}"
|
||||||
server = destination.server
|
val server = destination.server
|
||||||
channel = destination.channel
|
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
|
||||||
|
val room = destination.room
|
||||||
|
|
||||||
|
// Attach the user's profile if needed
|
||||||
|
if (message is VisibleMessage) {
|
||||||
|
val displayName = storage.getUserDisplayName()!!
|
||||||
|
val profileKey = storage.getUserProfileKey()
|
||||||
|
val profilePictureUrl = storage.getUserProfilePictureURL()
|
||||||
|
if (profileKey != null && profilePictureUrl != null) {
|
||||||
|
message.profile = Profile(displayName, profileKey, profilePictureUrl)
|
||||||
|
} else {
|
||||||
|
message.profile = Profile(displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the message
|
||||||
|
if (message !is VisibleMessage || !message.isValid()) {
|
||||||
|
throw Error.InvalidMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
val proto = message.toProto()!!
|
||||||
|
|
||||||
|
val openGroupMessage = OpenGroupMessageV2(
|
||||||
|
sender = message.sender,
|
||||||
|
sentTimestamp = message.sentTimestamp!!,
|
||||||
|
base64EncodedData = Base64.encodeBytes(proto.toByteArray()),
|
||||||
|
)
|
||||||
|
|
||||||
|
OpenGroupAPIV2.send(openGroupMessage,room,server).success {
|
||||||
|
message.openGroupServerMessageID = it.serverID
|
||||||
|
handleSuccessfulMessageSend(message, destination)
|
||||||
|
deferred.resolve(Unit)
|
||||||
|
}.fail {
|
||||||
|
handleFailure(it)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 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) ?: kotlin.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)
|
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
handleFailure(exception)
|
handleFailure(exception)
|
||||||
@ -245,8 +289,12 @@ object MessageSender {
|
|||||||
// Ignore future self-sends
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
// Track the open group server message ID
|
// Track the open group server message ID
|
||||||
if (message.openGroupServerMessageID != null) {
|
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
||||||
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||||
|
val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded))
|
||||||
|
if (threadID != null && threadID >= 0) {
|
||||||
|
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Mark the message as sent
|
// Mark the message as sent
|
||||||
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
|
@ -125,8 +125,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
|
||||||
|
val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.server }
|
||||||
for (openGroup in message.openGroups) {
|
for (openGroup in message.openGroups) {
|
||||||
if (allOpenGroups.contains(openGroup)) continue
|
if (allOpenGroups.contains(openGroup)) continue
|
||||||
|
// TODO: add in v2
|
||||||
storage.addOpenGroup(openGroup, 1)
|
storage.addOpenGroup(openGroup, 1)
|
||||||
}
|
}
|
||||||
if (message.displayName.isNotEmpty()) {
|
if (message.displayName.isNotEmpty()) {
|
||||||
@ -149,16 +151,27 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val userPublicKey = storage.getUserPublicKey()
|
val userPublicKey = storage.getUserPublicKey()
|
||||||
|
|
||||||
|
// Get or create thread
|
||||||
|
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
||||||
|
?: message.sender!!, message.groupPublicKey, openGroupID)
|
||||||
|
|
||||||
|
val openGroup = threadID.let {
|
||||||
|
storage.getOpenGroup(it.toString())
|
||||||
|
}
|
||||||
|
|
||||||
// Update profile if needed
|
// Update profile if needed
|
||||||
val newProfile = message.profile
|
val newProfile = message.profile
|
||||||
if (newProfile != null && openGroupID.isNullOrEmpty() && userPublicKey != message.sender) {
|
|
||||||
|
if (newProfile != null && userPublicKey != message.sender && openGroup == null) {
|
||||||
val profileManager = SSKEnvironment.shared.profileManager
|
val profileManager = SSKEnvironment.shared.profileManager
|
||||||
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false)
|
||||||
val displayName = newProfile.displayName!!
|
val displayName = newProfile.displayName!!
|
||||||
if (displayName.isNotEmpty()) {
|
if (displayName.isNotEmpty()) {
|
||||||
profileManager.setDisplayName(context, recipient, displayName)
|
profileManager.setDisplayName(context, recipient, displayName)
|
||||||
}
|
}
|
||||||
if (newProfile.profileKey?.isNotEmpty() == true && !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey)) {
|
if (newProfile.profileKey?.isNotEmpty() == true
|
||||||
|
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) {
|
||||||
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
profileManager.setProfileKey(context, recipient, newProfile.profileKey!!)
|
||||||
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
|
||||||
val newUrl = newProfile.profilePictureURL
|
val newUrl = newProfile.profilePictureURL
|
||||||
@ -170,9 +183,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get or create thread
|
|
||||||
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
|
|
||||||
?: message.sender!!, message.groupPublicKey, openGroupID)
|
|
||||||
// Parse quote if needed
|
// Parse quote if needed
|
||||||
var quoteModel: QuoteModel? = null
|
var quoteModel: QuoteModel? = null
|
||||||
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
if (message.quote != null && proto.dataMessage.hasQuote()) {
|
||||||
@ -213,7 +223,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
// Parse stickers if needed
|
// Parse stickers if needed
|
||||||
// Persist the message
|
// Persist the message
|
||||||
message.threadID = threadID
|
message.threadID = threadID
|
||||||
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.NoThread
|
val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage
|
||||||
// Parse & persist attachments
|
// Parse & persist attachments
|
||||||
// Start attachment downloads if needed
|
// Start attachment downloads if needed
|
||||||
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
storage.getAttachmentsForMessage(messageID).forEach { attachment ->
|
||||||
@ -222,6 +232,10 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
|
|||||||
JobQueue.shared.add(downloadJob)
|
JobQueue.shared.add(downloadJob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val openGroupServerID = message.openGroupServerMessageID
|
||||||
|
if (openGroupServerID != null) {
|
||||||
|
storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, !(message.isMediaMessage() || attachments.isNotEmpty()))
|
||||||
|
}
|
||||||
// Cancel any typing indicators if needed
|
// Cancel any typing indicators if needed
|
||||||
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
cancelTypingIndicatorsIfNeeded(message.sender!!)
|
||||||
//Notify the user if needed
|
//Notify the user if needed
|
||||||
|
@ -69,9 +69,6 @@ class ClosedGroupPoller {
|
|||||||
// ignore inactive group's messages
|
// ignore inactive group's messages
|
||||||
return@successBackground
|
return@successBackground
|
||||||
}
|
}
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
|
||||||
}
|
|
||||||
messages.forEach { envelope ->
|
messages.forEach { envelope ->
|
||||||
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
val job = MessageReceiveJob(envelope.toByteArray(), false)
|
||||||
JobQueue.shared.add(job)
|
JobQueue.shared.add(job)
|
||||||
|
@ -9,6 +9,8 @@ import org.session.libsession.messaging.jobs.MessageReceiveJob
|
|||||||
import org.session.libsession.messaging.open_groups.OpenGroup
|
import org.session.libsession.messaging.open_groups.OpenGroup
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
import org.session.libsession.messaging.open_groups.OpenGroupAPI
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
import org.session.libsession.messaging.open_groups.OpenGroupMessage
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsignal.service.internal.push.SignalServiceProtos.*
|
import org.session.libsignal.service.internal.push.SignalServiceProtos.*
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import org.session.libsignal.utilities.successBackground
|
import org.session.libsignal.utilities.successBackground
|
||||||
@ -72,7 +74,6 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ
|
|||||||
// Kovenant propagates a context to chained promises, so OpenGroupAPI.sharedContext should be used for all of the below
|
// 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 ->
|
OpenGroupAPI.getMessages(openGroup.channel, openGroup.server).successBackground { messages ->
|
||||||
// Process messages in the background
|
// Process messages in the background
|
||||||
Log.d("Loki", "received ${messages.size} messages")
|
|
||||||
messages.forEach { message ->
|
messages.forEach { message ->
|
||||||
try {
|
try {
|
||||||
val senderPublicKey = message.senderPublicKey
|
val senderPublicKey = message.senderPublicKey
|
||||||
@ -211,10 +212,13 @@ class OpenGroupPoller(private val openGroup: OpenGroup, private val executorServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun pollForDeletedMessages() {
|
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 ->
|
OpenGroupAPI.getDeletedMessageServerIDs(openGroup.channel, openGroup.server).success { deletedMessageServerIDs ->
|
||||||
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { MessagingModuleConfiguration.shared.messageDataProvider.getMessageID(it) }
|
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { messagingModule.messageDataProvider.getMessageID(it, threadId) }
|
||||||
deletedMessageIDs.forEach {
|
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||||
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(it)
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||||
}
|
}
|
||||||
}.fail {
|
}.fail {
|
||||||
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${openGroup.channel} on server: ${openGroup.server}.")
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
package org.session.libsession.messaging.sending_receiving.pollers
|
||||||
|
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
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.OpenGroupAPIV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
|
||||||
|
import org.session.libsession.messaging.open_groups.OpenGroupV2
|
||||||
|
import org.session.libsession.messaging.threads.Address
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
import org.session.libsignal.service.internal.push.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
|
import org.session.libsignal.utilities.successBackground
|
||||||
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class OpenGroupV2Poller(private val openGroups: List<OpenGroupV2>, private val executorService: ScheduledExecutorService? = null) {
|
||||||
|
|
||||||
|
private var hasStarted = false
|
||||||
|
@Volatile private var isPollOngoing = false
|
||||||
|
var isCaughtUp = false
|
||||||
|
|
||||||
|
private val cancellableFutures = mutableListOf<ScheduledFuture<out Any>>()
|
||||||
|
|
||||||
|
// use this as a receive time-based window to calculate re-poll interval
|
||||||
|
private val receivedQueue = ArrayDeque<Long>(50)
|
||||||
|
|
||||||
|
private fun calculatePollInterval(): Long {
|
||||||
|
// sample last default poll time * 2
|
||||||
|
while (receivedQueue.size > 50) {
|
||||||
|
receivedQueue.removeLast()
|
||||||
|
}
|
||||||
|
val sampleWindow = System.currentTimeMillis() - pollForNewMessagesInterval * 2
|
||||||
|
val numberInSample = receivedQueue.toList().filter { it > sampleWindow }.size.coerceAtLeast(1)
|
||||||
|
return ((2 + (50 / numberInSample / 20)*5) * 1000).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Settings
|
||||||
|
companion object {
|
||||||
|
private val pollForNewMessagesInterval: Long = 10 * 1000
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
fun startIfNeeded() {
|
||||||
|
if (hasStarted || executorService == null) return
|
||||||
|
cancellableFutures += executorService.schedule(::compactPoll, 0, TimeUnit.MILLISECONDS)
|
||||||
|
hasStarted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
cancellableFutures.forEach { future ->
|
||||||
|
future.cancel(false)
|
||||||
|
}
|
||||||
|
cancellableFutures.clear()
|
||||||
|
hasStarted = false
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Polling
|
||||||
|
|
||||||
|
private fun compactPoll(): Promise<Any, Exception> {
|
||||||
|
return compactPoll(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compactPoll(isBackgroundPoll: Boolean): Promise<Any, Exception> {
|
||||||
|
if (isPollOngoing || !hasStarted) return Promise.of(Unit)
|
||||||
|
isPollOngoing = true
|
||||||
|
val server = openGroups.first().server // assume all the same server
|
||||||
|
val rooms = openGroups.map { it.room }
|
||||||
|
return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results ->
|
||||||
|
results.forEach { (room, results) ->
|
||||||
|
val serverRoomId = "$server.$room"
|
||||||
|
handleDeletedMessages(serverRoomId,results.deletions)
|
||||||
|
handleNewMessages(serverRoomId, results.messages.sortedBy { it.serverID }, isBackgroundPoll)
|
||||||
|
}
|
||||||
|
}.always {
|
||||||
|
isPollOngoing = false
|
||||||
|
if (!isBackgroundPoll) {
|
||||||
|
val delay = calculatePollInterval()
|
||||||
|
executorService?.schedule(this@OpenGroupV2Poller::compactPoll, delay, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNewMessages(serverRoomId: String, newMessages: List<OpenGroupMessageV2>, isBackgroundPoll: Boolean) {
|
||||||
|
if (!hasStarted) return
|
||||||
|
newMessages.forEach { message ->
|
||||||
|
try {
|
||||||
|
val senderPublicKey = message.sender!!
|
||||||
|
// Main message
|
||||||
|
// Envelope
|
||||||
|
val builder = SignalServiceProtos.Envelope.newBuilder()
|
||||||
|
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
|
||||||
|
builder.source = senderPublicKey
|
||||||
|
builder.sourceDevice = 1
|
||||||
|
builder.content = message.toProto().toByteString()
|
||||||
|
builder.timestamp = message.sentTimestamp
|
||||||
|
val envelope = builder.build()
|
||||||
|
val job = MessageReceiveJob(envelope.toByteArray(), isBackgroundPoll, message.serverID, serverRoomId)
|
||||||
|
Log.d("Loki", "Scheduling Job $job")
|
||||||
|
if (isBackgroundPoll) {
|
||||||
|
job.executeAsync()
|
||||||
|
// The promise is just used to keep track of when we're done
|
||||||
|
} else {
|
||||||
|
JobQueue.shared.add(job)
|
||||||
|
}
|
||||||
|
receivedQueue.addFirst(message.sentTimestamp)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki", "Exception parsing message", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeletedMessages(serverRoomId: String, deletedMessageServerIDs: List<Long>) {
|
||||||
|
val messagingModule = MessagingModuleConfiguration.shared
|
||||||
|
val address = GroupUtil.getEncodedOpenGroupID(serverRoomId.toByteArray())
|
||||||
|
val threadId = messagingModule.storage.getThreadIdFor(Address.fromSerialized(address)) ?: return
|
||||||
|
|
||||||
|
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { serverId ->
|
||||||
|
messagingModule.messageDataProvider.getMessageID(serverId, threadId)
|
||||||
|
}
|
||||||
|
deletedMessageIDs.forEach { (messageId, isSms) ->
|
||||||
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
@ -10,7 +10,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
|
|||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.messaging.file_server.FileServerAPI
|
import org.session.libsession.messaging.file_server.FileServerAPI
|
||||||
|
|
||||||
import org.session.libsignal.utilities.logging.Log
|
|
||||||
import org.session.libsignal.utilities.DiffieHellman
|
import org.session.libsignal.utilities.DiffieHellman
|
||||||
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
import org.session.libsignal.service.api.messages.SignalServiceAttachment
|
||||||
@ -27,7 +26,7 @@ import org.session.libsignal.service.loki.HTTP
|
|||||||
import org.session.libsignal.service.loki.utilities.*
|
import org.session.libsignal.service.loki.utilities.*
|
||||||
import org.session.libsignal.utilities.*
|
import org.session.libsignal.utilities.*
|
||||||
import org.session.libsignal.utilities.Base64
|
import org.session.libsignal.utilities.Base64
|
||||||
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@ -269,13 +268,13 @@ open class DotNetAPI {
|
|||||||
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob
|
||||||
val data = json["data"] as? Map<*, *>
|
val data = json["data"] as? Map<*, *>
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
Log.d("Loki", "Couldn't parse attachment from: $json.")
|
Log.e("Loki", "Couldn't parse attachment from: $json.")
|
||||||
throw Error.ParsingFailed
|
throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong()
|
||||||
val url = data["url"] as? String
|
val url = data["url"] as? String
|
||||||
if (id == null || url == null || url.isEmpty()) {
|
if (id == null || url == null || url.isEmpty()) {
|
||||||
Log.d("Loki", "Couldn't parse upload from: $json.")
|
Log.e("Loki", "Couldn't parse upload from: $json.")
|
||||||
throw Error.ParsingFailed
|
throw Error.ParsingFailed
|
||||||
}
|
}
|
||||||
UploadResult(id, url, file.transmittedDigest)
|
UploadResult(id, url, file.transmittedDigest)
|
||||||
|
@ -14,7 +14,6 @@ import org.session.libsignal.utilities.*
|
|||||||
import org.session.libsignal.service.loki.Snode
|
import org.session.libsignal.service.loki.Snode
|
||||||
import org.session.libsignal.service.loki.*
|
import org.session.libsignal.service.loki.*
|
||||||
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
import org.session.libsession.utilities.AESGCM.EncryptionResult
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
|
||||||
import org.session.libsession.utilities.getBodyForOnionRequest
|
import org.session.libsession.utilities.getBodyForOnionRequest
|
||||||
import org.session.libsession.utilities.getHeadersForOnionRequest
|
import org.session.libsession.utilities.getHeadersForOnionRequest
|
||||||
import org.session.libsignal.service.loki.Broadcaster
|
import org.session.libsignal.service.loki.Broadcaster
|
||||||
@ -82,7 +81,7 @@ object OnionRequestAPI {
|
|||||||
|
|
||||||
internal sealed class Destination {
|
internal sealed class Destination {
|
||||||
class Snode(val snode: org.session.libsignal.service.loki.Snode) : Destination()
|
class Snode(val snode: org.session.libsignal.service.loki.Snode) : Destination()
|
||||||
class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination()
|
class Server(val host: String, val target: String, val x25519PublicKey: String, val scheme: String, val port: Int) : Destination()
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Private API
|
// region Private API
|
||||||
@ -330,7 +329,7 @@ object OnionRequestAPI {
|
|||||||
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
|
||||||
try {
|
try {
|
||||||
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
|
||||||
val statusCode = json["status"] as Int
|
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
|
||||||
if (statusCode == 406) {
|
if (statusCode == 406) {
|
||||||
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
|
||||||
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
val exception = HTTPRequestFailedAtDestinationException(statusCode, body)
|
||||||
@ -454,7 +453,7 @@ object OnionRequestAPI {
|
|||||||
val urlAsString = url.toString()
|
val urlAsString = url.toString()
|
||||||
val host = url.host()
|
val host = url.host()
|
||||||
val endpoint = when {
|
val endpoint = when {
|
||||||
server.count() < urlAsString.count() -> urlAsString.substringAfter("$server/")
|
server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/")
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
val body = request.getBodyForOnionRequest() ?: "null"
|
val body = request.getBodyForOnionRequest() ?: "null"
|
||||||
@ -464,7 +463,8 @@ object OnionRequestAPI {
|
|||||||
"method" to request.method(),
|
"method" to request.method(),
|
||||||
"headers" to headers
|
"headers" to headers
|
||||||
)
|
)
|
||||||
val destination = Destination.Server(host, target, x25519PublicKey)
|
url.isHttps
|
||||||
|
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port())
|
||||||
return sendOnionRequest(destination, payload, isJSONRequired).recover { exception ->
|
return sendOnionRequest(destination, payload, isJSONRequired).recover { exception ->
|
||||||
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
|
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
|
||||||
throw exception
|
throw exception
|
||||||
|
@ -70,7 +70,13 @@ object OnionRequestEncryption {
|
|||||||
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
|
||||||
}
|
}
|
||||||
is OnionRequestAPI.Destination.Server -> {
|
is OnionRequestAPI.Destination.Server -> {
|
||||||
payload = mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST" )
|
payload = mutableMapOf(
|
||||||
|
"host" to rhs.host,
|
||||||
|
"target" to rhs.target,
|
||||||
|
"method" to "POST",
|
||||||
|
"protocol" to rhs.scheme,
|
||||||
|
"port" to rhs.port
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
||||||
|
@ -41,7 +41,7 @@ object SnodeAPI {
|
|||||||
if (useTestnet) {
|
if (useTestnet) {
|
||||||
setOf( "http://public.loki.foundation:38157" )
|
setOf( "http://public.loki.foundation:38157" )
|
||||||
} else {
|
} else {
|
||||||
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
|
setOf( "https://storage.seed1.loki.network:$seedNodePort ", "https://storage.seed3.loki.network:$seedNodePort ", "https://public.loki.foundation:$seedNodePort" )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val snodeFailureThreshold = 4
|
private val snodeFailureThreshold = 4
|
||||||
|
@ -16,10 +16,10 @@ data class SnodeMessage(
|
|||||||
internal fun toJSON(): Map<String, String> {
|
internal fun toJSON(): Map<String, String> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
"pubKey" to if (SnodeAPI.useTestnet) recipient.removing05PrefixIfNeeded() else recipient,
|
||||||
"data" to data,
|
"data" to data,
|
||||||
"ttl" to ttl.toString(),
|
"ttl" to ttl.toString(),
|
||||||
"timestamp" to timestamp.toString(),
|
"timestamp" to timestamp.toString(),
|
||||||
"nonce" to ""
|
"nonce" to ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import org.whispersystems.curve25519.Curve25519
|
import androidx.annotation.WorkerThread
|
||||||
import org.session.libsignal.libsignal.util.ByteUtil
|
import org.session.libsignal.libsignal.util.ByteUtil
|
||||||
import org.session.libsignal.service.internal.util.Util
|
import org.session.libsignal.service.internal.util.Util
|
||||||
import org.session.libsignal.utilities.Hex
|
import org.session.libsignal.utilities.Hex
|
||||||
|
import org.whispersystems.curve25519.Curve25519
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import javax.crypto.spec.GCMParameterSpec
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
internal object AESGCM {
|
internal object AESGCM {
|
||||||
|
|
||||||
internal data class EncryptionResult(
|
internal data class EncryptionResult(
|
||||||
@ -31,6 +33,16 @@ internal object AESGCM {
|
|||||||
return cipher.doFinal(ciphertext)
|
return cipher.doFinal(ciphertext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync. Don't call from the main thread.
|
||||||
|
*/
|
||||||
|
internal fun generateSymmetricKey(x25519PublicKey: ByteArray, x25519PrivateKey: ByteArray): ByteArray {
|
||||||
|
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, x25519PrivateKey)
|
||||||
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
||||||
|
return mac.doFinal(ephemeralSharedSecret)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync. Don't call from the main thread.
|
* Sync. Don't call from the main thread.
|
||||||
*/
|
*/
|
||||||
@ -47,10 +59,7 @@ internal object AESGCM {
|
|||||||
internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
|
internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult {
|
||||||
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
|
val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey)
|
||||||
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
|
val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair()
|
||||||
val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey)
|
val symmetricKey = generateSymmetricKey(x25519PublicKey, ephemeralKeyPair.privateKey)
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
|
||||||
mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256"))
|
|
||||||
val symmetricKey = mac.doFinal(ephemeralSharedSecret)
|
|
||||||
val ciphertext = encrypt(plaintext, symmetricKey)
|
val ciphertext = encrypt(plaintext, symmetricKey)
|
||||||
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
|
return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,11 @@ interface LokiAPIDatabaseProtocol {
|
|||||||
fun getLastDeletionServerID(group: Long, server: String): Long?
|
fun getLastDeletionServerID(group: Long, server: String): Long?
|
||||||
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
|
||||||
fun setUserCount(group: Long, server: String, newValue: Int)
|
fun setUserCount(group: Long, server: String, newValue: Int)
|
||||||
|
fun getLastMessageServerID(room: String, server: String): Long?
|
||||||
|
fun setLastMessageServerID(room: String, server: String, newValue: Long)
|
||||||
|
fun getLastDeletionServerID(room: String, server: String): Long?
|
||||||
|
fun setLastDeletionServerID(room: String, server: String, newValue: Long)
|
||||||
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
fun getSessionRequestSentTimestamp(publicKey: String): Long?
|
||||||
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long)
|
||||||
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
fun getSessionRequestProcessedTimestamp(publicKey: String): Long?
|
||||||
|
@ -3,5 +3,5 @@ package org.session.libsignal.service.loki
|
|||||||
interface LokiMessageDatabaseProtocol {
|
interface LokiMessageDatabaseProtocol {
|
||||||
|
|
||||||
fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long?
|
fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long?
|
||||||
fun setServerID(messageID: Long, serverID: Long)
|
fun setServerID(messageID: Long, serverID: Long, isSms: Boolean)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,10 @@ package org.session.libsignal.utilities;
|
|||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.ResolvedType;
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
@ -42,6 +44,10 @@ public class JsonUtil {
|
|||||||
return objectMapper.readValue(serialized, clazz);
|
return objectMapper.readValue(serialized, clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static<T> T fromJson(String serialized, JavaType clazz) throws IOException {
|
||||||
|
return objectMapper.readValue(serialized, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
||||||
return objectMapper.readValue(serialized, clazz);
|
return objectMapper.readValue(serialized, clazz);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user