Merge branch 'dev' of https://github.com/loki-project/session-android into data-extraction-2

This commit is contained in:
Brice-W 2021-03-15 16:27:08 +11:00
commit 1daa2a336f
145 changed files with 1204 additions and 5428 deletions

View File

@ -157,8 +157,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2'
}
def canonicalVersionCode = 143
def canonicalVersionName = "1.8.0"
def canonicalVersionCode = 145
def canonicalVersionName = "1.9.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -32,9 +32,10 @@ import androidx.multidex.MultiDexApplication;
import org.conscrypt.Conscrypt;
import org.session.libsession.messaging.MessagingConfiguration;
import org.session.libsession.messaging.avatars.AvatarHelper;
import org.session.libsession.snode.SnodeConfiguration;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
@ -54,7 +55,7 @@ import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
import org.session.libsignal.utilities.logging.Log;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.session.libsession.utilities.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
@ -175,6 +176,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
DatabaseFactory.getStorage(this),
DatabaseFactory.getAttachmentProvider(this),
new SessionProtocolImpl(this));
SnodeConfiguration.Companion.configure(apiDB, broadcaster);
if (userPublicKey != null) {
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
@ -441,6 +443,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
org.session.libsession.messaging.fileserver.FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
return true;
}

View File

@ -37,7 +37,11 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import org.session.libsession.messaging.messages.visible.LinkPreview;
import org.session.libsession.messaging.messages.visible.Quote;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.MessageDetailsRecipientAdapter.RecipientDeliveryStatus;
import org.session.libsession.utilities.color.MaterialColor;
import org.thoughtcrime.securesms.conversation.ConversationItem;
@ -50,12 +54,12 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DateUtils;
import org.session.libsession.utilities.ExpirationUtil;
import org.session.libsession.utilities.Util;
@ -441,7 +445,28 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
private void onResendClicked(View v) {
MessageSender.resend(MessageDetailsActivity.this, messageRecord);
Recipient recipient = messageRecord.getRecipient();
VisibleMessage message = new VisibleMessage();
message.setId(messageRecord.getId());
message.setText(messageRecord.getBody());
message.setSentTimestamp(messageRecord.getTimestamp());
if (recipient.isGroupRecipient()) {
message.setGroupPublicKey(recipient.getAddress().toGroupString());
} else {
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
}
message.setThreadID(messageRecord.getThreadId());
if (messageRecord.isMms()) {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
message.setLinkPreview(LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
}
if (mmsMessageRecord.getQuote() != null) {
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
}
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
}
MessageSender.send(message, recipient.getAddress());
resendButton.setVisibility(View.GONE);
}
}

View File

@ -35,6 +35,7 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.session.libsession.messaging.threads.Address;
@ -249,7 +250,7 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
public void onContactSelected(String number) {
Recipient recipient = Recipient.from(this, Address.fromExternal(this, number), true);
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient);
createConversation(existingThread, recipient.getAddress(), ThreadDatabase.DistributionTypes.DEFAULT);
createConversation(existingThread, recipient.getAddress(), DistributionTypes.DEFAULT);
}
@Override

View File

@ -1,22 +1,29 @@
package org.thoughtcrime.securesms.attachments
import android.content.Context
import android.text.TextUtils
import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsession.utilities.Util
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceAttachment
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.events.PartProgressEvent
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import java.io.IOException
import java.io.InputStream
@ -40,6 +47,14 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return databaseAttachment.toSignalAttachmentStream(context)
}
override fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? {
val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = database.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
val mediaConstraints = MediaConstraints.getPushMediaConstraints()
val scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment) ?: return null
return getAttachmentFor(scaledAttachment)
}
override fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null
@ -51,12 +66,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value)
}
@Throws(Exception::class)
override fun uploadAttachment(attachmentId: Long) {
val attachmentUploadJob = AttachmentUploadJob(AttachmentId(attachmentId, 0), null)
attachmentUploadJob.onRun()
}
override fun getMessageForQuote(timestamp: Long, author: Address): Long? {
val messagingDatabase = DatabaseFactory.getMmsSmsDatabase(context)
return messagingDatabase.getMessageFor(timestamp, author)?.id
@ -72,6 +81,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return messagingDatabase.getMessage(messageID).body
}
override fun getAttachmentIDsFor(messageID: Long): List<Long> {
return DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageID).mapNotNull {
if (it.isQuote) return@mapNotNull null
it.attachmentId.rowId
}
}
override fun getLinkPreviewAttachmentIDFor(messageID: Long): Long? {
val message = DatabaseFactory.getMmsDatabase(context).getOutgoingMessage(messageID)
return message.linkPreviews.firstOrNull()?.attachmentId?.rowId
}
override fun insertAttachment(messageId: Long, attachmentId: Long, stream: InputStream) {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
attachmentDatabase.insertAttachmentsForPlaceholder(messageId, AttachmentId(attachmentId, 0), stream)
@ -79,7 +100,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
override fun isOutgoingMessage(timestamp: Long): Boolean {
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
return smsDatabase.isOutgoingMessage(timestamp)
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
}
override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id,
attachmentStream.contentType,
attachmentKey,
Optional.of(Util.toIntExact(attachmentStream.length)),
attachmentStream.preview,
attachmentStream.width, attachmentStream.height,
Optional.fromNullable(uploadResult.digest),
attachmentStream.fileName,
attachmentStream.voiceNote,
attachmentStream.caption,
uploadResult.url);
val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get()
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment)
}
override fun updateAttachmentAfterUploadFailed(attachmentId: Long) {
val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId)
}
override fun getMessageID(serverID: Long): Long? {
@ -92,6 +138,52 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
messagingDatabase.deleteMessage(messageID)
}
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context)
return attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0))
}
private fun scaleAndStripExif(attachmentDatabase: AttachmentDatabase, constraints: MediaConstraints, attachment: Attachment): Attachment? {
return try {
if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
val stripped = constraints.getResizedMedia(context, attachment)
attachmentDatabase.updateAttachmentData(attachment, stripped)
} else {
attachment
}
} else if (constraints.canResize(attachment)) {
val resized = constraints.getResizedMedia(context, attachment)
attachmentDatabase.updateAttachmentData(attachment, resized)
} else {
throw Exception("Size constraints could not be met!")
}
} catch (e: Exception) {
return null
}
}
private fun getAttachmentFor(attachment: Attachment): SignalServiceAttachmentStream? {
try {
if (attachment.dataUri == null || attachment.size == 0L) throw IOException("Assertion failed, outgoing attachment has no data!")
val `is` = PartAuthority.getAttachmentStream(context, attachment.dataUri!!)
return SignalServiceAttachment.newStreamBuilder()
.withStream(`is`)
.withContentType(attachment.contentType)
.withLength(attachment.size)
.withFileName(attachment.fileName)
.withVoiceNote(attachment.isVoiceNote)
.withWidth(attachment.width)
.withHeight(attachment.height)
.withCaption(attachment.caption)
.withListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(attachment, total, progress)) }
.build()
} catch (ioe: IOException) {
Log.w("Loki", "Couldn't open attachment", ioe)
}
return null
}
}
fun DatabaseAttachment.toAttachmentPointer(): SessionServiceAttachmentPointer {
@ -117,8 +209,28 @@ fun DatabaseAttachment.toAttachmentStream(context: Context): SessionServiceAttac
return attachmentStream
}
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(attachmentId.rowId, contentType, key?.toByteArray(), Optional.fromNullable(size.toInt()), Optional.absent(), width, height, Optional.fromNullable(digest), Optional.fromNullable(fileName), isVoiceNote, Optional.fromNullable(caption), url)
fun DatabaseAttachment.toSignalAttachmentPointer(): SignalServiceAttachmentPointer? {
if (TextUtils.isEmpty(location)) { return null }
if (TextUtils.isEmpty(key)) { return null }
return try {
val id: Long = location!!.toLong()
val key: ByteArray = Base64.decode(key!!)
SignalServiceAttachmentPointer(id,
contentType,
key,
Optional.of(Util.toIntExact(size)),
Optional.absent(),
width,
height,
Optional.fromNullable(digest),
Optional.fromNullable(fileName),
isVoiceNote,
Optional.fromNullable(caption),
url)
} catch (e: Exception) {
null
}
}
fun DatabaseAttachment.toSignalAttachmentStream(context: Context): SignalServiceAttachmentStream {

View File

@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
@ -30,11 +31,11 @@ public class MmsNotificationAttachment extends Attachment {
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED ||
status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY)
{
return AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
return AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING;
} else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) {
return AttachmentDatabase.TRANSFER_PROGRESS_STARTED;
return AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED;
} else {
return AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
return AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED;
}
}
}

View File

