diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 5f4a989f75..b1b1cdab4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -8,6 +8,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.* import org.session.libsession.messaging.threads.Address 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.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -32,6 +34,18 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return databaseAttachment.toAttachmentPointer() } + override fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? { + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null + return databaseAttachment.toSignalAttachmentStream(context) + } + + override fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? { + val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) + val databaseAttachment = attachmentDatabase.getAttachment(AttachmentId(attachmentId, 0)) ?: return null + return databaseAttachment.toSignalAttachmentPointer() + } + override fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) { val attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context) attachmentDatabase.setTransferState(messageID, AttachmentId(attachmentId, 0), attachmentState.value) @@ -103,6 +117,17 @@ 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.toSignalAttachmentStream(context: Context): SignalServiceAttachmentStream { + val stream = PartAuthority.getAttachmentStream(context, this.dataUri!!) + val listener = SignalServiceAttachment.ProgressListener { total: Long, progress: Long -> EventBus.getDefault().postSticky(PartProgressEvent(this, total, progress))} + + return SignalServiceAttachmentStream(stream, this.contentType, this.size, Optional.fromNullable(this.fileName), this.isVoiceNote, Optional.absent(), this.width, this.height, Optional.fromNullable(this.caption), listener) +} + fun DatabaseAttachment.shouldHaveImageSize(): Boolean { return (MediaUtil.isVideo(this) || MediaUtil.isImage(this) || MediaUtil.isGif(this)); } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 90dfedeea4..b0252aab78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -136,7 +136,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)); } else { String groupId = GroupUtil.getEncodedMMSGroupID(allocateGroupId()); - create(groupId, null, members, null, null, admins); + create(groupId, null, members, null, null, admins, System.currentTimeMillis()); return groupId; } } finally { @@ -196,7 +196,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } public long create(@NonNull String groupId, @Nullable String title, @NonNull List
members, - @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins) + @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins, @NonNull Long formationTimestamp) { Collections.sort(members); @@ -214,7 +214,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } contentValues.put(AVATAR_RELAY, relay); - contentValues.put(TIMESTAMP, System.currentTimeMillis()); + contentValues.put(TIMESTAMP, formationTimestamp); contentValues.put(ACTIVE, 1); contentValues.put(MMS, GroupUtil.isMmsGroup(groupId)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 2e6e2a9b3a..698ababc18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -905,6 +905,9 @@ public class MmsDatabase extends MessagingDatabase { public Optional insertSecureDecryptedMessageOutbox(OutgoingMediaMessage retrieved, long threadId, long serverTimestamp) throws MmsException { + if (threadId == -1) { + threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(retrieved.getRecipient()); + } long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp); if (messageId == -1) { return Optional.absent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index a27ae0c691..2deacb7fa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -78,6 +78,14 @@ public class MmsSmsDatabase extends Database { super(context, databaseHelper); } + public @Nullable MessageRecord getMessageForTimestamp(long timestamp) { + MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); + try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { + MmsSmsDatabase.Reader reader = db.readerFor(cursor); + return reader.getNext(); + } + } + public @Nullable MessageRecord getMessageFor(long messageId) { MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 0234dfae92..b27bda479e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -687,6 +687,9 @@ public class SmsDatabase extends MessagingDatabase { } public Optional insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) { + if (threadId == -1) { + threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(message.getRecipient()); + } long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null); if (messageId == -1) { return Optional.absent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 30e886d4cc..7d39d5032f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -12,6 +12,8 @@ import org.session.libsession.messaging.messages.visible.Attachment 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 @@ -27,17 +29,21 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer 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.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.thoughtcrime.securesms.mms.PartAuthority import org.thoughtcrime.securesms.sms.IncomingGroupMessage import org.thoughtcrime.securesms.sms.IncomingTextMessage +import org.thoughtcrime.securesms.sms.OutgoingTextMessage class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), StorageProtocol { override fun getUserPublicKey(): String? { @@ -90,9 +96,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?): Long? { var messageID: Long? = null - val address = Address.fromSerialized(message.sender!!) - val recipient = Recipient.from(context, address, false) - val body: Optional = if (message.text != null) Optional.of(message.text) else Optional.absent() + val senderAddress = Address.fromSerialized(message.sender!!) + val senderRecipient = Recipient.from(context, senderAddress, false) var group: Optional = Optional.absent() if (openGroupID != null) { group = Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT)) @@ -100,17 +105,39 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, group = Optional.of(SignalServiceGroup(groupPublicKey.toByteArray(), SignalServiceGroup.GroupType.SIGNAL)) } if (message.isMediaMessage()) { - val attachments: Optional> = Optional.absent() // TODO figure out how to get SignalServiceAttachment with attachmentID val quote: Optional = if (quotes != null) Optional.of(quotes) else Optional.absent() val linkPreviews: Optional> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! }) - val mediaMessage = IncomingMediaMessage(address, message.receivedTimestamp!!, -1, recipient.expireMessages * 1000L, false, false, body, group, attachments, quote, Optional.absent(), linkPreviews, Optional.absent()) val mmsDatabase = DatabaseFactory.getMmsDatabase(context) mmsDatabase.beginTransaction() - val insertResult: Optional - if (group.isPresent) { - insertResult = mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!); + val insertResult = if (message.sender == getUserPublicKey()) { + val targetAddress = if (message.syncTarget != null) { + Address.fromSerialized(message.syncTarget!!) + } else { + if (group.isPresent) { + Address.fromSerialized(GroupUtil.getEncodedId(group.get())) + } else { + Log.d("Loki", "Cannot handle message from self.") + return null + } + } + val attachments = message.attachmentIDs.mapNotNull { + DatabaseFactory.getAttachmentProvider(context).getSignalAttachmentPointer(it) + }.mapNotNull { + PointerAttachment.forPointer(Optional.of(it)).orNull() + } + val mediaMessage = OutgoingMediaMessage.from(message, Recipient.from(context, targetAddress, false), attachments, quote.orNull(), linkPreviews.orNull()) + mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) } else { - insertResult = mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1) + // It seems like we have replaced SignalServiceAttachment with SessionServiceAttachment + val attachments: Optional> = Optional.of(message.attachmentIDs.mapNotNull { + DatabaseFactory.getAttachmentProvider(context).getSignalAttachmentPointer(it) + }) + val mediaMessage = IncomingMediaMessage.from(message, senderAddress, senderRecipient.expireMessages * 1000L, group, attachments, quote, linkPreviews) + if (group.isPresent) { + mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!) + } else { + mmsDatabase.insertSecureDecryptedMessageInbox(mediaMessage, message.threadID ?: -1) + } } if (insertResult.isPresent) { mmsDatabase.setTransactionSuccessful() @@ -118,9 +145,28 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } mmsDatabase.endTransaction() } else { - val textMessage = IncomingTextMessage(address, 1, message.receivedTimestamp!!, body.get(), group, recipient.expireMessages * 1000L, false) val smsDatabase = DatabaseFactory.getSmsDatabase(context) - val insertResult = smsDatabase.insertMessageInbox(textMessage) + val insertResult = if (message.sender == getUserPublicKey()) { + val targetAddress = if (message.syncTarget != null) { + Address.fromSerialized(message.syncTarget!!) + } else { + if (group.isPresent) { + Address.fromSerialized(GroupUtil.getEncodedId(group.get())) + } else { + Log.d("Loki", "Cannot handle message from self.") + return null + } + } + val textMessage = OutgoingTextMessage.from(message, Recipient.from(context, targetAddress, false)) + smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!) + } else { + val textMessage = IncomingTextMessage.from(message, senderAddress, group, senderRecipient.expireMessages * 1000L) + if (group.isPresent) { + smsDatabase.insertMessageInbox(textMessage, message.sentTimestamp!!) + } else { + smsDatabase.insertMessageInbox(textMessage) + } + } if (insertResult.isPresent) { messageID = insertResult.get().messageId } @@ -129,7 +175,6 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } // JOBS - override fun persistJob(job: Job) { DatabaseFactory.getSessionJobDatabase(context).persistJob(job) } @@ -235,6 +280,15 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server) } + override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean { + val database = DatabaseFactory.getMmsSmsDatabase(context) + return if (sender.isEmpty()) { + database.getMessageForTimestamp(timestamp) != null + } else { + database.getMessageFor(timestamp, sender) != null + } + } + override fun setUserCount(group: Long, server: String, newValue: Int) { DatabaseFactory.getLokiAPIDatabase(context).setUserCount(group, server, newValue) } @@ -256,13 +310,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun getReceivedMessageTimestamps(): Set { - TODO("Not yet implemented") + return SessionMetaProtocol.getTimestamps() } override fun addReceivedMessageTimestamp(timestamp: Long) { - TODO("Not yet implemented") + SessionMetaProtocol.addTimestamp(timestamp) } +// override fun removeReceivedMessageTimestamps(timestamps: Set) { +// TODO("Not yet implemented") +// } + override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? { val database = DatabaseFactory.getMmsSmsDatabase(context) val address = Address.fromSerialized(author) @@ -314,8 +372,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return if (group.isPresent) { group.get() } else null } - override fun createGroup(groupId: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
) { - DatabaseFactory.getGroupDatabase(context).create(groupId, title, members, avatar, relay, admins) + override fun createGroup(groupId: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) { + DatabaseFactory.getGroupDatabase(context).create(groupId, title, members, avatar, relay, admins, formationTimestamp) } override fun setActive(groupID: String, value: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index af11e476c4..3606954a89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -77,7 +77,7 @@ public class GroupManager { String masterPublicKey = masterPublicKeyOrNull != null ? masterPublicKeyOrNull : TextSecurePreferences.getLocalNumber(context); memberAddresses.add(Address.Companion.fromSerialized(masterPublicKey)); - groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses)); + groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses), System.currentTimeMillis()); groupDatabase.updateProfilePicture(groupId, avatarBytes); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true); @@ -104,7 +104,7 @@ public class GroupManager { final Set
memberAddresses = new HashSet<>(); memberAddresses.add(Address.Companion.fromSerialized(Objects.requireNonNull(TextSecurePreferences.getLocalNumber(context)))); - groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>()); + groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(), System.currentTimeMillis()); groupDatabase.updateProfilePicture(groupId, avatarBytes); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index e6474ce4b7..cda9f4116e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -69,7 +69,7 @@ public class GroupMessageProcessor { if (record.isPresent() && group.getType() == Type.UPDATE) { return handleGroupUpdate(context, content, group, record.get(), outgoing); } else if (!record.isPresent() && group.getType() == Type.UPDATE) { - return handleGroupCreate(context, content, group, outgoing); + return handleGroupCreate(context, content, group, outgoing, message.getTimestamp()); } else if (record.isPresent() && group.getType() == Type.QUIT) { return handleGroupLeave(context, content, group, record.get(), outgoing); } else if (record.isPresent() && group.getType() == Type.REQUEST_INFO) { @@ -83,7 +83,8 @@ public class GroupMessageProcessor { private static @Nullable Long handleGroupCreate(@NonNull Context context, @NonNull SignalServiceContent content, @NonNull SignalServiceGroup group, - boolean outgoing) + boolean outgoing, + Long formationTimestamp) { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); String id = GroupUtil.getEncodedId(group); @@ -108,7 +109,7 @@ public class GroupMessageProcessor { } database.create(id, group.getName().orNull(), members, - avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins); + avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins, formationTimestamp); if (group.getMembers().isPresent()) { ClosedGroupsProtocol.establishSessionsWithMembersIfNeeded(context, group.getMembers().get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index fff03d6b74..90d18dd0d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -69,7 +69,7 @@ object ClosedGroupsProtocolV2 { 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!!) })) + 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 @@ -463,7 +463,7 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { groupDB.create(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + null, null, LinkedList(admins.map { Address.fromSerialized(it) }), sentTimestamp) } DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Add the group to the user's set of public keys to poll for @@ -716,7 +716,7 @@ object ClosedGroupsProtocolV2 { senderPublicKey: String): Boolean { val oldMembers = group.members.map { it.serialize() } // Check that the message isn't from before the group was created - if (group.createdAt > sentTimestamp) { + if (group.formationTimestamp > sentTimestamp) { Log.d("Loki", "Ignoring closed group update from before thread was created.") return false } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt index d0ba9c36e5..5e1923a5f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -19,6 +19,7 @@ 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 @@ -41,6 +42,7 @@ object MultiDeviceProtocol { } } + // TODO: refactor this to use new message sending job fun forceSyncConfigurationNowIfNeeded(context: Context) { val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return val configurationMessage = ConfigurationMessage.getCurrent() @@ -58,6 +60,7 @@ object MultiDeviceProtocol { } } + // TODO: remove this after we migrate to new message receiving pipeline @JvmStatic fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) { if (TextSecurePreferences.getConfigurationMessageSynced(context)) return diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt index 03eaf970c0..7e5125e58f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt @@ -21,6 +21,14 @@ object SessionMetaProtocol { timestamps.remove(timestamp) } + fun getTimestamps(): Set { + return timestamps + } + + fun addTimestamp(timestamp: Long) { + timestamps.add(timestamp) + } + @JvmStatic fun shouldIgnoreMessage(timestamp: Long): Boolean { val shouldIgnoreMessage = timestamps.contains(timestamp) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index 1ade07f466..6ea0ae5c73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -1,15 +1,18 @@ package org.thoughtcrime.securesms.mms; +import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment; import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact; import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; +import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.utilities.GroupUtil; import org.session.libsignal.libsignal.util.guava.Optional; import org.session.libsignal.service.api.messages.SignalServiceAttachment; import org.session.libsignal.service.api.messages.SignalServiceGroup; +import org.thoughtcrime.securesms.ApplicationContext; import java.util.Collections; import java.util.LinkedList; @@ -92,6 +95,18 @@ public class IncomingMediaMessage { } } + public static IncomingMediaMessage from(VisibleMessage message, + Address from, + long expiresIn, + Optional group, + Optional> attachments, + Optional quote, + Optional> linkPreviews) + { + return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false, + false, Optional.fromNullable(message.getText()), group, attachments, quote, Optional.absent(), linkPreviews, Optional.absent()); + } + public int getSubscriptionId() { return subscriptionId; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java index 903c44cbd7..fb22876181 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; +import org.session.libsession.messaging.messages.visible.VisibleMessage; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.session.libsession.messaging.sending_receiving.attachments.Attachment; @@ -12,6 +14,7 @@ import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPrevie import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel; import org.session.libsession.messaging.threads.recipients.Recipient; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -86,6 +89,17 @@ public class OutgoingMediaMessage { this.linkPreviews.addAll(that.linkPreviews); } + public static OutgoingMediaMessage from(VisibleMessage message, + Recipient recipient, + List attachments, + @Nullable QuoteModel outgoingQuote, + @NonNull List linkPreviews) + { + return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1, + recipient.getExpireMessages() * 1000, ThreadDatabase.DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(), + linkPreviews, Collections.emptyList(), Collections.emptyList()); + } + public Recipient getRecipient() { return recipient; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index eaf06f80d0..0c57fa004b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.telephony.SmsMessage; +import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.threads.Address; import org.session.libsession.utilities.GroupUtil; import org.session.libsignal.libsignal.util.guava.Optional; @@ -155,6 +156,14 @@ public class IncomingTextMessage implements Parcelable { this.unidentified = false; } + public static IncomingTextMessage from(VisibleMessage message, + Address sender, + Optional group, + long expiresInMillis) + { + return new IncomingTextMessage(sender, 1, message.getReceivedTimestamp(), message.getText(), group, expiresInMillis, false); + } + public int getSubscriptionId() { return subscriptionId; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java index 76fe051666..c09b426472 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.sms; +import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.session.libsession.messaging.threads.recipients.Recipient; @@ -28,6 +29,10 @@ public class OutgoingTextMessage { this.message = body; } + public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) { + return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1); + } + public long getExpiresIn() { return expiresIn; } diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index 27518b8d34..758710840b 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -6,6 +6,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.SessionSer import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream import org.session.libsession.messaging.threads.Address import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer +import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream import java.io.InputStream interface MessageDataProvider { @@ -14,9 +15,11 @@ interface MessageDataProvider { fun deleteMessage(messageID: Long) fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? - fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer? + fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream? + fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer? + fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long) fun insertAttachment(messageId: Long, attachmentId: Long, stream : InputStream) diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index 164bf53088..e3062f73ee 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -86,8 +86,10 @@ interface StorageProtocol { fun removeLastDeletionServerID(group: Long, server: String) // Message Handling + fun isMessageDuplicated(timestamp: Long, sender: String): Boolean fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) +// fun removeReceivedMessageTimestamps(timestamps: Set) // Returns the IDs of the saved attachments. fun persistAttachments(messageId: Long, attachments: List): List @@ -99,7 +101,7 @@ interface StorageProtocol { // Closed Groups fun getGroup(groupID: String): GroupRecord? - fun createGroup(groupID: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
) + fun createGroup(groupID: String, title: String?, members: List
, avatar: SignalServiceAttachmentPointer?, relay: String?, admins: List
, formationTimestamp: Long) fun setActive(groupID: String, value: Boolean) fun removeMember(groupID: String, member: Address) fun updateMembers(groupID: String, members: List
) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index f1b2136f8c..37eafe8b1b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -8,7 +8,7 @@ import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream import java.io.File import java.io.FileInputStream -class AttachmentDownloadJob(val attachmentID: Long, val tsIncomingMessageID: Long): Job { +class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long): Job { override var delegate: JobDelegate? = null override var id: String? = null @@ -34,17 +34,17 @@ class AttachmentDownloadJob(val attachmentID: Long, val tsIncomingMessageID: Lon override fun execute() { val messageDataProvider = MessagingConfiguration.shared.messageDataProvider val attachmentStream = messageDataProvider.getAttachmentStream(attachmentID) ?: return handleFailure(Error.NoAttachment) - messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.tsIncomingMessageID) + messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) val tempFile = createTempFile() val handleFailure: (java.lang.Exception) -> Unit = { exception -> tempFile.delete() if(exception is Error && exception == Error.NoAttachment) { - MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, tsIncomingMessageID) + MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) this.handlePermanentFailure(exception) } else if (exception is DotNetAPI.Error && exception == DotNetAPI.Error.ParsingFailed) { // No need to retry if the response is invalid. Most likely this means we (incorrectly) // got a "Cannot GET ..." error from the file server. - MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, tsIncomingMessageID) + MessagingConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) this.handlePermanentFailure(exception) } else { this.handleFailure(exception) @@ -62,7 +62,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val tsIncomingMessageID: Lon var stream = if (!attachmentStream.digest.isPresent || attachmentStream.key == null) FileInputStream(tempFile) else AttachmentCipherInputStream.createForAttachment(tempFile, attachmentStream.length.or(0).toLong(), attachmentStream.key?.toByteArray(), attachmentStream?.digest.get()) - messageDataProvider.insertAttachment(tsIncomingMessageID, attachmentID, stream) + messageDataProvider.insertAttachment(databaseMessageID, attachmentID, stream) tempFile.delete() @@ -90,7 +90,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val tsIncomingMessageID: Lon override fun serialize(): Data { return Data.Builder().putLong(KEY_ATTACHMENT_ID, attachmentID) - .putLong(KEY_TS_INCOMING_MESSAGE_ID, tsIncomingMessageID) + .putLong(KEY_TS_INCOMING_MESSAGE_ID, databaseMessageID) .build(); } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 4d75d31b27..dc276d7eb3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -76,14 +76,13 @@ class JobQueue : JobDelegate { private fun getRetryInterval(job: Job): Long { // Arbitrary backoff factor... - // try 1 delay: 0ms - // try 2 delay: 190ms + // try 1 delay: 0.5s + // try 2 delay: 1s // ... - // try 5 delay: 1300ms + // try 5 delay: 16s // ... - // try 11 delay: 61310ms - val backoffFactor = 1.9 - val maxBackoff = (60 * 60 * 1000).toDouble() - return (100 * min(maxBackoff, backoffFactor.pow(job.failureCount))).roundToLong() + // try 11 delay: 512s + val maxBackoff = (10 * 60).toDouble() // 10 minutes + return (1000 * 0.25 * min(maxBackoff, (2.0).pow(job.failureCount))).roundToLong() } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index e835c128b1..d6b2bfff8f 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -32,17 +32,21 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val fun executeAsync(): Promise { val deferred = deferred() try { - val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID) + val isRetry: Boolean = failureCount != 0 + val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, isRetry) MessageReceiver.handle(message, proto, this.openGroupID) this.handleSuccess() deferred.resolve(Unit) } catch (e: Exception) { Log.d(TAG, "Couldn't receive message due to error: $e.") val error = e as? MessageReceiver.Error - error?.let { - if (!error.isRetryable) this.handlePermanentFailure(error) + if (error != null && !error.isRetryable) { + Log.d("Loki", "Message receive job permanently failed due to error: $error.") + this.handlePermanentFailure(error) + } else { + Log.d("Loki", "Couldn't receive message due to error: $e.") + this.handleFailure(e) } - this.handleFailure(e) deferred.resolve(Unit) // The promise is just used to keep track of when we're done } return deferred.promise diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt index 176cd21182..41c9a92e8b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt @@ -18,24 +18,24 @@ class ClosedGroupControlMessage() : ControlMessage() { } } - override val isSelfSendValid: Boolean = run { - when(kind) { - is Kind.New -> false - else -> true - } - } + override val isSelfSendValid: Boolean = true var kind: Kind? = null // Kind enum sealed class Kind { class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List, val admins: List) : Kind() - class Update(val name: String, val members: List) : Kind() //deprecated - class EncryptionKeyPair(val wrappers: Collection) : Kind() + /// - Note: Deprecated in favor of more explicit group updates. + class Update(val name: String, val members: List) : Kind() + /// An encryption key pair encrypted for each member individually. + /// + /// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group). + class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection) : Kind() class NameChange(val name: String) : Kind() class MembersAdded(val members: List) : Kind() class MembersRemoved( val members: List) : Kind() object MemberLeft : Kind() + object EncryptionKeyPairRequest: Kind() val description: String = run { when(this) { @@ -46,6 +46,7 @@ class ClosedGroupControlMessage() : ControlMessage() { is MembersAdded -> "membersAdded" is MembersRemoved -> "membersRemoved" MemberLeft -> "memberLeft" + EncryptionKeyPairRequest -> "encryptionKeyPairRequest" } } } @@ -54,43 +55,45 @@ class ClosedGroupControlMessage() : ControlMessage() { const val TAG = "ClosedGroupControlMessage" fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? { - val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null + val closedGroupControlMessageProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null val kind: Kind - when(closedGroupUpdateProto.type) { + when(closedGroupControlMessageProto.type) { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> { - val publicKey = closedGroupUpdateProto.publicKey ?: return null - val name = closedGroupUpdateProto.name ?: return null - val encryptionKeyPairAsProto = closedGroupUpdateProto.encryptionKeyPair ?: return null + val publicKey = closedGroupControlMessageProto.publicKey ?: return null + val name = closedGroupControlMessageProto.name ?: return null + val encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair ?: return null try { val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) - kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupUpdateProto.membersList, closedGroupUpdateProto.adminsList) + kind = Kind.New(publicKey, name, encryptionKeyPair, closedGroupControlMessageProto.membersList, closedGroupControlMessageProto.adminsList) } catch (e: Exception) { Log.w(TAG, "Couldn't parse key pair") return null } } SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE -> { - val name = closedGroupUpdateProto.name ?: return null - kind = Kind.Update(name, closedGroupUpdateProto.membersList) + val name = closedGroupControlMessageProto.name ?: return null + kind = Kind.Update(name, closedGroupControlMessageProto.membersList) } SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR -> { - val wrappers = closedGroupUpdateProto.wrappersList.mapNotNull { KeyPairWrapper.fromProto(it) } - kind = Kind.EncryptionKeyPair(wrappers) + val publicKey = closedGroupControlMessageProto.publicKey + val wrappers = closedGroupControlMessageProto.wrappersList.mapNotNull { KeyPairWrapper.fromProto(it) } + kind = Kind.EncryptionKeyPair(publicKey, wrappers) } SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> { - val name = closedGroupUpdateProto.name ?: return null + val name = closedGroupControlMessageProto.name ?: return null kind = Kind.NameChange(name) } SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> { - kind = Kind.MembersAdded(closedGroupUpdateProto.membersList) + kind = Kind.MembersAdded(closedGroupControlMessageProto.membersList) } SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> { - kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList) + kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList) } SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> { kind = Kind.MemberLeft } + //TODO: SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR_REQUEST } return ClosedGroupControlMessage(kind) } @@ -116,6 +119,7 @@ class ClosedGroupControlMessage() : ControlMessage() { is Kind.MembersAdded -> kind.members.isNotEmpty() is Kind.MembersRemoved -> kind.members.isNotEmpty() is Kind.MemberLeft -> true + is Kind.EncryptionKeyPairRequest -> true } } @@ -126,53 +130,57 @@ class ClosedGroupControlMessage() : ControlMessage() { return null } try { - val closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2.Builder = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder() + val closedGroupControlMessage: SignalServiceProtos.ClosedGroupUpdateV2.Builder = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder() when (kind) { is Kind.New -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW - closedGroupUpdate.publicKey = kind.publicKey - closedGroupUpdate.name = kind.name + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW + closedGroupControlMessage.publicKey = kind.publicKey + closedGroupControlMessage.name = kind.name val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder() encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize()) encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize()) try { - closedGroupUpdate.encryptionKeyPair = encryptionKeyPairAsProto.build() + closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build() } catch (e: Exception) { Log.w(TAG, "Couldn't construct closed group update proto from: $this") return null } - closedGroupUpdate.addAllMembers(kind.members) - closedGroupUpdate.addAllAdmins(kind.admins) + closedGroupControlMessage.addAllMembers(kind.members) + closedGroupControlMessage.addAllAdmins(kind.admins) } is Kind.Update -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE - closedGroupUpdate.name = kind.name - closedGroupUpdate.addAllMembers(kind.members) + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.UPDATE + closedGroupControlMessage.name = kind.name + closedGroupControlMessage.addAllMembers(kind.members) } is Kind.EncryptionKeyPair -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR - closedGroupUpdate.addAllWrappers(kind.wrappers.map { it.toProto() }) + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR + closedGroupControlMessage.publicKey = kind.publicKey + closedGroupControlMessage.addAllWrappers(kind.wrappers.map { it.toProto() }) } is Kind.NameChange -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE - closedGroupUpdate.name = kind.name + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE + closedGroupControlMessage.name = kind.name } is Kind.MembersAdded -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED - closedGroupUpdate.addAllMembers(kind.members) + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED + closedGroupControlMessage.addAllMembers(kind.members) } is Kind.MembersRemoved -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED - closedGroupUpdate.addAllMembers(kind.members) + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED + closedGroupControlMessage.addAllMembers(kind.members) } is Kind.MemberLeft -> { - closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT + closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT + } + is Kind.EncryptionKeyPairRequest -> { + // TODO: closedGroupControlMessage.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.ENCRYPTION_KEY_PAIR_REQUEST } } val contentProto = SignalServiceProtos.Content.newBuilder() val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() - dataMessageProto.closedGroupUpdateV2 = closedGroupUpdate.build() + dataMessageProto.closedGroupUpdateV2 = closedGroupControlMessage.build() // Group context contentProto.dataMessage = dataMessageProto.build() return contentProto.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index d45a438049..0f5f283c73 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -10,6 +10,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class VisibleMessage : Message() { + var syncTarget: String? = null var text: String? = null var attachmentIDs = ArrayList() var quote: Quote? = null @@ -17,12 +18,15 @@ class VisibleMessage : Message() { var contact: Contact? = null var profile: Profile? = null + override val isSelfSendValid: Boolean = true + companion object { const val TAG = "VisibleMessage" fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? { val dataMessage = proto.dataMessage ?: return null val result = VisibleMessage() + result.syncTarget = dataMessage.syncTarget result.text = dataMessage.body // Attachments are handled in MessageReceiver val quoteProto = dataMessage.quote @@ -103,6 +107,10 @@ class VisibleMessage : Message() { } val attachmentProtos = attachments.mapNotNull { it.toProto() } dataMessage.addAllAttachments(attachmentProtos) + // Sync target + if (syncTarget != null) { + dataMessage.syncTarget = syncTarget + } // TODO Contact // Build try { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index e19686fabc..6ec3d4e1a3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -2,17 +2,15 @@ package org.session.libsession.messaging.sending_receiving import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.Message -import org.session.libsession.messaging.messages.control.ClosedGroupUpdate -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate -import org.session.libsession.messaging.messages.control.ReadReceipt -import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsession.messaging.messages.control.* import org.session.libsession.messaging.messages.visible.VisibleMessage -import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.service.internal.push.SignalServiceProtos -import java.lang.Error object MessageReceiver { + + private val lastEncryptionKeyPairRequest = mutableMapOf() + internal sealed class Error(val description: String) : Exception() { object DuplicateMessage: Error("Duplicate message.") object InvalidMessage: Error("Invalid message.") @@ -20,6 +18,7 @@ object MessageReceiver { object UnknownEnvelopeType: Error("Unknown envelope type.") object NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.") object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.") + object InvalidSignature: Error("Invalid message signature.") object NoData: Error("Received an empty envelope.") object SenderBlocked: Error("Received a message from a blocked user.") object NoThread: Error("Couldn't find thread for message.") @@ -28,12 +27,13 @@ object MessageReceiver { // Shared sender keys object InvalidGroupPublicKey: Error("Invalid group public key.") object NoGroupKeyPair: Error("Missing group key pair.") - object SharedSecretGenerationFailed: Error("Couldn't generate a shared secret.") internal val isRetryable: Boolean = when (this) { + is DuplicateMessage -> false is InvalidMessage -> false is UnknownMessage -> false is UnknownEnvelopeType -> false + is InvalidSignature -> false is NoData -> false is SenderBlocked -> false is SelfSend -> false @@ -41,13 +41,16 @@ object MessageReceiver { } } - internal fun parse(data: ByteArray, openGroupServerID: Long?): Pair { + internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair { val storage = MessagingConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() val isOpenGroupMessage = openGroupServerID != null // Parse the envelope val envelope = SignalServiceProtos.Envelope.parseFrom(data) - if (storage.getReceivedMessageTimestamps().contains(envelope.timestamp)) throw Error.DuplicateMessage + // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp + // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround + // for this issue. + if (storage.isMessageDuplicated(envelope.timestamp, envelope.source) && !isRetry) throw Error.DuplicateMessage storage.addReceivedMessageTimestamp(envelope.timestamp) // Decrypt the contents val ciphertext = envelope.content ?: throw Error.NoData @@ -60,7 +63,7 @@ object MessageReceiver { } else { when (envelope.type) { SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER -> { - val userX25519KeyPair = MessagingConfiguration.shared.storage.getUserX25519KeyPair() ?: throw Error.NoUserX25519KeyPair + val userX25519KeyPair = MessagingConfiguration.shared.storage.getUserX25519KeyPair() val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair) plaintext = decryptionResult.first sender = decryptionResult.second @@ -89,16 +92,28 @@ object MessageReceiver { } } } - decrypt() groupPublicKey = envelope.source + decrypt() +// try { +// decrypt() +// } catch(error: Exception) { +// val now = System.currentTimeMillis() +// var shouldRequestEncryptionKeyPair = true +// lastEncryptionKeyPairRequest[groupPublicKey!!]?.let { +// shouldRequestEncryptionKeyPair = now - it > 30 * 1000 +// } +// if (shouldRequestEncryptionKeyPair) { +// MessageSender.requestEncryptionKeyPair(groupPublicKey) +// lastEncryptionKeyPairRequest[groupPublicKey] = now +// } +// throw error +// } } else -> throw Error.UnknownEnvelopeType } } // Don't process the envelope any further if the sender is blocked if (isBlock(sender!!)) throw Error.SenderBlocked - // Ignore self sends - if (sender == userPublicKey) throw Error.SelfSend // Parse the proto val proto = SignalServiceProtos.Content.parseFrom(plaintext) // Parse the message @@ -106,17 +121,24 @@ object MessageReceiver { TypingIndicator.fromProto(proto) ?: ClosedGroupUpdate.fromProto(proto) ?: ExpirationTimerUpdate.fromProto(proto) ?: + ConfigurationMessage.fromProto(proto) ?: VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage + // Ignore self sends if needed + if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend + // Guard against control messages in open groups if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage + // Finish parsing message.sender = sender message.recipient = userPublicKey message.sentTimestamp = envelope.timestamp message.receivedTimestamp = System.currentTimeMillis() message.groupPublicKey = groupPublicKey message.openGroupServerMessageID = openGroupServerID + // Validate var isValid = message.isValid() if (message is VisibleMessage && !isValid && proto.dataMessage.attachmentsCount == 0) { isValid = true } if (!isValid) { throw Error.InvalidMessage } + // Return return Pair(message, proto) } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt index 9cac49d82d..b37c2eafe7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverHandler.kt @@ -103,18 +103,21 @@ fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, proto } private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) { + val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage + if (TextSecurePreferences.getConfigurationMessageSynced(context)) return if (message.sender != storage.getUserPublicKey()) return val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() for (closeGroup in message.closedGroups) { if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue - handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins) + handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } for (openGroup in message.openGroups) { if (allOpenGroups.contains(openGroup)) continue storage.addOpenGroup(openGroup, 1) } + TextSecurePreferences.setConfigurationMessageSynced(context, true) } fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) { @@ -155,7 +158,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS } } // Get or create thread - val threadID = storage.getOrCreateThreadIdFor(message.sender!!, message.groupPublicKey, openGroupID) + val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: message.sender!!, message.groupPublicKey, openGroupID) // Parse quote if needed var quoteModel: QuoteModel? = null if (message.quote != null && proto.dataMessage.hasQuote()) { @@ -209,6 +212,7 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message) is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message) ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message) + ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message) } } @@ -217,11 +221,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess val groupPublicKey = kind.publicKey.toByteArray().toHexString() val members = kind.members.map { it.toByteArray().toHexString() } val admins = kind.admins.map { it.toByteArray().toHexString() } - handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins) + handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!) } // Parameter @sender:String is just for inserting incoming info message -private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List) { +private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List, formationTimestamp: Long) { val context = MessagingConfiguration.shared.context val storage = MessagingConfiguration.shared.storage // Create the group @@ -232,15 +236,15 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) + // Notify the user + storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) } storage.setProfileSharing(Address.fromSerialized(groupID), true) // Add the group to the user's set of public keys to poll for storage.addClosedGroupPublicKey(groupPublicKey) // Store the encryption key pair storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) - // Notify the user - storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) // Notify the PN server PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!) } @@ -300,7 +304,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr val storage = MessagingConfiguration.shared.storage val senderPublicKey = message.sender ?: return val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return - val groupPublicKey = message.groupPublicKey ?: return + val groupPublicKey = kind.publicKey?.toByteArray()?.toHexString() ?: message.groupPublicKey ?: return val userPublicKey = storage.getUserPublicKey()!! val userKeyPair = storage.getUserX25519KeyPair() // Unwrap the message @@ -309,8 +313,8 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr Log.d("Loki", "Ignoring closed group info message for nonexistent group.") return } - if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { - android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.") + if (!group.members.map { it.toString() }.contains(senderPublicKey)) { + android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.") return } // Find our wrapper and decrypt it if possible @@ -320,7 +324,12 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr // Parse it val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) - // Store it + // Store it if needed + val closedGroupEncryptionKeyPairs = storage.getClosedGroupEncryptionKeyPairs(groupPublicKey) + if (closedGroupEncryptionKeyPairs.contains(keyPair)) { + Log.d("Loki", "Ignoring duplicate closed group encryption key pair.") + return + } storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) Log.d("Loki", "Received a new closed group encryption key pair") } @@ -368,12 +377,16 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo val members = group.members.map { it.serialize() } val admins = group.admins.map { it.serialize() } - // Users that are part of this remove update val updateMembers = kind.members.map { it.toByteArray().toHexString() } - // newMembers to save is old members minus removed members val newMembers = members + updateMembers storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - + // Send the latest encryption key pair to the added members if the current user is the admin of the group + val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!) + if (isCurrentUserAdmin) { + for (member in updateMembers) { + MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey) + } + } storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) } @@ -431,8 +444,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont val storage = MessagingConfiguration.shared.storage val senderPublicKey = message.sender ?: return val userPublicKey = storage.getUserPublicKey()!! - if (senderPublicKey == userPublicKey) { return } // Check the user leaving isn't us, will already be handled - val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return + if (message.kind!! !is ClosedGroupControlMessage.Kind.MemberLeft) return val groupPublicKey = message.groupPublicKey ?: return val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { @@ -462,12 +474,31 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) } +private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) { + val storage = MessagingConfiguration.shared.storage + val senderPublicKey = message.sender ?: return + val userPublicKey = storage.getUserPublicKey()!! + if (message.kind!! !is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest) return + if (senderPublicKey == userPublicKey) { + Log.d("Loki", "Ignoring invalid closed group update.") + return + } + val groupPublicKey = message.groupPublicKey ?: return + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } + MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey) +} + private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean { val oldMembers = group.members.map { it.serialize() } // Check that the message isn't from before the group was created - if (group.createdAt > sentTimestamp) { + if (group.formationTimestamp > sentTimestamp) { android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.") return false } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index eb9155eb47..f8840356ba 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -8,7 +8,9 @@ import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.NotifyPNServerJob import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ClosedGroupUpdate +import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.VisibleMessage @@ -86,7 +88,7 @@ object MessageSender { } // One-on-One Chats & Closed Groups - fun sendToSnodeDestination(destination: Destination, message: Message): Promise { + fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise { val deferred = deferred() val promise = deferred.promise val storage = MessagingConfiguration.shared.storage @@ -113,8 +115,13 @@ object MessageSender { } // Validate the message if (!message.isValid()) { throw Error.InvalidMessage } - // Stop here if this is a self-send - if (isSelfSend) { + // Stop here if this is a self-send, unless it's: + // • a configuration message + // • a sync message + // • a closed group control message of type `new` + var isNewClosedGroupControlMessage = false + if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true + if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage) { handleSuccessfulMessageSend(message, destination) deferred.resolve(Unit) return promise @@ -183,8 +190,8 @@ object MessageSender { if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) { //TODO Notify user for message sent } - handleSuccessfulMessageSend(message, destination) - var shouldNotify = (message is VisibleMessage) + handleSuccessfulMessageSend(message, destination, isSyncMessage) + var shouldNotify = (message is VisibleMessage && !isSyncMessage) if (message is ClosedGroupUpdate && message.kind is ClosedGroupUpdate.Kind.New) { shouldNotify = true } @@ -261,15 +268,28 @@ object MessageSender { } // Result Handling - fun handleSuccessfulMessageSend(message: Message, destination: Destination) { + fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) { val storage = MessagingConfiguration.shared.storage val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return + // Ignore future self-sends + storage.addReceivedMessageTimestamp(message.sentTimestamp!!) + // Track the open group server message ID if (message.openGroupServerMessageID != null) { storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!) } + // Mark the message as sent storage.markAsSent(messageId) storage.markUnidentified(messageId) + // Start the disappearing messages timer if needed SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(messageId) + // Sync the message if: + // • it's a visible message + // • the destination was a contact + // • we didn't sync it already + val userPublicKey = storage.getUserPublicKey()!! + if (destination is Destination.Contact && !isSyncMessage && message is VisibleMessage) { + sendToSnodeDestination(Destination.Contact(userPublicKey), message, true).get() + } } fun handleFailedMessageSend(message: Message, error: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt index 1f23e1fa3c..8299eb73c7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroup.kt @@ -2,32 +2,30 @@ package org.session.libsession.messaging.sending_receiving -import android.util.Log import com.google.protobuf.ByteString import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage -import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.threads.Address -import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.utilities.GroupUtil -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.Hex import org.session.libsignal.libsignal.ecc.Curve +import org.session.libsignal.libsignal.ecc.ECKeyPair +import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.internal.push.SignalServiceProtos -import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType -import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey -import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation -import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.ThreadUtils +import org.session.libsignal.utilities.logging.Log import java.util.* +import java.util.concurrent.ConcurrentHashMap + +private val pendingKeyPair = ConcurrentHashMap>() fun MessageSender.createClosedGroup(name: String, members: Collection): Promise { val deferred = deferred() @@ -46,12 +44,11 @@ fun MessageSender.createClosedGroup(name: String, members: Collection): val admins = setOf( userPublicKey ) val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) } storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) })) + null, null, LinkedList(admins.map { Address.fromSerialized(it) }), System.currentTimeMillis()) storage.setProfileSharing(Address.fromSerialized(groupID), true) // Send a closed group update message to all members individually val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData) for (member in members) { - if (member == userPublicKey) { continue } val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind) sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get() } @@ -160,7 +157,7 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List, name: String): Promise { - val deferred = deferred() - val context = MessagingConfiguration.shared.context - val storage = MessagingConfiguration.shared.storage - val userPublicKey = storage.getUserPublicKey()!! - val sskDatabase = MessagingConfiguration.shared.sskDatabase - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) // double encoded - val group = storage.getGroup(groupID) - if (group == null) { - Log.d("Loki", "Can't update nonexistent closed group.") - deferred.reject(Error.NoThread) - return deferred.promise - } - val oldMembers = group.members.map { it.serialize() }.toSet() - val newMembers = members.minus(oldMembers) - val membersAsData = members.map { Hex.fromStringCondensed(it) } - val admins = group.admins.map { it.serialize() } - val adminsAsData = admins.map { Hex.fromStringCondensed(it) } - val groupPrivateKey = sskDatabase.getClosedGroupPrivateKey(groupPublicKey) - if (groupPrivateKey == null) { - Log.d("Loki", "Couldn't get private key for closed group.") - deferred.reject(Error.NoPrivateKey) - return deferred.promise - } - val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() - val removedMembers = oldMembers.minus(members) - val isUserLeaving = removedMembers.contains(userPublicKey) - val newSenderKeys: List - if (wasAnyUserRemoved) { - if (isUserLeaving && removedMembers.count() != 1) { - Log.d("Loki", "Can't remove self and others simultaneously.") - deferred.reject(Error.InvalidClosedGroupUpdate) - return deferred.promise - } - // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) - val promises = oldMembers.map { member -> - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), - name, setOf(), membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(member) - MessageSender.sendNonDurably(closedGroupUpdate, address).get() - } - - val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - for (pair in allOldRatchets) { - val senderPublicKey = pair.first - val ratchet = pair.second - val collection = ClosedGroupRatchetCollectionType.Old - sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection) - } - // Delete all ratchets (it's important that this happens * after * sending out the update) - sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and - // send it out to all members (minus the removed ones) using established channels. - if (isUserLeaving) { - sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) - storage.setActive(groupID, false) - storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) - // Notify the PN server - PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) - } else { - // Send closed group update messages to any new members using established channels - for (member in newMembers) { - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, - Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(member) - MessageSender.sendNonDurably(closedGroupUpdate, address) - } - // Send out the user's new ratchet to all members (minus the removed ones) using established channels - val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) - val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) - for (member in members) { - if (member == userPublicKey) { continue } - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(member) - MessageSender.sendNonDurably(closedGroupUpdate, address) - } - } - } else if (newMembers.isNotEmpty()) { - // Generate ratchets for any new members - newSenderKeys = newMembers.map { publicKey -> - val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) - ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) - } - // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - newSenderKeys, membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(groupID) - MessageSender.send(closedGroupUpdate, address) - // Send closed group update messages to the new members using established channels - var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - allSenderKeys = allSenderKeys.union(newSenderKeys) - for (member in newMembers) { - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, - Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(member) - MessageSender.send(closedGroupUpdate, address) - } - } else { - val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey, ClosedGroupRatchetCollectionType.Current) - val closedGroupUpdateKind = ClosedGroupUpdate.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, - allSenderKeys, membersAsData, adminsAsData) - val closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - val address = Address.fromSerialized(groupID) - MessageSender.send(closedGroupUpdate, address) - } - // Update the group - storage.updateTitle(groupID, name) - if (!isUserLeaving) { - // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead - storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) - } - // Notify the user - val infoType = if (isUserLeaving) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE - val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) - storage.insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) - deferred.resolve(Unit) - return deferred.promise -} - -fun MessageSender.leave(groupPublicKey: String) { - val storage = MessagingConfiguration.shared.storage - val userPublicKey = storage.getUserPublicKey()!! - val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) // double encoded - val group = storage.getGroup(groupID) - if (group == null) { - Log.d("Loki", "Can't leave nonexistent closed group.") - return - } - val name = group.title - val oldMembers = group.members.map { it.serialize() }.toSet() - val newMembers = oldMembers.minus(userPublicKey) - return update(groupPublicKey, newMembers, name).get() -} - fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection) { // Prepare val storage = MessagingConfiguration.shared.storage @@ -372,6 +224,11 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta } // 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 val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) @@ -381,10 +238,54 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) } - val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(wrappers) + val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers) val closedGroupControlMessage = ClosedGroupControlMessage(kind) sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success { // Store it * after * having sent out the message to the group storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) + pendingKeyPair[groupPublicKey] = Optional.absent() } +} + +/// Note: Shouldn't currently be in use. +fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) { + val storage = MessagingConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't request encryption key pair for nonexistent closed group.") + throw Error.NoThread + } + val members = group.members.map { it.serialize() }.toSet() + if (!members.contains(storage.getUserPublicKey()!!)) return + // Send the request to the group + val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest) + send(closedGroupControlMessage, Address.fromSerialized(groupID)) +} + +fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) { + val storage = MessagingConfiguration.shared.storage + val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) + val group = storage.getGroup(groupID) ?: run { + Log.d("Loki", "Can't send encryption key pair for nonexistent closed group.") + throw Error.NoThread + } + val members = group.members.map { it.serialize() } + if (!members.contains(publicKey)) { + Log.d("Loki", "Refusing to send latest encryption key pair to non-member.") + return + } + // Get the latest encryption key pair + val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return + // Send it + val proto = SignalServiceProtos.KeyPair.newBuilder() + proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize()) + proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize()) + val plaintext = proto.build().toByteArray() + val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) + Log.d("Loki", "Sending latest encryption key pair to: $publicKey.") + val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) + val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper)) + val closedGroupControlMessage = ClosedGroupControlMessage(kind) + MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey)) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 748d2fdca5..f82aa532a1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -184,21 +184,13 @@ class OpenGroupPoller(private val openGroup: OpenGroup) { groupProto.setType(GroupContext.Type.DELIVER) groupProto.setName(openGroup.displayName) dataMessageProto.setGroup(groupProto.build()) + // Sync target + if (wasSentByCurrentUser) { + dataMessageProto.setSyncTarget(openGroup.id) + } // Content val content = Content.newBuilder() - if (!wasSentByCurrentUser) { // Incoming message - content.setDataMessage(dataMessageProto.build()) - } else { // Outgoing message - // FIXME: This needs to be updated as we removed sync message handling - val syncMessageSentBuilder = SyncMessage.Sent.newBuilder() - syncMessageSentBuilder.setMessage(dataMessageProto) - syncMessageSentBuilder.setDestination(userHexEncodedPublicKey) - syncMessageSentBuilder.setTimestamp(message.timestamp) - val syncMessageSent = syncMessageSentBuilder.build() - val syncMessageBuilder = SyncMessage.newBuilder() - syncMessageBuilder.setSent(syncMessageSent) - content.setSyncMessage(syncMessageBuilder.build()) - } + content.setDataMessage(dataMessageProto.build()) // Envelope val builder = Envelope.newBuilder() builder.type = Envelope.Type.UNIDENTIFIED_SENDER diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt index 28a64929f3..5e09d30ee5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/GroupRecord.kt @@ -9,7 +9,7 @@ class GroupRecord( val encodedId: String, val title: String, members: String?, val avatar: ByteArray?, val avatarId: Long?, val avatarKey: ByteArray?, val avatarContentType: String?, val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean, - val url: String?, admins: String?, val createdAt: Long + val url: String?, admins: String?, val formationTimestamp: Long ) { var members: List
= LinkedList
() var admins: List
= LinkedList
()