@ -18,7 +18,6 @@ import org.session.libsession.utilities.Conversions
import org.thoughtcrime.securesms.backup.BackupProtos.*
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.database.*
import org.session.libsignal.utilities.logging.Log
@ -91,7 +90,7 @@ object FullBackupExporter {
}
}
}
for (preference in IdentityKeyUtil.getBackupRecords(context)) {
for (preference in BackupUtil.getBackupRecords(context)) {
EventBus.getDefault().post(BackupEvent.createProgress(++count))
outputStream.writePreferenceEntry(preference)
}

View File

@ -20,6 +20,8 @@ import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.DocumentSlide;
@ -94,7 +96,7 @@ public class DocumentView extends FrameLayout {
controlToggle.displayQuick(downloadButton);
downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide));
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
} else if (showControls && documentSlide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
} else if (showControls && documentSlide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(downloadProgress);
downloadProgress.spin();
} else {

View File

@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import android.util.AttributeSet;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsignal.utilities.logging.Log;
import android.view.View;
import android.view.ViewGroup;
@ -248,7 +249,7 @@ public class ThumbnailView extends FrameLayout {
}
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
(slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
{
this.playOverlay.setVisibility(View.VISIBLE);
} else {
@ -402,7 +403,7 @@ public class ThumbnailView extends FrameLayout {
if (thumbnailClickListener != null &&
slide != null &&
slide.asAttachment().getDataUri() != null &&
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE)
{
thumbnailClickListener.onClick(view, slide);
} else if (parentClickListener != null) {

View File

@ -17,6 +17,7 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
@ -105,17 +106,17 @@ public class TransferControlView extends FrameLayout {
}
for (Slide slide : slides) {
if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
if (slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
downloadProgress.put(slide.asAttachment(), 1f);
}
}
switch (getTransferState(slides)) {
case AttachmentDatabase.TRANSFER_PROGRESS_STARTED:
case AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED:
showProgressSpinner(calculateProgress(downloadProgress));
break;
case AttachmentDatabase.TRANSFER_PROGRESS_PENDING:
case AttachmentDatabase.TRANSFER_PROGRESS_FAILED:
case AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING:
case AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED:
downloadDetailsText.setText(getDownloadText(this.slides));
display(downloadDetails);
break;
@ -174,9 +175,9 @@ public class TransferControlView extends FrameLayout {
}
private int getTransferState(@NonNull List<Slide> slides) {
int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE;
int transferState = AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
for (Slide slide : slides) {
if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
if (slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
transferState = slide.getTransferState();
} else {
transferState = Math.max(transferState, slide.getTransferState());
@ -189,7 +190,7 @@ public class TransferControlView extends FrameLayout {
if (slides.size() == 1) {
return slides.get(0).getContentDescription();
} else {
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE ? count + 1 : count);
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE ? count + 1 : count);
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
}
}

View File

@ -4,25 +4,21 @@ import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.messages.control.TypingIndicator;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SuppressLint("UseSparseArrays")
public class TypingStatusSender {
private static final String TAG = TypingStatusSender.class.getSimpleName();
private static final long REFRESH_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final long PAUSE_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
@ -82,12 +78,16 @@ public class TypingStatusSender {
private void sendTyping(long threadId, boolean typingStarted) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
if (recipient == null) { return; }
// Loki - Check whether we want to send a typing indicator to this user
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
// Loki - Take into account multi device
if (recipient == null) { return; }
long threadID = threadDatabase.getOrCreateThreadIdFor(recipient);
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadID, typingStarted));
TypingIndicator typingIndicator;
if (typingStarted) {
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STARTED);
} else {
typingIndicator = new TypingIndicator(TypingIndicator.Kind.STOPPED);
}
MessageSender.send(typingIndicator, recipient.getAddress());
}
private class StartRunnable implements Runnable {

View File

@ -85,6 +85,10 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsignal.libsignal.InvalidMessageException;
@ -137,7 +141,7 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
@ -153,9 +157,10 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
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.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -168,9 +173,8 @@ 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.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
@ -197,7 +201,6 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@ -571,16 +574,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final Context context = ConversationActivity.this.getApplicationContext();
sendMediaMessage(false,
message,
sendMediaMessage(message,
slideDeck,
inputPanel.getQuote().orNull(),
Collections.emptyList(),
Collections.emptyList(),
expiresIn,
subscriptionId,
initiating,
true).addListener(new AssertedSuccessListener<Void>() {
Optional.absent(),
initiating).addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
@ -648,7 +646,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (!isPushGroupConversation()) {
inflater.inflate(R.menu.conversation_mms_group_options, menu);
if (distributionType == ThreadDatabase.DistributionTypes.BROADCAST) {
if (distributionType == DistributionTypes.BROADCAST) {
menu.findItem(R.id.menu_distribution_broadcast).setChecked(true);
} else {
menu.findItem(R.id.menu_distribution_conversation).setChecked(true);
@ -808,9 +806,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient, expirationTime);
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
ExpirationTimerUpdate message = new ExpirationTimerUpdate(expirationTime);
message.setSentTimestamp(System.currentTimeMillis());
OutgoingExpirationUpdateMessage outgoingMessage = OutgoingExpirationUpdateMessage.from(message, recipient);
try {
message.setId(DatabaseFactory.getMmsDatabase(ConversationActivity.this).insertMessageOutbox(outgoingMessage, getAllocatedThreadId(ConversationActivity.this), false, null));
MessageSender.send(message, recipient.getAddress());
} catch (MmsException e) {
Log.w(TAG, e);
}
return null;
}
@ -839,17 +843,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
private void handleConversationSettings() {
/*
Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.ADDRESS_EXTRA, recipient.getAddress());
intent.putExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA,
isSecureText && !isSelfConversation());
startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
*/
}
private void handleUnmuteNotifications() {
recipient.setMuted(0);
@ -1018,7 +1011,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
try {
if (isClosedGroup) {
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey);
MessageSender.explicitLeave(groupPublicKey);
initializeEnabledCheck();
} else {
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
@ -1040,7 +1033,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleDistributionBroadcastEnabled(MenuItem item) {
distributionType = ThreadDatabase.DistributionTypes.BROADCAST;
distributionType = DistributionTypes.BROADCAST;
item.setChecked(true);
if (threadId != -1) {
@ -1048,7 +1041,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.BROADCAST);
.setDistributionType(threadId, DistributionTypes.BROADCAST);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -1056,7 +1049,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void handleDistributionConversationEnabled(MenuItem item) {
distributionType = ThreadDatabase.DistributionTypes.CONVERSATION;
distributionType = DistributionTypes.CONVERSATION;
item.setChecked(true);
if (threadId != -1) {
@ -1064,7 +1057,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getThreadDatabase(ConversationActivity.this)
.setDistributionType(threadId, ThreadDatabase.DistributionTypes.CONVERSATION);
.setDistributionType(threadId, DistributionTypes.CONVERSATION);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -1335,7 +1328,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (address == null) { finish(); return; }
recipient = Recipient.from(this, address, true);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT);
glideRequests = GlideApp.with(this);
recipient.addListener(this);
@ -1483,14 +1476,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
else if (contactData.numbers.size() > 1) selectContactInfo(contactData);
}
private void sendSharedContact(List<Contact> contacts) {
int subscriptionId = -1;
long expiresIn = recipient.getExpireMessages() * 1000L;
boolean initiating = threadId == -1;
sendMediaMessage(false, "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, subscriptionId, initiating, false);
}
private void selectContactInfo(ContactData contactData) {
final CharSequence[] numbers = new CharSequence[contactData.numbers.size()];
final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()];
@ -1677,7 +1662,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Context context = ConversationActivity.this;
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false);
if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) {
if (!SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) {
for (MarkedMessageInfo messageInfo : messageIds) {
MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo());
}
@ -1735,8 +1720,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
String message = getMessage();
int subscriptionId = -1;
long expiresIn = recipient.getExpireMessages() * 1000L;
boolean initiating = threadId == -1;
boolean needsSplit = message.length() > characterCalculator.calculateCharacters(message).maxPrimaryMessageSize;
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
@ -1747,9 +1730,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
needsSplit;
if (isMediaMessage) {
sendMediaMessage(expiresIn, subscriptionId, initiating);
sendMediaMessage(initiating);
} else {
sendTextMessage(expiresIn, subscriptionId, initiating);
sendTextMessage(initiating);
}
} catch (RecipientFormattingException ex) {
Log.w(TAG, ex);
@ -1764,23 +1747,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void sendMediaMessage(final long expiresIn, final int subscriptionId, boolean initiating)
private void sendMediaMessage(boolean initiating)
throws InvalidMessageException
{
Log.i(TAG, "Sending media message...");
sendMediaMessage(false, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, subscriptionId, initiating, true);
sendMediaMessage(getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), linkPreviewViewModel.getActiveLinkPreview(), initiating);
}
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
String body,
private ListenableFuture<Void> sendMediaMessage(String body,
SlideDeck slideDeck,
QuoteModel quote,
List<Contact> contacts,
List<LinkPreview> previews,
final long expiresIn,
final int subscriptionId,
final boolean initiating,
final boolean clearComposeBox)
Optional<LinkPreview> linkPreview,
final boolean initiating)
{
Pair<String, Optional<Slide>> splitMessage = getSplitMessage(body, characterCalculator.calculateCharacters(body).maxPrimaryMessageSize);
@ -1790,7 +1768,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
slideDeck.addSlide(splitMessage.second.get());
}
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, quote, contacts, previews);
List<Attachment> attachments = slideDeck.asAttachments();
VisibleMessage message = new VisibleMessage();
message.setSentTimestamp(System.currentTimeMillis());
message.setText(body);
OutgoingMediaMessage outgoingMessageCandidate = OutgoingMediaMessage.from(message, recipient, attachments, quote, linkPreview.orNull());
final SettableFuture<Void> future = new SettableFuture<>();
final Context context = getApplicationContext();
@ -1800,11 +1783,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate);
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
if (clearComposeBox) {
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false);
silentlySetComposeText("");
}
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false);
silentlySetComposeText("");
final long id = fragment.stageOutgoingMessage(outgoingMessage);
@ -1812,43 +1793,44 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
sendComplete(result);
try {
long allocatedThreadId = getAllocatedThreadId(context);
message.setId(DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMessage, allocatedThreadId, false, ()->fragment.releaseOutgoingMessage(id)));
MessageSender.send(message, recipient.getAddress(), attachments, quote, linkPreview.orNull());
sendComplete(allocatedThreadId);
} catch (MmsException e) {
Log.w(TAG, e);
sendComplete(threadId);
}
future.set(null);
return future;
}
private void sendTextMessage(final long expiresIn, final int subscriptionId, final boolean initiatingConversation)
private void sendTextMessage(final boolean initiating)
throws InvalidMessageException
{
final Context context = getApplicationContext();
final String messageBody = getMessage();
OutgoingTextMessage message;
message = new OutgoingEncryptedMessage(recipient, messageBody, expiresIn);
VisibleMessage message = new VisibleMessage();
message.setSentTimestamp(System.currentTimeMillis());
message.setText(messageBody);
OutgoingTextMessage outgoingTextMessage = OutgoingTextMessage.from(message, recipient);
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
silentlySetComposeText("");
final long id = fragment.stageOutgoingMessage(message);
final long id = fragment.stageOutgoingMessage(outgoingTextMessage);
if (initiatingConversation) {
if (initiating) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
long result = MessageSender.send(context, message, threadId, false, () -> fragment.releaseOutgoingMessage(id));
long allocatedThreadId = getAllocatedThreadId(context);
message.setId(DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(allocatedThreadId, outgoingTextMessage, false, message.getSentTimestamp(), ()->fragment.releaseOutgoingMessage(id)));
MessageSender.send(message, recipient.getAddress());
sendComplete(result);
}
private void showDefaultSmsPrompt() {
new AlertDialog.Builder(this)
.setMessage(R.string.ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app)
.setNegativeButton(R.string.ConversationActivity_no, (dialog, which) -> dialog.dismiss())
.setPositiveButton(R.string.ConversationActivity_yes, (dialog, which) -> handleMakeDefaultSms())
.show();
sendComplete(allocatedThreadId);
}
private void updateToggleButtonState() {
@ -1934,7 +1916,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
sendMediaMessage(false, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
sendMediaMessage("", slideDeck, inputPanel.getQuote().orNull(), Optional.absent(), initiating).addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@ -2319,6 +2301,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
// region Loki
private long getAllocatedThreadId(Context context) {
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
} else {
allocatedThreadId = threadId;
}
return allocatedThreadId;
}
private void updateTitleTextView(Recipient recipient) {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (recipient == null) {

View File

@ -57,6 +57,9 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.messages.visible.Quote;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MessageDetailsActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
@ -77,22 +80,21 @@ import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.session.libsession.utilities.task.ProgressDialogAsyncTask;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
@ -405,7 +407,7 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean userCanModerate = isPublicChat && PublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
// allow banning if moderating a public chat and only one user's messages are selected
boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1;
@ -521,7 +523,6 @@ public class ConversationFragment extends Fragment
ArrayList<Long> ignoredMessages = new ArrayList<>();
ArrayList<Long> failedMessages = new ArrayList<>();
boolean isSentByUser = true;
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
for (MessageRecord messageRecord : messageRecords) {
isSentByUser = isSentByUser && messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
@ -531,8 +532,8 @@ public class ConversationFragment extends Fragment
ignoredMessages.add(messageRecord.getId());
}
}
if (publicChat != null && publicChatAPI != null) {
publicChatAPI
if (publicChat != null) {
OpenGroupAPI
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> {
for (MessageRecord messageRecord : messageRecords) {
@ -602,9 +603,7 @@ public class ConversationFragment extends Fragment
protected Void doInBackground(String... userPublicKeyParam) {
String userPublicKey = userPublicKeyParam[0];
if (publicChat != null) {
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
if (publicChat != null && publicChatAPI != null) {
publicChatAPI
OpenGroupAPI
.ban(userPublicKey, publicChat.getServer())
.success(l -> {
Log.d("Loki", "User banned");
@ -613,7 +612,6 @@ public class ConversationFragment extends Fragment
Log.d("Loki", "Couldn't ban user due to error: " + e.toString() + ".");
return null;
});
}
} else {
Log.d("Loki", "Tried to ban user from a non-public chat");
}
@ -696,11 +694,32 @@ public class ConversationFragment extends Fragment
}
private void handleResendMessage(final MessageRecord message) {
final Context context = getActivity().getApplicationContext();
new AsyncTask<MessageRecord, Void, Void>() {
@Override
protected Void doInBackground(MessageRecord... messageRecords) {
MessageSender.resend(context, messageRecords[0]);
MessageRecord messageRecord = messageRecords[0];
Recipient recipient = messageRecord.getRecipient();
VisibleMessage message = new VisibleMessage();
message.setId(messageRecord.getId());
message.setText(messageRecord.getBody());
message.setSentTimestamp(messageRecord.getTimestamp());
if (recipient.isGroupRecipient()) {
message.setGroupPublicKey(recipient.getAddress().toGroupString());
} else {
message.setRecipient(messageRecord.getRecipient().getAddress().serialize());
}
message.setThreadID(messageRecord.getThreadId());
if (messageRecord.isMms()) {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (!mmsMessageRecord.getLinkPreviews().isEmpty()) {
message.setLinkPreview(org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from(mmsMessageRecord.getLinkPreviews().get(0)));
}
if (mmsMessageRecord.getQuote() != null) {
message.setQuote(Quote.Companion.from(mmsMessageRecord.getQuote().getQuoteModel()));
}
message.addSignalAttachments(mmsMessageRecord.getSlideDeck().asAttachments());
}
MessageSender.send(message, recipient.getAddress());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);

View File

@ -53,6 +53,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
import org.session.libsignal.service.loki.api.opengroups.PublicChatAPI;
@ -1003,10 +1004,10 @@ public class ConversationItem extends LinearLayout
if (messageRecord.isMms()) {
TextSlide slide = ((MmsMessageRecord) messageRecord).getSlideDeck().getTextSlide();
if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE) {
message = getResources().getString(R.string.ConversationItem_read_more);
action = () -> eventListener.onMoreTextClicked(conversationRecipient.getAddress(), messageRecord.getId(), messageRecord.isMms());
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
} else if (slide != null && slide.asAttachment().getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) {
message = getResources().getString(R.string.ConversationItem_pending);
action = () -> {};
} else if (slide != null) {

View File

@ -1,88 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsignal.metadata.SignalProtos;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.session.libsignal.service.api.push.SignalServiceAddress;
public class UnidentifiedAccessUtil {
private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName();
@WorkerThread
public static Optional<UnidentifiedAccess> getAccessFor(@NonNull Context context,
@NonNull Recipient recipient)
{
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
}
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null));
if (theirUnidentifiedAccessKey != null &&
ourUnidentifiedAccessKey != null &&
ourUnidentifiedAccessCertificate != null)
{
return Optional.of(new UnidentifiedAccess(theirUnidentifiedAccessKey));
}
return Optional.absent();
}
public static Optional<UnidentifiedAccess> getAccessForSync(@NonNull Context context) {
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
}
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
return Optional.of(new UnidentifiedAccess(ourUnidentifiedAccessKey));
}
return Optional.absent();
}
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
}
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
byte[] theirProfileKey = recipient.resolve().getProfileKey();
if (theirProfileKey == null) return Util.getSecretBytes(16);
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
}
private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) {
String ourNumber = TextSecurePreferences.getLocalNumber(context);
if (ourNumber != null) {
SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder()
.setSender(ourNumber)
.setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID)
.build();
return certificate.toByteArray();
}
return null;
}
}

View File

@ -1,21 +0,0 @@
package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import org.session.libsignal.libsignal.IdentityKeyPair;
import org.session.libsignal.libsignal.state.IdentityKeyStore;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
public class SignalProtocolStoreImpl implements IdentityKeyStore {
private final Context context;
public SignalProtocolStoreImpl(Context context) {
this.context = context;
}
@Override
public IdentityKeyPair getIdentityKeyPair() {
return IdentityKeyUtil.getIdentityKeyPair(context);
}
}

View File

@ -38,6 +38,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import org.json.JSONArray;
import org.json.JSONException;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsignal.utilities.logging.Log;
@ -119,12 +120,8 @@ public class AttachmentDatabase extends Database {
static final String AUDIO_VISUAL_SAMPLES = "audio_visual_samples"; // Small amount of audio byte samples to visualise the content (e.g. draw waveform).
static final String AUDIO_DURATION = "audio_duration"; // Duration of the audio track in milliseconds.
public static final int TRANSFER_PROGRESS_DONE = 0;
public static final int TRANSFER_PROGRESS_STARTED = 1;
public static final int TRANSFER_PROGRESS_PENDING = 2;
public static final int TRANSFER_PROGRESS_FAILED = 3;
private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?";
private static final String ROW_ID_WHERE = ROW_ID + " = ?";
private static final String PART_AUDIO_ONLY_WHERE = CONTENT_TYPE + " LIKE \"audio/%\"";
private static final String[] PROJECTION = new String[] {ROW_ID,
@ -199,21 +196,12 @@ public class AttachmentDatabase extends Database {
}
}
public boolean containsStickerPackId(@NonNull String stickerPackId) {
String selection = STICKER_PACK_ID + " = ?";
String[] args = new String[] { stickerPackId };
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, args, null, null, "1")) {
return cursor != null && cursor.moveToFirst();
}
}
public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId)
throws MmsException
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED);
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId));
@ -225,7 +213,7 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, PROJECTION, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
cursor = database.query(TABLE_NAME, PROJECTION, ROW_ID_WHERE, new String[]{String.valueOf(attachmentId.getRowId())}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
List<DatabaseAttachment> list = getAttachment(cursor);
@ -268,7 +256,7 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null);
cursor = database.query(TABLE_NAME, PROJECTION, TRANSFER_STATE + " = ?", new String[] {String.valueOf(AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED)}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
attachments.addAll(getAttachment(cursor));
}
@ -372,7 +360,7 @@ public class AttachmentDatabase extends Database {
values.put(DATA_RANDOM, dataInfo.random);
}
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, (String)null);
values.put(CONTENT_DISPOSITION, (String)null);
values.put(DIGEST, (byte[])null);
@ -395,7 +383,7 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
values.put(CONTENT_LOCATION, attachment.getLocation());
values.put(DIGEST, attachment.getDigest());
values.put(CONTENT_DISPOSITION, attachment.getKey());
@ -411,7 +399,7 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED);
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
}
@ -506,37 +494,17 @@ public class AttachmentDatabase extends Database {
databaseAttachment.getUrl());
}
public void updateAttachmentFileName(@NonNull AttachmentId attachmentId,
@Nullable String fileName)
{
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
contentValues.put(FILE_NAME, ExternalStorageUtil.getCleanFileName(fileName));
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, attachmentId.toStrings());
}
public void markAttachmentUploaded(long messageId, Attachment attachment) {
ContentValues values = new ContentValues(1);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE);
values.put(TRANSFER_STATE, AttachmentTransferProgress.TRANSFER_PROGRESS_DONE);
database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings());
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
((DatabaseAttachment) attachment).setUploaded(true);
}
public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) {
if (!(attachment instanceof DatabaseAttachment)) {
throw new AssertionError("Attempt to update attachment that doesn't belong to DB!");
}
setTransferState(messageId, ((DatabaseAttachment) attachment).getAttachmentId(), transferState);
}
public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) {
final ContentValues values = new ContentValues(1);
final SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -546,14 +514,6 @@ public class AttachmentDatabase extends Database {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
public boolean hasStickerAttachments() {
String selection = STICKER_PACK_ID + " NOT NULL";
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, selection, null, null, null, null, "1")) {
return cursor != null && cursor.moveToFirst();
}
}
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
protected @Nullable InputStream getDataStream(AttachmentId attachmentId, String dataType, long offset)

View File

@ -1,59 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import org.session.libsession.utilities.Util;
public class ContentValuesBuilder {
private final ContentValues contentValues;
public ContentValuesBuilder(ContentValues contentValues) {
this.contentValues = contentValues;
}
public void add(String key, String charsetKey, EncodedStringValue value) {
if (value != null) {
contentValues.put(key, Util.toIsoString(value.getTextString()));
contentValues.put(charsetKey, value.getCharacterSet());
}
}
public void add(String contentKey, byte[] value) {
if (value != null) {
contentValues.put(contentKey, Util.toIsoString(value));
}
}
public void add(String contentKey, int b) {
if (b != 0)
contentValues.put(contentKey, b);
}
public void add(String contentKey, long value) {
if (value != -1L)
contentValues.put(contentKey, value);
}
public ContentValues getContentValues() {
return contentValues;
}
}

View File

@ -7,9 +7,9 @@ import android.text.TextUtils;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.documents.Document;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.session.libsession.database.documents.Document;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.libsignal.IdentityKey;
@ -38,27 +38,6 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markAsSent(long messageId, boolean secure);
public abstract void markUnidentified(long messageId, boolean unidentified);
public void setMismatchedIdentity(long messageId, final Address address, final IdentityKey identityKey) {
List<IdentityKeyMismatch> items = new ArrayList<IdentityKeyMismatch>() {{
add(new IdentityKeyMismatch(address, identityKey));
}};
IdentityKeyMismatchList document = new IdentityKeyMismatchList(items);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
try {
setDocument(database, messageId, MISMATCHED_IDENTITIES, document);
database.setTransactionSuccessful();
} catch (IOException ioe) {
Log.w(TAG, ioe);
} finally {
database.endTransaction();
}
}
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
try {
addToDocument(messageId, MISMATCHED_IDENTITIES,

View File

@ -19,9 +19,7 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -38,22 +36,22 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.IdentityKeyMismatchList;
import org.session.libsession.database.documents.NetworkFailure;
import org.session.libsession.database.documents.NetworkFailureList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingExpirationUpdateMessage;
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;

View File

@ -86,15 +86,6 @@ public class MmsSmsDatabase extends Database {
}
}
public @Nullable MessageRecord getMessageFor(long messageId) {
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.ID + " = " + messageId, null, null)) {
MmsSmsDatabase.Reader reader = db.readerFor(cursor);
return reader.getNext();
}
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);

View File

@ -23,8 +23,6 @@ import android.database.Cursor;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase;
@ -32,15 +30,15 @@ import net.sqlcipher.database.SQLiteStatement;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage;
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
@ -51,7 +49,6 @@ import org.session.libsignal.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

View File

@ -13,7 +13,6 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.opengroups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachment
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address
@ -30,20 +29,21 @@ import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.opengroups.PublicChat
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import org.session.libsession.messaging.messages.signal.IncomingGroupMessage
import org.session.libsession.messaging.messages.signal.IncomingTextMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.utilities.preferences.ProfileKeyUtil
class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol {
override fun getUserPublicKey(): String? {
@ -65,8 +65,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
override fun getUserProfileKey(): ByteArray? {
val profileKey = TextSecurePreferences.getProfileKey(context) ?: return null
return profileKey.toByteArray()
return ProfileKeyUtil.getProfileKey(context)
}
override fun getUserProfilePictureURL(): String? {
@ -81,7 +80,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun getOrGenerateRegistrationID(): Int {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == null) {
if (registrationID == 0) {
registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
}
@ -90,7 +89,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long> {
val database = DatabaseFactory.getAttachmentDatabase(context)
val databaseAttachments = attachments.map { it.toDatabaseAttachment() }
val databaseAttachments = attachments.mapNotNull { it.toSignalAttachment() }
return database.insertAttachments(messageId, databaseAttachments)
}
@ -125,7 +124,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}.mapNotNull {
PointerAttachment.forPointer(Optional.of(it)).orNull()
}
val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull())
val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull().firstOrNull())
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!)
} else {
// It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment
@ -331,9 +330,13 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, serverID)
}
override fun markAsSent(messageID: Long) {
override fun getQuoteServerID(quoteID: Long, publicKey: String): Long? {
return DatabaseFactory.getLokiMessageDatabase(context).getQuoteServerID(quoteID, publicKey)
}
override fun markAsSent(timestamp: Long, author: String) {
val database = DatabaseFactory.getMmsSmsDatabase(context)
val messageRecord = database.getMessageFor(messageID)!!
val messageRecord = database.getMessageFor(timestamp, author) ?: return
if (messageRecord.isMms) {
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
mmsDatabase.markAsSent(messageRecord.getId(), true)
@ -343,9 +346,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun markUnidentified(messageID: Long) {
override fun markUnidentified(timestamp: Long, author: String) {
val database = DatabaseFactory.getMmsSmsDatabase(context)
val messageRecord = database.getMessageFor(messageID)!!
val messageRecord = database.getMessageFor(timestamp, author) ?: return
if (messageRecord.isMms) {
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
mmsDatabase.markUnidentified(messageRecord.getId(), true)
@ -355,15 +358,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
}
}
override fun setErrorMessage(messageID: Long, error: Exception) {
override fun setErrorMessage(timestamp: Long, author: String, error: Exception) {
val database = DatabaseFactory.getMmsSmsDatabase(context)
val messageRecord = database.getMessageFor(messageID) ?: return
val messageRecord = database.getMessageFor(timestamp, author) ?: return
if (messageRecord.isMms) {
val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
mmsDatabase.markAsSentFailed(messageID)
mmsDatabase.markAsSentFailed(messageRecord.getId())
} else {
val smsDatabase = DatabaseFactory.getSmsDatabase(context)
smsDatabase.markAsSentFailed(messageID)
smsDatabase.markAsSentFailed(messageRecord.getId())
}
}
@ -388,7 +391,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseFactory.getGroupDatabase(context).updateMembers(groupID, members)
}
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>) {
override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
.setType(type0)
@ -396,13 +399,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.addAllMembers(members)
.addAllAdmins(admins)
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
val smsDB = DatabaseFactory.getSmsDatabase(context)
smsDB.insertMessageInbox(infoMessage)
}
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long) {
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long) {
val userPublicKey = getUserPublicKey()
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
val groupContextBuilder = SignalServiceProtos.GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
@ -410,8 +414,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.setName(name)
.addAllMembers(members)
.addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context)
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true)
}

View File

@ -30,6 +30,7 @@ import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.GroupRecord;
@ -598,14 +599,6 @@ public class ThreadDatabase extends Database {
return new Reader(cursor);
}
public static class DistributionTypes {
public static final int DEFAULT = 2;
public static final int BROADCAST = 1;
public static final int CONVERSATION = 2;
public static final int ARCHIVE = 3;
public static final int INBOX_ZERO = 4;
}
public class Reader implements Closeable {
private final Cursor cursor;

View File

@ -1,245 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.text.TextUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class XmlBackup {
private static final String PROTOCOL = "protocol";
private static final String ADDRESS = "address";
private static final String CONTACT_NAME = "contact_name";
private static final String DATE = "date";
private static final String READABLE_DATE = "readable_date";
private static final String TYPE = "type";
private static final String SUBJECT = "subject";
private static final String BODY = "body";
private static final String SERVICE_CENTER = "service_center";
private static final String READ = "read";
private static final String STATUS = "status";
private static final String TOA = "toa";
private static final String SC_TOA = "sc_toa";
private static final String LOCKED = "locked";
private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
private final XmlPullParser parser;
public XmlBackup(String path) throws XmlPullParserException, FileNotFoundException {
this.parser = XmlPullParserFactory.newInstance().newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(new FileInputStream(path), null);
}
public XmlBackupItem getNext() throws IOException, XmlPullParserException {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (!name.equalsIgnoreCase("sms")) {
continue;
}
int attributeCount = parser.getAttributeCount();
if (attributeCount <= 0) {
continue;
}
XmlBackupItem item = new XmlBackupItem();
for (int i=0;i<attributeCount;i++) {
String attributeName = parser.getAttributeName(i);
if (attributeName.equals(PROTOCOL )) item.protocol = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(ADDRESS )) item.address = parser.getAttributeValue(i);
else if (attributeName.equals(CONTACT_NAME )) item.contactName = parser.getAttributeValue(i);
else if (attributeName.equals(DATE )) item.date = Long.parseLong(parser.getAttributeValue(i));
else if (attributeName.equals(READABLE_DATE )) item.readableDate = parser.getAttributeValue(i);
else if (attributeName.equals(TYPE )) item.type = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(SUBJECT )) item.subject = parser.getAttributeValue(i);
else if (attributeName.equals(BODY )) item.body = parser.getAttributeValue(i);
else if (attributeName.equals(SERVICE_CENTER)) item.serviceCenter = parser.getAttributeValue(i);
else if (attributeName.equals(READ )) item.read = Integer.parseInt(parser.getAttributeValue(i));
else if (attributeName.equals(STATUS )) item.status = Integer.parseInt(parser.getAttributeValue(i));
}
return item;
}
return null;
}
public static class XmlBackupItem {
private int protocol;
private String address;
private String contactName;
private long date;
private String readableDate;
private int type;
private String subject;
private String body;
private String serviceCenter;
private int read;
private int status;
public XmlBackupItem() {}
public XmlBackupItem(int protocol, String address, String contactName, long date, int type,
String subject, String body, String serviceCenter, int read, int status)
{
this.protocol = protocol;
this.address = address;
this.contactName = contactName;
this.date = date;
this.readableDate = dateFormatter.format(date);
this.type = type;
this.subject = subject;
this.body = body;
this.serviceCenter = serviceCenter;
this.read = read;
this.status = status;
}
public int getProtocol() {
return protocol;
}
public String getAddress() {
return address;
}
public String getContactName() {
return contactName;
}
public long getDate() {
return date;
}
public String getReadableDate() {
return readableDate;
}
public int getType() {
return type;
}
public String getSubject() {
return subject;
}
public String getBody() {
return body;
}
public String getServiceCenter() {
return serviceCenter;
}
public int getRead() {
return read;
}
public int getStatus() {
return status;
}
}
public static class Writer {
private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?>";
private static final String CREATED_BY = "<!-- File Created By Signal -->";
private static final String OPEN_TAG_SMSES = "<smses count=\"%d\">";
private static final String CLOSE_TAG_SMSES = "</smses>";
private static final String OPEN_TAG_SMS = " <sms ";
private static final String CLOSE_EMPTYTAG = "/>";
private static final String OPEN_ATTRIBUTE = "=\"";
private static final String CLOSE_ATTRIBUTE = "\" ";
private static final Pattern PATTERN = Pattern.compile("[^\u0020-\uD7FF]");
private final BufferedWriter bufferedWriter;
public Writer(String path, int count) throws IOException {
bufferedWriter = new BufferedWriter(new FileWriter(path, false));
bufferedWriter.write(XML_HEADER);
bufferedWriter.newLine();
bufferedWriter.write(CREATED_BY);
bufferedWriter.newLine();
bufferedWriter.write(String.format(Locale.ROOT, OPEN_TAG_SMSES, count));
}
public void writeItem(XmlBackupItem item) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(OPEN_TAG_SMS);
appendAttribute(stringBuilder, PROTOCOL, item.getProtocol());
appendAttribute(stringBuilder, ADDRESS, escapeXML(item.getAddress()));
appendAttribute(stringBuilder, CONTACT_NAME, escapeXML(item.getContactName()));
appendAttribute(stringBuilder, DATE, item.getDate());
appendAttribute(stringBuilder, READABLE_DATE, item.getReadableDate());
appendAttribute(stringBuilder, TYPE, item.getType());
appendAttribute(stringBuilder, SUBJECT, escapeXML(item.getSubject()));
appendAttribute(stringBuilder, BODY, escapeXML(item.getBody()));
appendAttribute(stringBuilder, TOA, "null");
appendAttribute(stringBuilder, SC_TOA, "null");
appendAttribute(stringBuilder, SERVICE_CENTER, item.getServiceCenter());
appendAttribute(stringBuilder, READ, item.getRead());
appendAttribute(stringBuilder, STATUS, item.getStatus());
appendAttribute(stringBuilder, LOCKED, 0);
stringBuilder.append(CLOSE_EMPTYTAG);
bufferedWriter.newLine();
bufferedWriter.write(stringBuilder.toString());
}
private <T> void appendAttribute(StringBuilder stringBuilder, String name, T value) {
stringBuilder.append(name).append(OPEN_ATTRIBUTE).append(value).append(CLOSE_ATTRIBUTE);
}
public void close() throws IOException {
bufferedWriter.newLine();
bufferedWriter.write(CLOSE_TAG_SMSES);
bufferedWriter.close();
}
private String escapeXML(String s) {
if (TextUtils.isEmpty(s)) return s;
Matcher matcher = PATTERN.matcher( s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;"));
StringBuffer st = new StringBuffer();
while (matcher.find()) {
String escaped="";
for (char ch: matcher.group(0).toCharArray()) {
escaped += ("&#" + ((int) ch) + ";");
}
matcher.appendReplacement(st, escaped);
}
matcher.appendTail(st);
return st.toString();
}
}
}

View File

@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.loki.database.LokiBackupFilesDatabase;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SessionJobDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsMigration;
public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@ -52,9 +53,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV19 = 40;
private static final int lokiV20 = 41;
private static final int lokiV21 = 42;
private static final int lokiV22 = 43;
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV21;
private static final int DATABASE_VERSION = lokiV22;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -121,6 +123,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
db.execSQL(LokiBackupFilesDatabase.getCreateTableCommand());
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -252,7 +255,21 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
"SmsSentJob",
"SmsReceiveJob",
"PushGroupUpdateJob",
"ResetThreadSessionJob");
"ResetThreadSessionJob",
"SendDeliveryReceiptJob");
}
if (oldVersion < lokiV22) {
db.execSQL(SessionJobDatabase.getCreateSessionJobTableCommand());
deleteJobRecords(db,
"PushGroupSendJob",
"PushMediaSendJob",
"PushTextSendJob",
"SendReadReceiptJob",
"TypingSendJob",
"AttachmentUploadJob",
"RequestGroupInfoJob",
"ClosedGroupUpdateMessageSendJobV2");
}
db.setTransactionSuccessful();

View File

@ -45,6 +45,7 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
return new Pair<>(cursor, leftIsRecent ? cursor.getPosition() : cursor.getCount() - 1 - cursor.getPosition());
}
}
cursor.close();
return null;
}

View File

@ -26,8 +26,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.threads.recipients.Recipient;

View File

@ -26,8 +26,8 @@ import android.text.style.StyleSpan;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.ExpirationUtil;
@ -158,18 +158,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isIdentityDefault(type);
}
public boolean isIdentityMismatchFailure() {
return mismatches != null && !mismatches.isEmpty();
}
public boolean isBundleKeyExchange() {
return SmsDatabase.Types.isBundleKeyExchange(type);
}
public boolean isContentBundleKeyExchange() {
return SmsDatabase.Types.isContentBundleKeyExchange(type);
}
public boolean isIdentityUpdate() {
return SmsDatabase.Types.isIdentityUpdate(type);
}
@ -195,10 +187,6 @@ public abstract class MessageRecord extends DisplayRecord {
return individualRecipient;
}
public int getRecipientDeviceId() {
return recipientDeviceId;
}
public long getType() {
return type;
}
@ -211,10 +199,6 @@ public abstract class MessageRecord extends DisplayRecord {
return networkFailures;
}
public boolean hasNetworkFailures() {
return networkFailures != null && !networkFailures.isEmpty();
}
protected SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@ -234,10 +218,6 @@ public abstract class MessageRecord extends DisplayRecord {
return (int)getId();
}
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresIn;
}

View File

@ -8,8 +8,8 @@ import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;

View File

@ -23,8 +23,8 @@ import android.text.SpannableString;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.session.libsession.messaging.threads.recipients.Recipient;

View File

@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -42,4 +43,8 @@ public class Quote {
public @NonNull SlideDeck getAttachment() {
return attachment;
}
public QuoteModel getQuoteModel() {
return new QuoteModel(id, author, text, missing, attachment.asAttachments());
}
}

View File

@ -24,7 +24,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.LinkedList;

View File

@ -3,42 +3,20 @@ package org.thoughtcrime.securesms.dependencies;
import android.content.Context;
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.session.libsession.utilities.TextSecurePreferences;
import dagger.Module;
import dagger.Provides;
@Module(complete = false, injects = {PushGroupSendJob.class,
PushTextSendJob.class,
PushMediaSendJob.class,
AttachmentDownloadJob.class,
RequestGroupInfoJob.class,
@Module(complete = false, injects = {AttachmentDownloadJob.class,
AvatarDownloadJob.class,
RetrieveProfileAvatarJob.class,
SendReadReceiptJob.class,
AppProtectionPreferenceFragment.class,
SendDeliveryReceiptJob.class,
TypingSendJob.class,
AttachmentUploadJob.class,
PushDecryptJob.class,
LinkPreviewRepository.class})
@ -46,30 +24,12 @@ public class SignalCommunicationModule {
private final Context context;
private SignalServiceMessageSender messageSender;
private SignalServiceMessageReceiver messageReceiver;
public SignalCommunicationModule(Context context) {
this.context = context;
}
@Provides
public synchronized SignalServiceMessageSender provideSignalMessageSender() {
if (this.messageSender == null) {
this.messageSender = new SignalServiceMessageSender(new SignalProtocolStoreImpl(context),
TextSecurePreferences.getLocalNumber(context),
DatabaseFactory.getLokiAPIDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context),
new SessionProtocolImpl(context),
DatabaseFactory.getLokiUserDatabase(context),
DatabaseFactory.getGroupDatabase(context),
((ApplicationContext)context.getApplicationContext()).broadcaster);
}
return this.messageSender;
}
@Provides
synchronized SignalServiceMessageReceiver provideSignalMessageReceiver() {
if (this.messageReceiver == null) {

View File

@ -2,36 +2,23 @@ package org.thoughtcrime.securesms.groups;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.protobuf.ByteString;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
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.libsession.utilities.MediaTypes;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@ -72,7 +59,7 @@ public class GroupManager {
groupDatabase.updateProfilePicture(groupId, avatarBytes);
long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(
groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
groupRecipient, DistributionTypes.CONVERSATION);
return new GroupActionResult(groupRecipient, threadID);
}
@ -95,82 +82,6 @@ public class GroupManager {
return groupDatabase.delete(groupId);
}
public static GroupActionResult updateGroup(@NonNull Context context,
@NonNull String groupId,
@NonNull Set<Recipient> members,
@Nullable Bitmap avatar,
@Nullable String name,
@NonNull Set<Recipient> admins)
{
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final Set<Address> memberAddresses = getMemberAddresses(members);
final Set<Address> adminAddresses = getMemberAddresses(admins);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses));
groupDatabase.updateTitle(groupId, name);
groupDatabase.updateProfilePicture(groupId, avatarBytes);
if (!GroupUtil.isMmsGroup(groupId)) {
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
} else {
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
return new GroupActionResult(groupRecipient, threadId);
}
}
private static GroupActionResult sendGroupUpdate(@NonNull Context context,
@NonNull String groupId,
@NonNull Set<Address> members,
@Nullable String groupName,
@Nullable byte[] avatar,
@NonNull Set<Address> admins)
{
Attachment avatarAttachment = null;
Address groupAddress = Address.fromSerialized(groupId);
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
List<String> numbers = new LinkedList<>();
for (Address member : members) {
numbers.add(member.serialize());
}
List<String> adminNumbers = new LinkedList<>();
for (Address admin : admins) {
adminNumbers.add(admin.serialize());
}
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId)))
.setType(GroupContext.Type.UPDATE)
.addAllMembers(numbers)
.addAllAdmins(adminNumbers);
if (groupName != null) groupContextBuilder.setName(groupName);
GroupContext groupContext = groupContextBuilder.build();
if (avatar != null) {
Uri avatarUri = BlobProvider.getInstance().forData(avatar).createForSingleUseInMemory();
avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null);
}
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId);
}
private static Set<Address> getMemberAddresses(Collection<Recipient> recipients) {
final Set<Address> results = new HashSet<>();
for (Recipient recipient : recipients) {
results.add(recipient.getAddress());
}
return results;
}
public static class GroupActionResult {
private Recipient groupRecipient;
private long threadId;

View File

@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting;
import org.greenrobot.eventbus.EventBus;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsignal.libsignal.InvalidMessageException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
@ -96,11 +97,11 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE;
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
}
}
@ -129,12 +130,12 @@ public class AttachmentDownloadJob extends BaseJob implements InjectableType {
if (!manual && !AttachmentUtil.isAutoDownloadPermitted(context, attachment)) {
Log.w(TAG, "Attachment can't be auto downloaded...");
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_PENDING);
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING);
return;
}
Log.i(TAG, "Downloading push part " + attachmentId);
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
retrieveAttachment(messageId, attachmentId, attachment);
}

View File

@ -1,169 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.greenrobot.eventbus.EventBus;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
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.PointerAttachment;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.loki.api.utilities.HTTP;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class AttachmentUploadJob extends BaseJob implements InjectableType {
public static final String KEY = "AttachmentUploadJob";
private static final String TAG = AttachmentUploadJob.class.getSimpleName();
private static final String KEY_ROW_ID = "row_id";
private static final String KEY_UNIQUE_ID = "unique_id";
private static final String KEY_DESTINATION = "destination";
private AttachmentId attachmentId;
private Address destination;
@Inject SignalServiceMessageSender messageSender;
public AttachmentUploadJob(AttachmentId attachmentId, Address destination) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(10)
.build(),
attachmentId, destination);
}
private AttachmentUploadJob(@NonNull Job.Parameters parameters, @NonNull AttachmentId attachmentId, Address destination) {
super(parameters);
this.attachmentId = attachmentId;
this.destination = destination;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putLong(KEY_ROW_ID, attachmentId.getRowId())
.putLong(KEY_UNIQUE_ID, attachmentId.getUniqueId())
.putString(KEY_DESTINATION, destination.serialize())
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws Exception {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
if (databaseAttachment == null) {
throw new IllegalStateException("Cannot find the specified attachment.");
}
// Only upload attachment if necessary
if (databaseAttachment.getUrl().isEmpty()) {
final Attachment attachment;
try {
MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), false, new SignalServiceAddress(destination.serialize()));
attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), databaseAttachment.getFastPreflightId()).get();
} catch (Exception e) {
// On any error make sure we mark the related DB record's transfer state as failed.
database.updateAttachmentAfterUploadFailed(databaseAttachment.getAttachmentId());
throw e;
}
database.updateAttachmentAfterUploadSucceeded(databaseAttachment.getAttachmentId(), attachment);
}
}
@Override
public void onCanceled() { }
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
return exception instanceof IOException ||
exception instanceof HTTP.HTTPRequestFailedException;
}
private SignalServiceAttachment getAttachmentFor(Attachment attachment) {
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
return SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withVoiceNote(attachment.isVoiceNote())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
return null;
}
private Attachment scaleAndStripExif(@NonNull AttachmentDatabase attachmentDatabase,
@NonNull MediaConstraints constraints,
@NonNull Attachment attachment)
throws UndeliverableMessageException
{
try {
if (constraints.isSatisfied(context, attachment)) {
if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = constraints.getResizedMedia(context, attachment);
return attachmentDatabase.updateAttachmentData(attachment, stripped);
} else {
return attachment;
}
} else if (constraints.canResize(attachment)) {
MediaStream resized = constraints.getResizedMedia(context, attachment);
return attachmentDatabase.updateAttachmentData(attachment, resized);
} else {
throw new UndeliverableMessageException("Size constraints could not be met!");
}
} catch (IOException | MmsException e) {
throw new UndeliverableMessageException(e);
}
}
public static final class Factory implements Job.Factory<AttachmentUploadJob> {
@Override
public @NonNull AttachmentUploadJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AttachmentUploadJob(parameters, new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)), Address.fromSerialized(data.getString(KEY_DESTINATION)));
}
}
}

View File

@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJobV2;
import java.util.ArrayList;
import java.util.Arrays;
@ -31,21 +30,12 @@ public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(ClosedGroupUpdateMessageSendJobV2.KEY, new ClosedGroupUpdateMessageSendJobV2.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory());
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory());
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
put(TypingSendJob.KEY, new TypingSendJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
}};

View File

@ -15,6 +15,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsignal.metadata.InvalidMetadataMessageException;
import org.session.libsignal.metadata.ProtocolInvalidMessageException;
import org.session.libsignal.service.api.crypto.SignalServiceCipher;
@ -33,8 +34,7 @@ import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.session.libsession.utilities.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
@ -59,15 +59,14 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage;
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceContent;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
@ -84,8 +83,6 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
import network.loki.messenger.R;
public class PushDecryptJob extends BaseJob implements InjectableType {
@ -102,8 +99,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private MessageNotifier messageNotifier;
@Inject SignalServiceMessageSender messageSender;
public PushDecryptJob(Context context) {
this(context, -1);
}
@ -232,17 +227,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleTextMessage(content, message, smsMessageId, Optional.absent());
}
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
handleUnknownGroupMessage(content, message.getGroupInfo().get());
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
}
if (SessionMetaProtocol.shouldSendDeliveryReceipt(message, Address.fromSerialized(content.getSender()))) {
handleNeedsDeliveryReceipt(content, message);
}
} else if (content.getReceiptMessage().isPresent()) {
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
@ -268,16 +255,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceGroup group)
{
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
}
}
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId)
@ -346,7 +323,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
attachments,
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000,
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
DistributionTypes.DEFAULT, quote.orNull(),
sharedContacts.or(Collections.emptyList()),
linkPreviews.or(Collections.emptyList()),
Collections.emptyList(), Collections.emptyList());
@ -476,10 +453,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
}
Long threadId = null;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
@ -608,14 +581,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
{
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SendDeliveryReceiptJob(Address.fromSerialized(content.getSender()), message.getTimestamp()));
}
@SuppressLint("DefaultLocale")
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message)

View File

@ -1,298 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SendMessageResult;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Quote;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class PushGroupSendJob extends PushSendJob implements InjectableType {
public static final String KEY = "PushGroupSendJob";
private static final String TAG = PushGroupSendJob.class.getSimpleName();
@Inject SignalServiceMessageSender messageSender;
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_FILTER_ADDRESS = "filter_address";
private long messageId;
private String filterAddress;
public PushGroupSendJob(long messageId, @NonNull Address destination, @Nullable Address filterAddress) {
this(new Job.Parameters.Builder()
.setQueue(destination.toGroupString())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
messageId, filterAddress);
}
private PushGroupSendJob(@NonNull Job.Parameters parameters, long messageId, @Nullable Address filterAddress) {
super(parameters);
this.messageId = messageId;
this.filterAddress = filterAddress == null ? null :filterAddress.toString();
}
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, @Nullable Address filterAddress) {
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
List<Attachment> attachments = new LinkedList<>();
attachments.addAll(message.getAttachments());
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
if (attachmentJobs.isEmpty()) {
jobManager.add(new PushGroupSendJob(messageId, destination, filterAddress));
} else {
jobManager.startChain(attachmentJobs)
.then(new PushGroupSendJob(messageId, destination, filterAddress))
.enqueue();
}
} catch (NoSuchMessageException | MmsException e) {
Log.w(TAG, "Failed to enqueue message.", e);
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_FILTER_ADDRESS, filterAddress)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onAdded() {
DatabaseFactory.getMmsDatabase(context).markAsSending(messageId);
}
@Override
public void onPushSend()
throws MmsException, NoSuchMessageException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
if (database.isSent(messageId)) {
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
return;
}
try {
log(TAG, "Sending message: " + messageId);
List<Address> targets;
if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress));
else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
else targets = ClosedGroupsProtocolV2.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString());
List<SendMessageResult> results = deliver(message, targets);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
Set<Address> successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet());
List<NetworkFailure> resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successAddresses.contains(failure.getAddress())).toList();
List<IdentityKeyMismatch> resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successAddresses.contains(failure.getAddress())).toList();
List<SendMessageResult> successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList();
for (NetworkFailure resolvedFailure : resolvedNetworkFailures) {
database.removeFailure(messageId, resolvedFailure);
existingNetworkFailures.remove(resolvedFailure);
}
for (IdentityKeyMismatch resolvedIdentity : resolvedIdentityFailures) {
database.removeMismatchedIdentity(messageId, resolvedIdentity.getAddress(), resolvedIdentity.getIdentityKey());
existingIdentityMismatches.remove(resolvedIdentity);
}
if (!networkFailures.isEmpty()) {
database.addFailures(messageId, networkFailures);
}
for (IdentityKeyMismatch mismatch : identityMismatches) {
database.addMismatchedIdentity(messageId, mismatch.getAddress(), mismatch.getIdentityKey());
}
for (SendMessageResult success : successes) {
DatabaseFactory.getGroupReceiptDatabase(context).setUnidentified(Address.fromSerialized(success.getAddress().getNumber()),
messageId,
success.getSuccess().isUnidentified());
}
if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) {
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);
ApplicationContext.getInstance(context)
.getExpiringMessageManager()
.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} else if (!networkFailures.isEmpty()) {
throw new RetryLaterException();
} else if (!identityMismatches.isEmpty()) {
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
} catch (Exception e) {
warn(TAG, e);
database.markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
if (exception instanceof IOException) return true;
// Loki - Disable since we have our own retrying
return false;
}
@Override
public void onCanceled() {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
}
private List<SendMessageResult> deliver(OutgoingMediaMessage message, @NonNull List<Address> destinations)
throws IOException {
Address address = message.getRecipient().getAddress();
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
List<Optional<UnidentifiedAccess>> unidentifiedAccess = Stream.of(addresses)
.map(a -> Address.fromSerialized(a.getNumber()))
.map(a -> Recipient.from(context, a, false))
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
.toList();
if (message.isGroup() && address.isClosedGroup()) {
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
String groupId = address.toGroupString();
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
// Loki - Only send GroupUpdate or GroupQuit messages to closed groups
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
GroupContext groupContext = groupMessage.getGroupContext();
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList());
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.withExpiration(message.getRecipient().getExpireMessages())
.asGroupMessage(group)
.build();
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
} else {
SignalServiceDataMessage groupMessage = getDataMessage(address, message).build();
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage);
}
}
public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) {
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
String groupId = address.toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<Quote> quote = getQuoteFor(message);
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
return SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.asGroupMessage(group)
.withAttachments(attachmentPointers)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews);
}
public static class Factory implements Job.Factory<PushGroupSendJob> {
@Override
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
String address = data.getString(KEY_FILTER_ADDRESS);
Address filter = address != null ? Address.fromSerialized(data.getString(KEY_FILTER_ADDRESS)) : null;
return new PushGroupSendJob(parameters, data.getLong(KEY_MESSAGE_ID), filter);
}
}
}

View File

@ -1,308 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SendMessageResult;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.loki.api.SnodeAPI;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
public class PushMediaSendJob extends PushSendJob implements InjectableType {
public static final String KEY = "PushMediaSendJob";
private static final String TAG = PushMediaSendJob.class.getSimpleName();
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
@Inject SignalServiceMessageSender messageSender;
private long messageId;
private long templateMessageId;
private Address destination;
public PushMediaSendJob(long templateMessageId, long messageId, Address destination) {
this(constructParameters(destination), templateMessageId, messageId, destination);
}
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
}
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, messageId, messageId, destination);
}
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination)));
}
@WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, List<PushMediaSendJob> jobs) {
if (jobs.size() == 0) { return; }
PushMediaSendJob first = jobs.get(0);
long messageId = first.templateMessageId;
try {
List<AttachmentUploadJob> attachmentJobs = getAttachmentUploadJobs(context, messageId, first.destination);
if (attachmentJobs.isEmpty()) {
for (PushMediaSendJob job : jobs) { jobManager.add(job); }
} else {
jobManager.startChain(attachmentJobs)
.then((List<Job>)(List)jobs)
.enqueue();
}
} catch (NoSuchMessageException | MmsException e) {
Log.w(TAG, "Failed to enqueue message.", e);
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
public static List<AttachmentUploadJob> getAttachmentUploadJobs(@NonNull Context context, long messageId, @NonNull Address destination)
throws NoSuchMessageException, MmsException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
List<Attachment> attachments = new LinkedList<>();
attachments.addAll(message.getAttachments());
attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
return Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onAdded() {
DatabaseFactory.getMmsDatabase(context).markAsSending(messageId);
}
@Override
public void onPushSend()
throws RetryLaterException, MmsException, NoSuchMessageException,
UndeliverableMessageException
{
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(templateMessageId);
if (messageId >= 0 && database.isSent(messageId)) {
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
return;
}
try {
log(TAG, "Sending message: " + messageId);
Recipient recipient = Recipient.from(context, destination, false);
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(message);
if (messageId >= 0) {
database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments());
database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED);
} else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) {
log(TAG, "Marking recipient as UD-enabled following a UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED);
} else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) {
log(TAG, "Marking recipient as UD-disabled following a non-UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED);
}
if (messageId > 0 && message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
log(TAG, "Sent message: " + messageId);
} catch (SnodeAPI.Error e) {
Log.d("Loki", "Couldn't send message due to error: " + e.getDescription());
if (messageId >= 0) {
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setErrorMessage(messageId, e.getDescription());
database.markAsSentFailed(messageId);
}
}
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
// Loki - Disable since we have our own retrying
return false;
}
@Override
public void onCanceled() {
if (messageId >= 0) {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
private boolean deliver(OutgoingMediaMessage message)
throws RetryLaterException, UndeliverableMessageException, SnodeAPI.Error
{
try {
Recipient recipient = Recipient.from(context, destination, false);
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
List<Attachment> attachments = Stream.of(message.getAttachments()).toList();
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<SignalServiceDataMessage.Quote> quote = getQuoteFor(message);
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
Optional<UnidentifiedAccess> unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient);
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate())
.build();
SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withSyncTarget(destination.serialize())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate())
.build();
if (userPublicKey == address.getNumber()) {
// Loki - Device link messages don't go through here
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, true);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
return result.getSuccess().isUnidentified();
}
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage, false);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
boolean isUnidentified = result.getSuccess().isUnidentified();
try {
// send to ourselves to sync multi-device
Optional<UnidentifiedAccess> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage, true);
if (selfSendResult.getLokiAPIError() != null) {
throw selfSendResult.getLokiAPIError();
}
} catch (Exception e) {
Log.e("Loki", "Error sending message to ourselves", e);
}
return isUnidentified;
}
}
} catch (FileNotFoundException e) {
warn(TAG, e);
throw new UndeliverableMessageException(e);
} catch (IOException e) {
warn(TAG, e);
throw new RetryLaterException(e);
}
}
public static final class Factory implements Job.Factory<PushMediaSendJob> {
@Override
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination);
}
}
}

View File

@ -1,230 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.utilities.MediaTypes;
import org.session.libsignal.utilities.Base64;
import org.session.libsession.utilities.Util;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.session.libsession.utilities.preferences.ProfileKeyUtil;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.session.libsignal.utilities.Hex;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
import org.session.libsignal.service.api.messages.shared.SharedContact;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public abstract class PushSendJob extends SendJob {
private static final String TAG = PushSendJob.class.getSimpleName();
protected PushSendJob(Job.Parameters parameters) {
super(parameters);
}
protected static Job.Parameters constructParameters(Address destination) {
return new Parameters.Builder()
.setQueue(destination.serialize())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1)
.build();
}
@Override
protected final void onSend() throws Exception {
onPushSend();
}
@Override
public void onRetry() {
super.onRetry();
Log.i(TAG, "onRetry()");
}
protected Optional<byte[]> getProfileKey(@NonNull Recipient recipient) {
if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) {
return Optional.absent();
}
return Optional.of(ProfileKeyUtil.getProfileKey(context));
}
protected SignalServiceAddress getPushAddress(Address address) {
String relay = null;
return new SignalServiceAddress(address.toString(), Optional.fromNullable(relay));
}
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
return SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withVoiceNote(attachment.isVoiceNote())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
return null;
}
protected @NonNull List<SignalServiceAttachment> getAttachmentPointersFor(List<Attachment> attachments) {
return Stream.of(attachments).map(this::getAttachmentPointerFor).filter(a -> a != null).toList();
}
protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) {
if (TextUtils.isEmpty(attachment.getLocation())) {
Log.w(TAG, "empty content id");
return null;
}
if (TextUtils.isEmpty(attachment.getKey())) {
Log.w(TAG, "empty encrypted key");
return null;
}
try {
long id = Long.parseLong(attachment.getLocation());
byte[] key = Base64.decode(attachment.getKey());
return new SignalServiceAttachmentPointer(id,
attachment.getContentType(),
key,
Optional.of(Util.toIntExact(attachment.getSize())),
Optional.absent(),
attachment.getWidth(),
attachment.getHeight(),
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
Optional.fromNullable(attachment.getCaption()), attachment.getUrl());
} catch (IOException | ArithmeticException e) {
Log.w(TAG, e);
return null;
}
}
protected static void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadId != -1 && recipient != null) {
ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
}
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMediaMessage message) {
if (message.getOutgoingQuote() == null) return Optional.absent();
long quoteId = message.getOutgoingQuote().getId();
String quoteBody = message.getOutgoingQuote().getText();
Address quoteAuthor = message.getOutgoingQuote().getAuthor();
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
BitmapUtil.ScaleResult thumbnailData = null;
SignalServiceAttachment thumbnail = null;
String thumbnailType = MediaTypes.IMAGE_JPEG;
try {
if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
Bitmap.CompressFormat format = BitmapUtil.getCompressFormatForContentType(attachment.getContentType());
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024, format);
thumbnailType = attachment.getContentType();
} else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
}
if (thumbnailData != null) {
thumbnail = SignalServiceAttachment.newStreamBuilder()
.withContentType(thumbnailType)
.withWidth(thumbnailData.getWidth())
.withHeight(thumbnailData.getHeight())
.withLength(thumbnailData.getBitmap().length)
.withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
.build();
}
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
attachment.getFileName(),
thumbnail));
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}
}
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments));
}
List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
List<SharedContact> sharedContacts = new LinkedList<>();
for (Contact contact : mediaMessage.getSharedContacts()) {
SharedContact.Builder builder = ContactModelMapper.localToRemoteBuilder(contact);
SharedContact.Avatar avatar = null;
if (contact.getAvatar() != null && contact.getAvatar().getAttachment() != null) {
avatar = SharedContact.Avatar.newBuilder().withAttachment(getAttachmentFor(contact.getAvatarAttachment()))
.withProfileFlag(contact.getAvatar().isProfile())
.build();
}
builder.setAvatar(avatar);
sharedContacts.add(builder.build());
}
return sharedContacts;
}
List<Preview> getPreviewsFor(OutgoingMediaMessage mediaMessage) {
return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> {
SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null;
return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment));
}).toList();
}
protected abstract void onPushSend() throws Exception;
}

View File

@ -1,241 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.recipients.Recipient.UnidentifiedAccessMode;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SendMessageResult;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.loki.api.SnodeAPI;
import java.io.IOException;
import javax.inject.Inject;
public class PushTextSendJob extends PushSendJob implements InjectableType {
public static final String KEY = "PushTextSendJob";
private static final String TAG = PushTextSendJob.class.getSimpleName();
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
@Inject SignalServiceMessageSender messageSender;
private long messageId;
private long templateMessageId;
private Address destination;
public PushTextSendJob(long messageId, Address destination) {
this(messageId, messageId, destination);
}
public PushTextSendJob(long templateMessageId, long messageId, Address destination) {
this(constructParameters(destination), templateMessageId, messageId, destination);
}
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onAdded() {
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
}
}
@Override
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
SmsMessageRecord record = database.getMessage(templateMessageId);
Recipient recordRecipient = record.getRecipient().resolve();
boolean hasSameDestination = destination.equals(recordRecipient.getAddress());
if (hasSameDestination && !record.isPending() && !record.isFailed()) {
Log.d("Loki", "Message with ID: " + templateMessageId + " was already sent; ignoring.");
return;
}
try {
log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device."));
Recipient recipient = Recipient.from(context, destination, false);
byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(record);
if (messageId >= 0) {
database.markAsSent(messageId, true);
database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
}
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {
log(TAG, "Marking recipient as UD-unrestricted following a UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.UNRESTRICTED);
} else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) {
log(TAG, "Marking recipient as UD-enabled following a UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.ENABLED);
} else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) {
log(TAG, "Marking recipient as UD-disabled following a non-UD send.");
DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient, UnidentifiedAccessMode.DISABLED);
}
if (record.getExpiresIn() > 0 && messageId >= 0) {
database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
}
log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to a linked device."));
} catch (SnodeAPI.Error e) {
Log.d("Loki", "Couldn't send message due to error: " + e.getDescription());
if (messageId >= 0) {
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setErrorMessage(record.getId(), e.getDescription());
database.markAsSentFailed(record.getId());
}
}
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
// Loki - Disable since we have our own retrying
return false;
}
@Override
public void onCanceled() {
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadId != -1 && recipient != null) {
ApplicationContext.getInstance(context).messageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
}
}
private boolean deliver(SmsMessageRecord message) throws RetryLaterException, SnodeAPI.Error
{
try {
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
Optional<byte[]> profileKey = getProfileKey(recipient);
Optional<UnidentifiedAccess> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.build();
SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withSyncTarget(destination.serialize())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.build();
if (userPublicKey.equals(address.getNumber())) {
// Loki - Device link messages don't go through here
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, true);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
return result.getSuccess().isUnidentified();
}
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, false);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
boolean isUnidentified = result.getSuccess().isUnidentified();
try {
// send to ourselves to sync multi-device
Optional<UnidentifiedAccess> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage, true);
if (selfSendResult.getLokiAPIError() != null) {
throw selfSendResult.getLokiAPIError();
}
} catch (Exception e) {
Log.e("Loki", "Error sending message to ourselves", e);
}
return isUnidentified;
}
}
} catch (IOException e) {
warn(TAG, "Failure", e);
throw new RetryLaterException(e);
}
}
public static class Factory implements Job.Factory<PushTextSendJob> {
@Override
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
return new PushTextSendJob(parameters, templateMessageID, messageID, destination);
}
}
}

View File

@ -1,107 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.GroupUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.SignalServiceGroup.Type;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class RequestGroupInfoJob extends BaseJob implements InjectableType {
public static final String KEY = "RequestGroupInfoJob";
@SuppressWarnings("unused")
private static final String TAG = RequestGroupInfoJob.class.getSimpleName();
private static final String KEY_SOURCE = "source";
private static final String KEY_GROUP_ID = "group_id";
@Inject SignalServiceMessageSender messageSender;
private String source;
private byte[] groupId;
public RequestGroupInfoJob(@NonNull String source, @NonNull byte[] groupId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
source,
groupId);
}
private RequestGroupInfoJob(@NonNull Job.Parameters parameters, @NonNull String source, @NonNull byte[] groupId) {
super(parameters);
this.source = source;
this.groupId = groupId;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putString(KEY_SOURCE, source)
.putString(KEY_GROUP_ID, GroupUtil.getEncodedClosedGroupID(groupId))
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException {
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
.build();
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
.asGroupMessage(group)
.withTimestamp(System.currentTimeMillis())
.build();
messageSender.sendMessage(0, new SignalServiceAddress(source),
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromExternal(context, source), false)),
message, false);
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public void onCanceled() {
}
public static final class Factory implements Job.Factory<RequestGroupInfoJob> {
@Override
public @NonNull RequestGroupInfoJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RequestGroupInfoJob(parameters,
data.getString(KEY_SOURCE),
GroupUtil.getDecodedGroupIDAsData(data.getString(KEY_GROUP_ID)));
}
}
}

View File

@ -1,112 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class SendDeliveryReceiptJob extends BaseJob implements InjectableType {
public static final String KEY = "SendDeliveryReceiptJob";
private static final String KEY_ADDRESS = "address";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_TIMESTAMP = "timestamp";
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
@Inject
transient SignalServiceMessageSender messageSender;
private String address;
private long messageId;
private long timestamp;
public SendDeliveryReceiptJob(@NonNull Address address, long messageId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
address,
messageId,
System.currentTimeMillis());
}
private SendDeliveryReceiptJob(@NonNull Job.Parameters parameters,
@NonNull Address address,
long messageId,
long timestamp)
{
super(parameters);
this.address = address.serialize();
this.messageId = messageId;
this.timestamp = timestamp;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putString(KEY_ADDRESS, address)
.putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_TIMESTAMP, timestamp)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException {
Log.d("Loki", "Sending delivery receipt.");
SignalServiceAddress remoteAddress = new SignalServiceAddress(address);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY,
Collections.singletonList(messageId),
timestamp);
messageSender.sendReceipt(remoteAddress,
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)),
receiptMessage);
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
if (e instanceof PushNetworkException) return true;
return false;
}
@Override
public void onCanceled() {
Log.w(TAG, "Failed to send delivery receipt to: " + address);
}
public static final class Factory implements Job.Factory<SendDeliveryReceiptJob> {
@Override
public @NonNull SendDeliveryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new SendDeliveryReceiptJob(parameters,
Address.fromSerialized(data.getString(KEY_ADDRESS)),
data.getLong(KEY_MESSAGE_ID),
data.getLong(KEY_TIMESTAMP));
}
}
}

View File

@ -1,63 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobLogger;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public abstract class SendJob extends BaseJob {
@SuppressWarnings("unused")
private final static String TAG = SendJob.class.getSimpleName();
public SendJob(Job.Parameters parameters) {
super(parameters);
}
@Override
public final void onRun() throws Exception {
Log.i(TAG, "Starting message send attempt");
onSend();
Log.i(TAG, "Message send completed");
}
protected abstract void onSend() throws Exception;
protected void markAttachmentsUploaded(long messageId, @NonNull List<Attachment> attachments) {
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
for (Attachment attachment : attachments) {
database.markAttachmentUploaded(messageId, attachment);
}
}
protected void log(@NonNull String tag, @NonNull String message) {
Log.i(tag, JobLogger.format(this, message));
}
protected void warn(@NonNull String tag, @NonNull String message) {
warn(tag, message, null);
}
protected void warn(@NonNull String tag, @Nullable Throwable t) {
warn(tag, "", t);
}
protected void warn(@NonNull String tag, @NonNull String message, @Nullable Throwable t) {
Log.w(tag, JobLogger.format(this, message), t);
}
}

View File

@ -1,123 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class SendReadReceiptJob extends BaseJob implements InjectableType {
public static final String KEY = "SendReadReceiptJob";
private static final String TAG = SendReadReceiptJob.class.getSimpleName();
private static final String KEY_ADDRESS = "address";
private static final String KEY_MESSAGE_IDS = "message_ids";
private static final String KEY_TIMESTAMP = "timestamp";
@Inject SignalServiceMessageSender messageSender;
private String address;
private List<Long> messageIds;
private long timestamp;
public SendReadReceiptJob(Address address, List<Long> messageIds) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
address,
messageIds,
System.currentTimeMillis());
}
private SendReadReceiptJob(@NonNull Job.Parameters parameters,
@NonNull Address address,
@NonNull List<Long> messageIds,
long timestamp)
{
super(parameters);
this.address = address.serialize();
this.messageIds = messageIds;
this.timestamp = timestamp;
}
@Override
public @NonNull
Data serialize() {
long[] ids = new long[messageIds.size()];
for (int i = 0; i < ids.length; i++) {
ids[i] = messageIds.get(i);
}
return new Data.Builder().putString(KEY_ADDRESS, address)
.putLongArray(KEY_MESSAGE_IDS, ids)
.putLong(KEY_TIMESTAMP, timestamp)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException {
if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return;
SignalServiceAddress remoteAddress = new SignalServiceAddress(address);
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp);
messageSender.sendReceipt(remoteAddress,
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)),
receiptMessage);
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
if (e instanceof PushNetworkException) return true;
return false;
}
@Override
public void onCanceled() {
Log.w(TAG, "Failed to send read receipts to: " + address);
}
public static final class Factory implements Job.Factory<SendReadReceiptJob> {
@Override
public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) {
Address address = Address.fromSerialized(data.getString(KEY_ADDRESS));
long timestamp = data.getLong(KEY_TIMESTAMP);
long[] ids = data.hasLongArray(KEY_MESSAGE_IDS) ? data.getLongArray(KEY_MESSAGE_IDS) : new long[0];
List<Long> messageIds = new ArrayList<>(ids.length);
for (long id : ids) {
messageIds.add(id);
}
return new SendReadReceiptJob(parameters, address, messageIds, timestamp);
}
}
}

View File

@ -1,116 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.jobs.Data;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsignal.service.api.SignalServiceMessageSender;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage.Action;
import org.session.libsignal.service.api.push.SignalServiceAddress;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.utilities.TextSecurePreferences;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
public class TypingSendJob extends BaseJob implements InjectableType {
public static final String KEY = "TypingSendJob";
private static final String TAG = TypingSendJob.class.getSimpleName();
private static final String KEY_THREAD_ID = "thread_id";
private static final String KEY_TYPING = "typing";
private long threadId;
private boolean typing;
@Inject SignalServiceMessageSender messageSender;
public TypingSendJob(long threadId, boolean typing) {
this(new Job.Parameters.Builder()
.setQueue("TYPING_" + threadId)
.setMaxAttempts(1)
.setLifespan(TimeUnit.SECONDS.toMillis(5))
.build(),
threadId,
typing);
}
private TypingSendJob(@NonNull Job.Parameters parameters, long threadId, boolean typing) {
super(parameters);
this.threadId = threadId;
this.typing = typing;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder().putLong(KEY_THREAD_ID, threadId)
.putBoolean(KEY_TYPING, typing)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws Exception {
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
return;
}
Log.d(TAG, "Sending typing " + (typing ? "started" : "stopped") + " for thread " + threadId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (recipient == null) {
throw new IllegalStateException("Tried to send a typing indicator to a non-existent thread.");
}
List<Recipient> recipients = Collections.singletonList(recipient);
if (recipient.isGroupRecipient()) {
recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.getAddress().toGroupString(), false);
}
List<SignalServiceAddress> addresses = Stream.of(recipients).map(r -> new SignalServiceAddress(r.getAddress().serialize())).toList();
List<Optional<UnidentifiedAccess>> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList();
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis());
messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage);
}
@Override
public void onCanceled() {
}
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
return false;
}
public static final class Factory implements Job.Factory<TypingSendJob> {
@Override
public @NonNull TypingSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new TypingSendJob(parameters, data.getLong(KEY_THREAD_ID), data.getBoolean(KEY_TYPING));
}
}
}

View File

@ -9,9 +9,9 @@ import androidx.annotation.Nullable;
import com.google.android.gms.common.util.IOUtils;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.utilities.MediaTypes;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.net.CallRequestController;
@ -192,7 +192,7 @@ public class LinkPreviewRepository implements InjectableType {
return Optional.of(new UriAttachment(uri,
uri,
contentType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
bytes.length,
bitmap.getWidth(),
bitmap.getHeight(),

View File

@ -43,13 +43,13 @@ public class LinkPreviewViewModel extends ViewModel {
return linkPreviewState.getValue() != null && linkPreviewState.getValue().getLinkPreview().isPresent();
}
public @NonNull List<LinkPreview> getActiveLinkPreviews() {
public Optional<LinkPreview> getActiveLinkPreview() {
final LinkPreviewState state = linkPreviewState.getValue();
if (state == null || !state.getLinkPreview().isPresent()) {
return Collections.emptyList();
return Optional.absent();
} else {
return Collections.singletonList(state.getLinkPreview().get());
return state.getLinkPreview();
}
}

View File

@ -13,18 +13,18 @@ import android.widget.Toast
import kotlinx.android.synthetic.main.activity_create_closed_group.*
import network.loki.messenger.R
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
//TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
@ -104,13 +104,13 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
if (selectedMembers.count() < 1) {
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
if (selectedMembers.count() >= ClosedGroupsProtocolV2.groupSizeLimit) { // Minus one because we're going to include self later
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
isLoading = true
loaderContainer.fadeIn()
ClosedGroupsProtocolV2.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
loaderContainer.fadeOut()
isLoading = false
val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
@ -128,7 +128,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
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, ThreadDatabase.DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
context.startActivity(intent)
}

View File

@ -18,6 +18,7 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
@ -69,7 +70,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent)
finish()
}

View File

@ -19,12 +19,12 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
@ -260,7 +260,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
val maxGroupMembers = if (isClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
@ -277,17 +277,17 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
isLoading = true
loaderContainer.fadeIn()
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!)
MessageSender.explicitLeave(groupPublicKey!!)
} else {
task {
if (hasNameChanged) {
ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity, groupPublicKey!!, name)
MessageSender.explicitNameChange(groupPublicKey!!, name)
}
members.filterNot { it in originalMembers }.let { adds ->
if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
if (adds.isNotEmpty()) MessageSender.explicitAddMembers(groupPublicKey!!, adds.map { it.address.serialize() })
}
originalMembers.filterNot { it in members }.let { removes ->
if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
if (removes.isNotEmpty()) MessageSender.explicitRemoveMembers(groupPublicKey!!, removes.map { it.address.serialize() })
}
}
}
@ -296,13 +296,11 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
isLoading = false
finish()
}.failUi { exception ->
val message = if (exception is ClosedGroupsProtocolV2.Error) exception.description else "An error occurred"
val message = if (exception is MessageSender.Error) exception.description else "An error occurred"
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
loaderContainer.fadeOut()
isLoading = false
}
} else {
GroupManager.updateGroup(this, groupID, members, null, name, admins)
}
}
}

View File

@ -26,13 +26,11 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.ProfilePictureModifiedEvent
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Util
import org.session.libsession.utilities.*
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.ThreadUtils
@ -42,7 +40,6 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.loki.dialogs.*
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.ConversationView
@ -344,7 +341,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
isClosedGroup = false
}
if (isClosedGroup) {
ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!)
MessageSender.explicitLeave(groupPublicKey!!)
} else {
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
return@launch

View File

@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.loki.activities
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.views.FakeChatView

View File

@ -35,7 +35,7 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.session.libsession.utilities.KeyPairUtilities
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo

View File

@ -17,6 +17,7 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.DistributionTypes
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
@ -60,7 +61,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperF
intent.setDataAndType(getIntent().data, getIntent().type)
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, DistributionTypes.DEFAULT)
startActivity(intent)
finish()
}

View File

@ -19,7 +19,7 @@ import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Hex
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.session.libsession.utilities.KeyPairUtilities
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo

View File

@ -23,7 +23,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.libsignal.util.KeyHelper
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.session.libsession.utilities.KeyPairUtilities
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
import java.util.*

View File

@ -12,7 +12,7 @@ import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.session.libsession.utilities.TextSecurePreferences

View File

@ -7,12 +7,11 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.PushDecryptJob
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.guava.Optional

View File

@ -14,36 +14,12 @@ import org.session.libsignal.service.loki.api.crypto.SessionProtocol
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.session.libsession.utilities.KeyPairUtilities
class SessionProtocolImpl(private val context: Context) : SessionProtocol {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't sign message due to error: $exception.")
throw SessionProtocol.Exception.SigningFailed
}
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
try {
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
throw SessionProtocol.Exception.EncryptionFailed
}
return ciphertext
}
override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair<ByteArray, String> {
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())

View File

@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.session.libsignal.utilities.Hex
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.loki.utilities.PublicKeyValidation

View File

@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.loki.utilities.*
class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
private val sessionJobTable = "loki_thread_session_reset_database"
private val sessionJobTable = "session_job_database"
val jobID = "job_id"
val jobType = "job_type"
val failureCount = "failure_count"
@ -22,12 +22,12 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
fun persistJob(job: Job) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
val contentValues = ContentValues(4)
contentValues.put(jobID, job.id)
contentValues.put(jobType, job.getFactoryKey())
contentValues.put(failureCount, job.failureCount)
contentValues.put(serializedData, SessionJobHelper.dataSerializer.serialize(job.serialize()))
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID.toString()))
database.insertOrUpdate(sessionJobTable, contentValues, "$jobID = ?", arrayOf(jobID))
}
fun markJobAsSucceeded(job: Job) {
@ -51,7 +51,7 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
database.getAll(sessionJobTable, "$jobType = ?", arrayOf(AttachmentUploadJob.KEY)) { cursor ->
result.add(jobFromCursor(cursor) as AttachmentUploadJob)
}
return result.first { job -> job.attachmentID == attachmentID }
return result.firstOrNull { job -> job.attachmentID == attachmentID }
}
fun getMessageSendJob(messageSendJobID: String): MessageSendJob? {

View File

@ -11,7 +11,7 @@ import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
import org.session.libsession.utilities.KeyPairUtilities
class ClearAllDataDialog : DialogFragment() {

View File

@ -13,7 +13,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import kotlinx.android.synthetic.main.dialog_seed.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
import org.session.libsignal.service.loki.crypto.MnemonicCodec
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey

View File

@ -1,253 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import com.google.protobuf.ByteString
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.session.libsession.messaging.jobs.Data
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.TTLUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Hex
import java.util.concurrent.TimeUnit
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters,
private val destination: String,
private val kind: Kind,
private val sentTime: Long) : BaseJob(parameters) {
sealed class Kind {
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
class Update(val name: String, val members: Collection<ByteArray>) : Kind()
object Leave : Kind()
class RemoveMembers(val members: Collection<ByteArray>) : Kind()
class AddMembers(val members: Collection<ByteArray>) : Kind()
class NameChange(val name: String) : Kind()
class EncryptionKeyPair(val wrappers: Collection<KeyPairWrapper>, val targetUser: String?) : Kind() // The new encryption key pair encrypted for each member individually
}
companion object {
const val KEY = "ClosedGroupUpdateMessageSendJobV2"
}
@Serializable
data class KeyPairWrapper(val publicKey: String, val encryptedKeyPair: ByteArray) {
companion object {
fun fromProto(proto: DataMessage.ClosedGroupControlMessage.KeyPairWrapper): KeyPairWrapper {
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair.toByteArray())
}
}
fun toProto(): DataMessage.ClosedGroupControlMessage.KeyPairWrapper {
val result = DataMessage.ClosedGroupControlMessage.KeyPairWrapper.newBuilder()
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.encryptedKeyPair = ByteString.copyFrom(encryptedKeyPair)
return result.build()
}
}
constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(20)
.build(),
destination,
kind,
sentTime)
override fun getFactoryKey(): String { return KEY }
override fun serialize(): Data {
val builder = Data.Builder()
builder.putString("destination", destination)
builder.putLong("sentTime", sentTime)
when (kind) {
is Kind.New -> {
builder.putString("kind", "New")
builder.putByteArray("publicKey", kind.publicKey)
builder.putString("name", kind.name)
builder.putByteArray("encryptionKeyPairPublicKey", kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
builder.putByteArray("encryptionKeyPairPrivateKey", kind.encryptionKeyPair.privateKey.serialize())
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
builder.putString("admins", admins)
}
is Kind.Update -> {
builder.putString("kind", "Update")
builder.putString("name", kind.name)
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
is Kind.RemoveMembers -> {
builder.putString("kind", "RemoveMembers")
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
Kind.Leave -> {
builder.putString("kind", "Leave")
}
is Kind.AddMembers -> {
builder.putString("kind", "AddMembers")
val members = kind.members.joinToString(" - ") { it.toHexString() }
builder.putString("members", members)
}
is Kind.NameChange -> {
builder.putString("kind", "NameChange")
builder.putString("name", kind.name)
}
is Kind.EncryptionKeyPair -> {
builder.putString("kind", "EncryptionKeyPair")
val wrappers = kind.wrappers.joinToString(" - ") { Json.encodeToString(it) }
builder.putString("wrappers", wrappers)
builder.putString("targetUser", kind.targetUser)
}
}
return builder.build()
}
class Factory : Job.Factory<ClosedGroupUpdateMessageSendJobV2> {
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
val destination = data.getString("destination")
val rawKind = data.getString("kind")
val sentTime = data.getLong("sentTime")
val kind: Kind
when (rawKind) {
"New" -> {
val publicKey = data.getByteArray("publicKey")
val name = data.getString("name")
val encryptionKeyPairPublicKey = data.getByteArray("encryptionKeyPairPublicKey")
val encryptionKeyPairPrivateKey = data.getByteArray("encryptionKeyPairPrivateKey")
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairPublicKey), DjbECPrivateKey(encryptionKeyPairPrivateKey))
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.New(publicKey, name, encryptionKeyPair, members, admins)
}
"Update" -> {
val name = data.getString("name")
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.Update(name, members)
}
"EncryptionKeyPair" -> {
val wrappers: Collection<KeyPairWrapper> = data.getString("wrappers").split(" - ").map { Json.decodeFromString(it) }
val targetUser = data.getString("targetUser")
kind = Kind.EncryptionKeyPair(wrappers, targetUser)
}
"RemoveMembers" -> {
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.RemoveMembers(members)
}
"AddMembers" -> {
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.AddMembers(members)
}
"NameChange" -> {
val name = data.getString("name")
kind = Kind.NameChange(name)
}
"Leave" -> {
kind = Kind.Leave
}
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
}
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
}
}
public override fun onRun() {
val sendDestination = if (kind is Kind.EncryptionKeyPair && kind.targetUser != null) {
kind.targetUser
} else {
destination
}
val contentMessage = SignalServiceProtos.Content.newBuilder()
val dataMessage = DataMessage.newBuilder()
val closedGroupUpdate = DataMessage.ClosedGroupControlMessage.newBuilder()
when (kind) {
is Kind.New -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupUpdate.publicKey = ByteString.copyFrom(kind.publicKey)
closedGroupUpdate.name = kind.name
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
}
is Kind.Update -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.UPDATE
closedGroupUpdate.name = kind.name
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.EncryptionKeyPair -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR
closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() })
if (kind.targetUser != null) {
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(destination))
}
}
Kind.Leave -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT
}
is Kind.RemoveMembers -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.AddMembers -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
}
is Kind.NameChange -> {
closedGroupUpdate.type = DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE
closedGroupUpdate.name = kind.name
}
}
dataMessage.closedGroupControlMessage = closedGroupUpdate.build()
contentMessage.dataMessage = dataMessage.build()
val serializedContentMessage = contentMessage.build().toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(sendDestination)
val recipient = Recipient.from(context, Address.fromSerialized(sendDestination), false)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
val ttl = when (kind) {
is Kind.EncryptionKeyPair -> 4 * 24 * 60 * 60 * 1000
else -> TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
}
try {
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
messageSender.sendMessage(0, address, udAccess,
sentTime, serializedContentMessage, false, ttl,
true, false, false, Optional.absent())
} catch (e: Exception) {
Log.d("Loki", "Failed to send closed group update message to: $sendDestination due to error: $e.")
}
}
public override fun onShouldRetry(e: Exception): Boolean {
return true
}
override fun onCanceled() { }
}

View File

@ -3,32 +3,25 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import android.util.Log
import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsignal.libsignal.ecc.Curve
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.session.libsignal.utilities.Hex
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair
import org.session.libsession.messaging.sending_receiving.pendingKeyPair
import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord
@ -37,258 +30,8 @@ import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import java.util.*
import java.util.concurrent.ConcurrentHashMap
object ClosedGroupsProtocolV2 {
const val groupSizeLimit = 100
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
sealed class Error(val description: String) : Exception() {
object NoThread : Error("Couldn't find a thread associated with the given group public key")
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
object InvalidUpdate : Error("Invalid group update.")
}
fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
ThreadUtils.queue {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val membersAsData = members.map { Hex.fromStringCondensed(it) }
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Generate the group's public key
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
val sentTime = System.currentTimeMillis()
// Generate the key pair that'll be used for encryption and decryption
val encryptionKeyPair = Curve.generateKeyPair()
// Create the group
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val admins = setOf( userPublicKey )
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }), System.currentTimeMillis())
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Send a closed group update message to all members individually
// Add the group to the user's set of public keys to poll for
apiDB.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
for (member in members) {
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately to make all of this sync
}
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
deferred.resolve(groupID)
}
// Return
return deferred.promise
}
/**
* @param notifyUser Inserts an outgoing info message for the user's leave message, useful to set `false` if
* you are exiting asynchronously and deleting the thread from [HomeActivity][org.thoughtcrime.securesms.loki.activities.HomeActivity.deleteConversation]
*/
@JvmStatic @JvmOverloads
fun explicitLeave(context: Context, groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
ThreadUtils.queue {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
val sentTime = System.currentTimeMillis()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
return@queue deferred.reject(Error.NoThread)
}
// Send the update to the group
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Notify the user
val infoType = GroupContext.Type.QUIT
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
if (notifyUser) {
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
// Remove the group private key and unsubscribe from PNs
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
deferred.resolve(Unit)
}
return deferred.promise
}
@JvmStatic
fun explicitAddMembers(context: Context, groupPublicKey: String, membersToAdd: List<String>) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
throw Error.NoThread
}
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
val membersAsData = updatedMembers.map { Hex.fromStringCondensed(it) }
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val sentTime = System.currentTimeMillis()
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() ?: Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)).orNull()
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
throw Error.NoKeyPair
}
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Save the new group members
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
// Send closed group update messages to any new members individually
for (member in membersToAdd) {
@Suppress("NAME_SHADOWING")
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
}
}
@JvmStatic
fun explicitRemoveMembers(context: Context, groupPublicKey: String, membersToRemove: List<String>) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
throw Error.NoThread
}
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
throw Error.NoKeyPair
}
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
throw Error.InvalidUpdate
}
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Save the new group members
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
}
}
@JvmStatic
fun explicitNameChange(context: Context, groupPublicKey: String, newName: String) {
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
val members = group.members.map { it.serialize() }.toSet()
val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
throw Error.NoThread
}
// Send the update to the group
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
// Update the group
groupDB.updateTitle(groupID, newName)
}
private fun generateAndSendNewEncryptionKeyPair(context: Context, groupPublicKey: String, targetMembers: Collection<String>) {
// Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Can't update nonexistent closed group.")
return
}
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
return
}
// Generate the new encryption key pair
val newKeyPair = Curve.generateKeyPair()
// replace call will not succeed if no value already set
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
do {
// make sure we set the pendingKeyPair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
// Store it * after * having sent out the message to the group
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
}
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true) {
val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
val plaintext = proto.build().toByteArray()
val wrappers = targetMembers.mapNotNull { publicKey ->
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
}
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers, targetUser), System.currentTimeMillis())
if (force) {
job.setContext(context)
job.onRun() // Run the job immediately
} else {
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
@JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
@ -361,11 +104,11 @@ object ClosedGroupsProtocolV2 {
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user (if we didn't make the group)
if (userPublicKey != senderPublicKey) {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
} else if (prevGroup == null) {
// only notify if we created this group
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
}
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
@ -410,7 +153,7 @@ object ClosedGroupsProtocolV2 {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, newMembers)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
}
val (contextType, signalType) =
@ -418,9 +161,9 @@ object ClosedGroupsProtocolV2 {
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, sentTimestamp)
}
}
@ -448,9 +191,9 @@ object ClosedGroupsProtocolV2 {
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
@ -460,7 +203,7 @@ object ClosedGroupsProtocolV2 {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
}
}
}
@ -487,9 +230,9 @@ object ClosedGroupsProtocolV2 {
// Notify the user
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
}
@ -523,15 +266,15 @@ object ClosedGroupsProtocolV2 {
val isCurrentUserAdmin = admins.contains(userPublicKey)
groupDB.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
// Notify user
if (userLeft) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, sentTimestamp)
}
}
@ -598,59 +341,6 @@ object ClosedGroupsProtocolV2 {
Log.d("Loki", "Received a new closed group encryption key pair")
}
private fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long) {
val groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
.setType(type0)
.setName(name)
.addAllMembers(members)
.addAllAdmins(admins)
val group = SignalServiceGroup(type1, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList())
val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true)
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
val smsDB = DatabaseFactory.getSmsDatabase(context)
smsDB.insertMessageInbox(infoMessage)
}
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long,
sentTimestamp: Long) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
val groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
.setType(type)
.setName(name)
.addAllMembers(members)
.addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context)
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTimestamp)
mmsDB.markAsSent(infoMessageID, true)
}
@JvmStatic
fun getMessageDestinations(context: Context, groupID: String): List<Address> {
return if (GroupUtil.isOpenGroup(groupID)) {
listOf(Address.fromSerialized(groupID))
} else {
var groupPublicKey: String? = null
try {
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString() // TODO: The toHexString() here might be unnecessary
} catch (exception: Exception) {
// Do nothing
}
if (groupPublicKey != null && DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey)) {
listOf(Address.fromSerialized(groupPublicKey))
} else {
DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address }
}
}
}
// region Deprecated
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: DataMessage.ClosedGroupControlMessage, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Prepare
@ -685,7 +375,7 @@ object ClosedGroupsProtocolV2 {
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
if (wasAnyUserRemoved && isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, members)
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
}
// Update the group
groupDB.updateTitle(groupID, name)
@ -700,9 +390,9 @@ object ClosedGroupsProtocolV2 {
val admins = group.admins.map { it.toString() }
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
DatabaseFactory.getStorage(context).insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
DatabaseFactory.getStorage(context).insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins, sentTimestamp)
}
}
// endregion

View File

@ -3,30 +3,26 @@ package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageSender
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.preferences.ProfileKeyUtil
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.loki.utilities.ContactUtilities
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import java.util.*
object MultiDeviceProtocol {
// TODO: refactor this to use new message sending job
@JvmStatic
fun syncConfigurationIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
@ -39,22 +35,10 @@ object MultiDeviceProtocol {
ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey)
}
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(userPublicKey)
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
try {
messageSender.sendMessage(0, address, udAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(),
true, false, false, Optional.absent())
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
} catch (e: Exception) {
Log.d("Loki", "Failed to send configuration message due to error: $e.")
}
MessageSender.send(configurationMessage, Address.fromSerialized(userPublicKey))
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
}
// TODO: refactor this to use new message sending job
fun forceSyncConfigurationNowIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val contacts = ContactUtilities.getAllContacts(context).filter { recipient ->
@ -63,19 +47,8 @@ object MultiDeviceProtocol {
ConfigurationMessage.Contact(recipient.address.serialize(), recipient.name!!, recipient.profileAvatar, recipient.profileKey)
}
val configurationMessage = ConfigurationMessage.getCurrent(contacts) ?: return
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(userPublicKey)
val recipient = Recipient.from(context, Address.fromSerialized(userPublicKey), false)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
try {
messageSender.sendMessage(0, address, udAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(),
true, false, false, Optional.absent())
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
} catch (e: Exception) {
Log.d("Loki", "Failed to send configuration message due to error: $e.")
}
MessageSender.send(configurationMessage, Destination.from(Address.fromSerialized(userPublicKey)))
TextSecurePreferences.setLastConfigurationSyncTime(context, System.currentTimeMillis())
}
// TODO: remove this after we migrate to new message receiving pipeline

View File

@ -20,6 +20,7 @@ import network.loki.messenger.R
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.thoughtcrime.securesms.ApplicationContext
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
@ -152,7 +153,7 @@ class MessageAudioView: FrameLayout, AudioSlidePlayer.Listener {
downloadProgress.progress = 0
}
}
(showControls && audio.transferState == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) -> {
(showControls && audio.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED) -> {
controlToggle.displayQuick(downloadProgress)
seekBar.isEnabled = false
downloadProgress.isIndeterminate = true

View File

@ -145,14 +145,14 @@ class ProfilePictureView : RelativeLayout {
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
if (publicKey.isNotEmpty()) {
if (imagesCached.contains(publicKey)) return
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (imagesCached.contains(recipient.profileAvatar.orEmpty())) return
val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.clear(imageView)
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).circleCrop().into(imageView)
imagesCached.add(publicKey)
imagesCached.add(recipient.profileAvatar.orEmpty())
} else {
val sizeInPX = resources.getDimensionPixelSize(sizeResId)
glide.clear(imageView)
@ -162,7 +162,7 @@ class ProfilePictureView : RelativeLayout {
publicKey,
displayName
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
imagesCached.add(publicKey)
imagesCached.add(recipient.profileAvatar.orEmpty())
}
} else {
imageView.setImageDrawable(null)

View File

@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
import network.loki.messenger.R;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
import org.session.libsession.utilities.MediaTypes;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -38,7 +39,7 @@ public class AudioSlide extends Slide {
}
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null));
super(context, new UriAttachment(uri, null, contentType, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null));
}
public AudioSlide(Context context, Attachment attachment) {

View File

@ -23,7 +23,7 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
@ -135,8 +135,8 @@ public abstract class Slide {
}
public boolean isPendingDownload() {
return getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED ||
getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
return getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED ||
getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING;
}
public int getTransferState() {
@ -172,7 +172,7 @@ public abstract class Slide {
return new UriAttachment(uri,
hasThumbnail ? uri : null,
resolvedType,
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
size,
width,
height,

View File

@ -25,18 +25,19 @@ import android.os.AsyncTask;
import android.os.Bundle;
import androidx.core.app.RemoteInput;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsignal.utilities.logging.Log;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
@ -72,17 +73,29 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
long replyThreadId;
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
if (threadId == -1) {
replyThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
} else {
replyThreadId = threadId;
}
VisibleMessage message = new VisibleMessage();
message.setText(responseText.toString());
message.setSentTimestamp(System.currentTimeMillis());
MessageSender.send(message, recipient.getAddress());
if (recipient.isGroupRecipient()) {
Log.w("AndroidAutoReplyReceiver", "GroupRecipient, Sending media message");
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
try {
DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, replyThreadId, false, null);
} catch (MmsException e) {
Log.w(TAG, e);
}
} else {
Log.w("AndroidAutoReplyReceiver", "Sending regular message ");
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(replyThreadId, reply, false, System.currentTimeMillis(), null);
}
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true);

View File

@ -11,13 +11,15 @@ import androidx.core.app.NotificationManagerCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.session.libsession.messaging.messages.control.ReadReceipt;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
@ -25,7 +27,6 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class MarkReadReceiver extends BroadcastReceiver {
@ -68,8 +69,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
if (markedReadMessages.isEmpty()) return;
List<SyncMessageId> syncMessageIds = new LinkedList<>();
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) return;
for (MarkedMessageInfo messageInfo : markedReadMessages) {
scheduleDeletion(context, messageInfo.getExpirationInfo());
@ -83,10 +83,9 @@ public class MarkReadReceiver extends BroadcastReceiver {
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
// Loki - Check whether we want to send a read receipt to this user
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
// Loki - Take into account multi device
ApplicationContext.getInstance(context)
.getJobManager()
.add(new SendReadReceiptJob(address, timestamps));
ReadReceipt readReceipt = new ReadReceipt(timestamps);
readReceipt.setSentTimestamp(System.currentTimeMillis());
MessageSender.send(readReceipt, address);
}
}

View File

@ -25,17 +25,19 @@ import android.os.AsyncTask;
import android.os.Bundle;
import androidx.core.app.RemoteInput;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
@ -68,21 +70,27 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
long threadId;
Recipient recipient = Recipient.from(context, address, false);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
VisibleMessage message = new VisibleMessage();
message.setSentTimestamp(System.currentTimeMillis());
message.setText(responseText.toString());
switch (replyMethod) {
case GroupMessage: {
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
threadId = MessageSender.send(context, reply, -1, false, null);
OutgoingMediaMessage reply = OutgoingMediaMessage.from(message, recipient, Collections.emptyList(), null, null);
try {
DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(reply, threadId, false, null);
MessageSender.send(message, address);
} catch (MmsException e) {
Log.w(TAG, e);
}
break;
}
case SecureMessage: {
OutgoingEncryptedMessage reply = new OutgoingEncryptedMessage(recipient, responseText.toString(), expiresIn);
threadId = MessageSender.send(context, reply, -1, false, null);
OutgoingTextMessage reply = OutgoingTextMessage.from(message, recipient);
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, reply, false, System.currentTimeMillis(), null);
MessageSender.send(message, address);
break;
}
default:

View File

@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import java.util.Comparator;
@ -70,6 +70,9 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Address address = Address.fromSerialized(senderPublicKey);
Recipient recipient = Recipient.from(context, address, false);
if (recipient.isBlocked()) return;
Optional<SignalServiceGroup> groupInfo = Optional.absent();
if (content.getDataMessage().hasGroup()) {
GroupContext groupContext = content.getDataMessage().getGroup();
@ -103,17 +106,18 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
}
@Override
public void startAnyExpiration(long messageID) {
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(messageID);
public void startAnyExpiration(long timestamp, @NotNull String author) {
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author);
if (messageRecord != null) {
boolean mms = messageRecord.isMms();
Recipient recipient = messageRecord.getRecipient();
if (recipient.getExpireMessages() <= 0) return;
if (mms) {
mmsDatabase.markExpireStarted(messageID);
mmsDatabase.markExpireStarted(messageRecord.getId());
} else {
smsDatabase.markExpireStarted(messageID);
smsDatabase.markExpireStarted(messageRecord.getId());
}
scheduleDeletion(messageID, mms, recipient.getExpireMessages());
scheduleDeletion(messageRecord.getId(), mms, recipient.getExpireMessages() * 1000);
}
}

View File

@ -7,11 +7,13 @@ import android.text.TextUtils;
import android.widget.Toast;
import network.loki.messenger.R;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.threads.Address;
import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsession.messaging.sending_receiving.MessageSender;
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.Rfc5724Uri;
import java.net.URISyntaxException;
@ -47,13 +49,11 @@ public class QuickResponseService extends IntentService {
number = URLDecoder.decode(number);
}
Address address = Address.fromExternal(this, number);
Recipient recipient = Recipient.from(this, address, false);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
if (!TextUtils.isEmpty(content)) {
MessageSender.send(this, new OutgoingTextMessage(recipient, content, expiresIn, subscriptionId), -1, false, null);
VisibleMessage message = new VisibleMessage();
message.setText(content);
message.setSentTimestamp(System.currentTimeMillis());
MessageSender.send(message, Address.fromExternal(this, number));
}
} catch (URISyntaxException e) {
Toast.makeText(this, R.string.QuickResponseService_problem_sending_message, Toast.LENGTH_LONG).show();

View File

@ -1,213 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.sms;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.threads.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.session.libsignal.utilities.logging.Log;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.session.libsession.utilities.TextSecurePreferences;
public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName();
public static long send(final Context context,
final OutgoingTextMessage message,
final long threadId,
final boolean forceSms,
final SmsDatabase.InsertListener insertListener)
{
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
Recipient recipient = message.getRecipient();
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
} else {
allocatedThreadId = threadId;
}
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
sendTextMessage(context, recipient, forceSms, messageId);
return allocatedThreadId;
}
public static long send(final Context context,
final OutgoingMediaMessage message,
final long threadId,
final boolean forceSms,
final SmsDatabase.InsertListener insertListener)
{
try {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long allocatedThreadId;
if (threadId == -1) {
allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType());
} else {
allocatedThreadId = threadId;
}
Recipient recipient = message.getRecipient();
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn());
return allocatedThreadId;
} catch (MmsException e) {
Log.w(TAG, e);
return threadId;
}
}
public static void resend(Context context, MessageRecord messageRecord) {
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
long expiresIn = messageRecord.getExpiresIn();
Recipient recipient = messageRecord.getRecipient();
if (messageRecord.isMms()) {
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
} else {
sendTextMessage(context, recipient, forceSms, messageId);
}
}
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, long expiresIn)
{
if (isLocalSelfSend(context, recipient, forceSms)) {
sendLocalMediaSelf(context, messageId);
} else if (isGroupPushSend(recipient)) {
sendGroupPush(context, recipient, messageId, null);
} else {
sendMediaPush(context, recipient, messageId);
}
}
private static void sendTextMessage(Context context, Recipient recipient,
boolean forceSms, long messageId)
{
if (isLocalSelfSend(context, recipient, forceSms)) {
sendLocalTextSelf(context, messageId);
} else {
sendTextPush(context, recipient, messageId);
}
}
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
}
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
}
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), filterAddress);
}
private static boolean isGroupPushSend(Recipient recipient) {
return recipient.getAddress().isGroup() &&
!recipient.getAddress().isMmsGroup();
}
private static boolean isLocalSelfSend(@NonNull Context context, @NonNull Recipient recipient, boolean forceSms) {
return recipient.isLocalNumber() &&
!forceSms &&
TextSecurePreferences.isPushRegistered(context);
}
private static void sendLocalMediaSelf(Context context, long messageId) {
try {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId);
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getSentTimeMillis());
for (Attachment attachment : message.getAttachments()) {
attachmentDatabase.markAttachmentUploaded(messageId, attachment);
}
mmsDatabase.markAsSent(messageId, true);
mmsDatabase.markUnidentified(messageId, true);
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
mmsDatabase.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
}
} catch (NoSuchMessageException | MmsException e) {
Log.w("Failed to update self-sent message.", e);
}
}
private static void sendLocalTextSelf(Context context, long messageId) {
try {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
SmsMessageRecord message = smsDatabase.getMessage(messageId);
SyncMessageId syncId = new SyncMessageId(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), message.getDateSent());
smsDatabase.markAsSent(messageId, true);
smsDatabase.markUnidentified(messageId, true);
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
if (message.getExpiresIn() > 0) {
smsDatabase.markExpireStarted(messageId);
expirationManager.scheduleDeletion(message.getId(), message.isMms(), message.getExpiresIn());
}
} catch (NoSuchMessageException e) {
Log.w("Failed to update self-sent message.", e);
}
}
}

View File

@ -1,15 +0,0 @@
package org.thoughtcrime.securesms.sms;
import org.session.libsession.messaging.threads.recipients.Recipient;
public class OutgoingEncryptedMessage extends OutgoingTextMessage {
public OutgoingEncryptedMessage(Recipient recipient, String body, long expiresIn) {
super(recipient, body, expiresIn, -1);
}
@Override
public boolean isSecureMessage() {
return true;
}
}

View File

@ -51,6 +51,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
return;
}
if (Recipient.from(context, author, false).isBlocked()) {
return;
}
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);
@ -76,6 +80,10 @@ public class TypingStatusRepository implements SSKEnvironment.TypingIndicatorsPr
return;
}
if (Recipient.from(context, author, false).isBlocked()) {
return;
}
Set<Typist> typists = Util.getOrDefault(typistMap, threadId, new LinkedHashSet<>());
Typist typist = new Typist(Recipient.from(context, author, false), device, threadId);

View File

@ -1,9 +0,0 @@
package org.thoughtcrime.securesms.transport;
public class RetryLaterException extends Exception {
public RetryLaterException() {}
public RetryLaterException(Exception e) {
super(e);
}
}

View File

@ -1,12 +0,0 @@
package org.thoughtcrime.securesms.transport;
public class UndeliverableMessageException extends Exception {
public UndeliverableMessageException(String detailMessage) {
super(detailMessage);
}
public UndeliverableMessageException(Throwable throwable) {
super(throwable);
}
}

View File

@ -7,35 +7,73 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import org.session.libsignal.utilities.logging.Log
import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import network.loki.messenger.R
import org.greenrobot.eventbus.EventBus
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.ByteUtil
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.backup.BackupEvent
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.BackupProtos.SharedPreference
import org.thoughtcrime.securesms.backup.FullBackupExporter
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.session.libsession.utilities.IdentityKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.database.BackupFileRecord
import org.thoughtcrime.securesms.service.LocalBackupListener
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.ByteUtil
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.util.*
import kotlin.jvm.Throws
object BackupUtil {
private const val MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences"
private const val TAG = "BackupUtil"
const val BACKUP_FILE_MIME_TYPE = "application/session-backup"
const val BACKUP_PASSPHRASE_LENGTH = 30
fun getBackupRecords(context: Context): List<SharedPreference> {
val prefName = MASTER_SECRET_UTIL_PREFERENCES_NAME
val preferences = context.getSharedPreferences(prefName, 0)
val prefList = LinkedList<SharedPreference>()
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF)
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, null))
.build())
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF)
.setValue(preferences.getString(IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, null))
.build())
if (preferences.contains(IdentityKeyUtil.ED25519_PUBLIC_KEY)) {
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.ED25519_PUBLIC_KEY)
.setValue(preferences.getString(IdentityKeyUtil.ED25519_PUBLIC_KEY, null))
.build())
}
if (preferences.contains(IdentityKeyUtil.ED25519_SECRET_KEY)) {
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.ED25519_SECRET_KEY)
.setValue(preferences.getString(IdentityKeyUtil.ED25519_SECRET_KEY, null))
.build())
}
prefList.add(SharedPreference.newBuilder()
.setFile(prefName)
.setKey(IdentityKeyUtil.LOKI_SEED)
.setValue(preferences.getString(IdentityKeyUtil.LOKI_SEED, null))
.build())
return prefList
}
/**
* Set app-wide configuration to enable the backups and schedule them.
*
@ -91,7 +129,7 @@ object BackupUtil {
@JvmStatic
fun generateBackupPassphrase(): Array<String> {
val random = ByteArray(BACKUP_PASSPHRASE_LENGTH).also { SecureRandom().nextBytes(it) }
return Array(6) {i ->
return Array(6) { i ->
String.format("%05d", ByteUtil.byteArray5ToLong(random, i * 5) % 100000)
}
}

View File

@ -13,13 +13,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center">
<org.thoughtcrime.securesms.components.location.SignalMapView
android:id="@+id/attachment_location"
android:layout_width="210dp"
android:layout_height="210dp"
android:layout_gravity="center_horizontal"
android:visibility="gone"/>
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/attachment_thumbnail"
android:layout_width="230dp"

View File

@ -1,10 +1,8 @@
package org.session.libsession.database
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import java.io.InputStream
@ -14,10 +12,13 @@ interface MessageDataProvider {
fun getMessageID(serverID: Long): Long?
fun deleteMessage(messageID: Long)
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
@ -26,12 +27,15 @@ interface MessageDataProvider {
fun isOutgoingMessage(timestamp: Long): Boolean
@Throws(Exception::class)
fun uploadAttachment(attachmentId: Long)
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
// Quotes
fun getMessageForQuote(timestamp: Long, author: Address): Long?
fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment>
fun getMessageBodyFor(messageID: Long): String
fun getAttachmentIDsFor(messageID: Long): List<Long>
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database.documents;
package org.session.libsession.database.documents;
import java.util.List;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database.documents;
package org.session.libsession.database.documents;
import org.session.libsignal.utilities.logging.Log;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database.documents;
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database.documents;
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.database.documents;
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -59,6 +59,8 @@ interface StorageProtocol {
fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, PublicChat>
fun addOpenGroup(server: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?
@ -94,10 +96,9 @@ interface StorageProtocol {
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun markAsSent(messageID: Long)
fun markUnidentified(messageID: Long)
fun setErrorMessage(messageID: Long, error: Exception)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
// Closed Groups
fun getGroup(groupID: String): GroupRecord?
@ -112,9 +113,9 @@ interface StorageProtocol {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>)
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long)
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?

View File

@ -1,40 +0,0 @@
package org.session.libsession.messaging.avatars;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.color.MaterialColor;
/**
* Used for migrating legacy colors to modern colors. For normal color generation, use
* {@link ContactColors}.
*/
public class ContactColorsLegacy {
private static final String[] LEGACY_PALETTE = new String[] {
"red",
"pink",
"purple",
"deep_purple",
"indigo",
"blue",
"light_blue",
"cyan",
"teal",
"green",
"light_green",
"orange",
"deep_orange",
"amber",
"blue_grey"
};
public static MaterialColor generateFor(@NonNull String name) {
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
try {
return MaterialColor.fromSerialized(serialized);
} catch (MaterialColor.UnknownColorException e) {
return ContactColors.generateFor(name);
}
}
}

View File

@ -29,7 +29,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
* possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
*/
public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5?
val server = "https://file.getsession.org"
public val fileStorageBucketURL = "https://file-static.lokinet.org"
// endregion

View File

@ -8,11 +8,15 @@ import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import org.session.libsignal.service.internal.crypto.PaddingInputStream
import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.logging.Log
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
@ -28,7 +32,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
// Settings
override val maxFailureCount: Int = 20
companion object {
val TAG = AttachmentUploadJob::class.qualifiedName
val TAG = AttachmentUploadJob::class.simpleName
val KEY: String = "AttachmentUploadJob"
val maxFailureCount: Int = 20
@ -41,46 +45,54 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
override fun execute() {
try {
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
ThreadUtils.queue {
try {
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return@queue handleFailure(Error.NoAttachment)
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
val server = openGroup?.server ?: FileServerAPI.server
var server = FileServerAPI.shared.server
var shouldEncrypt = true
val usePadding = false
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
openGroup?.let {
server = it.server
shouldEncrypt = false
}
//TODO add some encryption stuff here
val isEncryptionRequired = false
//val isEncryptionRequired = (server == FileServerAPI.server)
val attachmentKey = Util.getSecretBytes(64)
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
val attachmentKey = Util.getSecretBytes(64)
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val ciphertextLength = attachmentStream.length
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
handleSuccess(attachment, attachmentKey, uploadResult)
FileServerAPI.shared.uploadAttachment(server, attachmentData)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
} else {
this.handleFailure(e)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
} else {
this.handleFailure(e)
}
}
}
}
private fun handleSuccess() {
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
Log.w(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
//TODO interaction stuff, not sure how to deal with that
}
private fun handlePermanentFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
failAssociatedMessageSendJob(e)
}
@ -95,7 +107,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
private fun failAssociatedMessageSendJob(e: Exception) {
val storage = MessagingConfiguration.shared.storage
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message!!, e)
MessageSender.handleFailedMessageSend(this.message, e)
if (messageSendJob != null) {
storage.markJobAsFailed(messageSendJob)
}
@ -119,7 +131,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<AttachmentUploadJob> {

View File

@ -75,7 +75,7 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<MessageReceiveJob> {

View File

@ -19,7 +19,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
// Settings
override val maxFailureCount: Int = 10
companion object {
val TAG = MessageSendJob::class.qualifiedName
val TAG = MessageSendJob::class.simpleName
val KEY: String = "MessageSendJob"
//keys used for database storage purpose
@ -32,13 +32,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val message = message as? VisibleMessage
message?.let {
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
val attachmentsToUpload = attachments.filter { !it.isUploaded }
val attachmentIDs = mutableListOf<Long>()
attachmentIDs.addAll(message.attachmentIDs)
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
message.linkPreview?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) }
val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() }
attachmentsToUpload.forEach {
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {
if (MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
// Wait for it to finish
} else {
val job = AttachmentUploadJob(it.attachmentId, message.threadID!!.toString(), message, id!!)
val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!)
JobQueue.shared.add(job)
}
}
@ -82,10 +86,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val serializedMessage = ByteArray(4096)
val serializedDestination = ByteArray(4096)
var output = Output(serializedMessage)
kryo.writeObject(output, message)
kryo.writeClassAndObject(output, message)
output.close()
output = Output(serializedDestination)
kryo.writeObject(output, destination)
kryo.writeClassAndObject(output, destination)
output.close()
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
.putByteArray(KEY_DESTINATION, serializedDestination)
@ -93,7 +97,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<MessageSendJob> {
@ -103,10 +107,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
//deserialize Message and Destination properties
val kryo = Kryo()
var input = Input(serializedMessage)
val message: Message = kryo.readObject(input, Message::class.java)
val message = kryo.readClassAndObject(input) as Message
input.close()
input = Input(serializedDestination)
val destination: Destination = kryo.readObject(input, Destination::class.java)
val destination = kryo.readClassAndObject(input) as Destination
input.close()
return MessageSendJob(message, destination)
}

View File

@ -76,7 +76,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<NotifyPNServerJob> {

View File

@ -3,26 +3,50 @@ package org.session.libsession.messaging.messages
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.loki.utilities.toHexString
sealed class Destination {
class Contact(val publicKey: String) : Destination()
class ClosedGroup(val groupPublicKey: String) : Destination()
class OpenGroup(val channel: Long, val server: String) : Destination()
class Contact() : Destination() {
var publicKey: String = ""
internal constructor(publicKey: String): this() {
this.publicKey = publicKey
}
}
class ClosedGroup() : Destination() {
var groupPublicKey: String = ""
internal constructor(groupPublicKey: String): this() {
this.groupPublicKey = groupPublicKey
}
}
class OpenGroup() : Destination() {
var channel: Long = 0
var server: String = ""
internal constructor(channel: Long, server: String): this() {
this.channel = channel
this.server = server
}
}
companion object {
fun from(address: Address): Destination {
if (address.isContact) {
return Contact(address.contactIdentifier())
} else if (address.isClosedGroup) {
val groupID = address.contactIdentifier()
val groupPublicKey = GroupUtil.getDecodedGroupID(groupID)
return ClosedGroup(groupPublicKey)
} else if (address.isOpenGroup) {
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!!
return OpenGroup(openGroup.channel, openGroup.server)
} else {
throw Exception("TODO: Handle legacy closed groups.")
return when {
address.isContact -> {
Contact(address.contactIdentifier())
}
address.isClosedGroup -> {
val groupID = address.toGroupString()
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
ClosedGroup(groupPublicKey)
}
address.isOpenGroup -> {
val threadID = MessagingConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)!!
OpenGroup(openGroup.channel, openGroup.server)
}
else -> {
throw Exception("TODO: Handle legacy closed groups.")
}
}
}
}

View File

@ -1,5 +1,7 @@
package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
abstract class Message {
@ -18,11 +20,23 @@ abstract class Message {
// validation
open fun isValid(): Boolean {
sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false
receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false
sentTimestamp?.let {
if (it <= 0) return false
}
receivedTimestamp?.let {
if (it <= 0) return false
}
return sender != null && recipient != null
}
abstract fun toProto(): SignalServiceProtos.Content?
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
dataMessage.group = groupProto.build()
}
}

Some files were not shown because too many files have changed in this diff Show More