diff --git a/app/build.gradle b/app/build.gradle index 64b4078941..b83c5ae9c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -158,8 +158,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 121 -def canonicalVersionName = "1.6.4" +def canonicalVersionCode = 135 +def canonicalVersionName = "1.6.12" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, 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 3603b68f51..90dfedeea4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -195,7 +195,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } } - public void create(@NonNull String groupId, @Nullable String title, @NonNull List
members, + public long create(@NonNull String groupId, @Nullable String title, @NonNull List
members, @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List
admins) { Collections.sort(members); @@ -222,7 +222,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt contentValues.put(ADMINS, Address.Companion.toSerializedList(admins, ',')); } - databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); + long threadId = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); Recipient.applyCached(Address.Companion.fromSerialized(groupId), recipient -> { recipient.setName(title); @@ -231,6 +231,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt }); notifyConversationListListeners(); + return threadId; } public boolean delete(@NonNull String 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 7b84e9975d..2e6e2a9b3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -902,6 +902,17 @@ public class MmsDatabase extends MessagingDatabase { return insertMessageInbox(retrieved, contentLocation, threadId, type, 0); } + public Optional insertSecureDecryptedMessageOutbox(OutgoingMediaMessage retrieved, long threadId, long serverTimestamp) + throws MmsException + { + long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp); + if (messageId == -1) { + return Optional.absent(); + } + markAsSent(messageId, true); + return Optional.fromNullable(new InsertResult(messageId, threadId)); + } + public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId, long serverTimestamp) throws MmsException { 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 0ddf142866..159d961816 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -87,7 +87,7 @@ public class MmsSmsDatabase extends Database { } } - public @Nullable MessageRecord getMessageFor(long timestamp, Address author) { + public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { @@ -96,8 +96,8 @@ public class MmsSmsDatabase extends Database { MessageRecord messageRecord; while ((messageRecord = reader.getNext()) != null) { - if ((Util.isOwnNumber(context, author.serialize()) && messageRecord.isOutgoing()) || - (!Util.isOwnNumber(context, author.serialize()) && messageRecord.getIndividualRecipient().getAddress().equals(author))) + if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) || + (!Util.isOwnNumber(context, serializedAuthor) && messageRecord.getIndividualRecipient().getAddress().equals(serializedAuthor))) { return messageRecord; } @@ -107,6 +107,10 @@ public class MmsSmsDatabase extends Database { return null; } + public @Nullable MessageRecord getMessageFor(long timestamp, Address author) { + return getMessageFor(timestamp, author.serialize()); + } + public Cursor getConversation(long threadId, long offset, long limit) { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; 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 61a7af375e..0234dfae92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -686,6 +686,15 @@ public class SmsDatabase extends MessagingDatabase { return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp); } + public Optional insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) { + long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null); + if (messageId == -1) { + return Optional.absent(); + } + markAsSent(messageId, true); + return Optional.fromNullable(new InsertResult(messageId, threadId)); + } + public long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener) { @@ -718,7 +727,6 @@ public class SmsDatabase extends MessagingDatabase { SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); - if (insertListener != null) { insertListener.onComplete(); } 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 8fdcb3b100..30e886d4cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageSendJob -import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.opengroups.OpenGroup @@ -353,7 +352,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) 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 6d9ac5e29a..af11e476c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -194,7 +194,7 @@ public class GroupManager { avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null); } - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList()); + 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); 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 ab5dede3ea..e6474ce4b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.session.libsignal.utilities.logging.Log; @@ -123,11 +125,19 @@ public class GroupMessageProcessor { { GroupDatabase database = DatabaseFactory.getGroupDatabase(context); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); String id = GroupUtil.getEncodedId(group); + Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group)); + Recipient recipient = Recipient.from(context, address, false); String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); } + if (content.getSender().equals(userMasterDevice)) { + long threadId = threadDatabase.getThreadIdIfExistsFor(recipient); + return threadId == -1 ? null : threadId; + } + if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { // Loki - Only update the group if the group admin sent the message String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender()); @@ -260,9 +270,16 @@ public class GroupMessageProcessor { MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group)); Recipient recipient = Recipient.from(context, address, false); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, 0, null, Collections.emptyList(), Collections.emptyList()); long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + Address senderAddress = Address.Companion.fromExternal(context, content.getSender()); + MessageRecord existingMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), senderAddress); + long messageId; + if (existingMessage != null) { + messageId = existingMessage.getId(); + } else { + messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + } mmsDatabase.markAsSent(messageId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index e7f3c02a54..ca8bd7fdf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -47,7 +47,6 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.crypto.SecurityEvent; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -97,7 +96,6 @@ import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; -import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.session.libsignal.utilities.Hex; import org.session.libsignal.libsignal.InvalidMessageException; @@ -398,33 +396,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message) - { - SmsDatabase database = DatabaseFactory.getSmsDatabase(context); - Recipient recipient = getSyncMessageDestination(message); - OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1); - OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage); - - long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); - - if (!recipient.isGroupRecipient()) { - // TODO: Handle session reset on sync messages - /* - SessionStore sessionStore = new TextSecureSessionStore(context); - sessionStore.deleteAllSessions(recipient.getAddress().toPhoneString()); - */ - - SecurityEvent.broadcastSecurityUpdateEvent(context); - - long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage, - false, message.getTimestamp(), - null); - database.markAsSent(messageId, true); - } - - return threadId; - } - private void handleGroupMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId) @@ -483,98 +454,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private void handleSynchronizeStickerPackOperation(@NonNull List stickerPackOperations) { - JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); - - for (StickerPackOperationMessage operation : stickerPackOperations) { - if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) { - String packId = Hex.toStringCondensed(operation.getPackId().get()); - String packKey = Hex.toStringCondensed(operation.getPackKey().get()); - - switch (operation.getType().get()) { - case INSTALL: - jobManager.add(new StickerPackDownloadJob(packId, packKey, false)); - break; - case REMOVE: - DatabaseFactory.getStickerDatabase(context).uninstallPack(packId); - break; - } - } else { - Log.w(TAG, "Received incomplete sticker pack operation sync."); - } - } - } - - private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content, - @NonNull SentTranscriptMessage message) - throws StorageFailedException - - { - try { - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - - Long threadId; - - if (message.getMessage().isEndSession()) { - threadId = handleSynchronizeSentEndSessionMessage(message); - } else if (message.getMessage().isGroupUpdate()) { - threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true); - } else if (message.getMessage().isExpirationUpdate()) { - threadId = handleSynchronizeSentExpirationUpdate(message); - } else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) { - threadId = handleSynchronizeSentMediaMessage(message); - } else { - threadId = handleSynchronizeSentTextMessage(message); - } - - if (threadId == -1L) { threadId = null; } - - if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) { - handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); - } - - if (message.getMessage().getProfileKey().isPresent()) { - Recipient recipient = null; - - if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(message.getDestination().get()), false); - else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false); - - - if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); - } - - SessionMetaProtocol.handleProfileKeyUpdate(context, content); - } - - SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content); - - if (threadId != null) { - DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); - messageNotifier.updateNotification(context); - } - - messageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp()); - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); - } - } - public void handleMediaMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Optional messageServerIDOrNull) throws StorageFailedException { - Recipient originalRecipient = getMessageDestination(content, message); - Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + Recipient originalRecipient = getMessageDestination(content, message); + Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + String syncTarget = message.getSyncTarget().orNull(); + notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); - Optional quote = getValidatedQuote(message.getQuote()); - Optional> sharedContacts = getContacts(message.getSharedContacts()); - Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); - Optional sticker = getStickerAttachment(message.getSticker()); + Optional quote = getValidatedQuote(message.getQuote()); + Optional> sharedContacts = getContacts(message.getSharedContacts()); + Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); + Optional sticker = getStickerAttachment(message.getSticker()); Address masterAddress = masterRecipient.getAddress(); @@ -582,75 +478,140 @@ public class PushDecryptJob extends BaseJob implements InjectableType { masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); } - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, - message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), - quote, sharedContacts, linkPreviews, sticker); + // Handle sync message from ourselves + if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) { + Address targetAddress = masterRecipient.getAddress(); + if (message.getGroupInfo().isPresent()) { + targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())); + } else if (syncTarget != null && !syncTarget.isEmpty()) { + targetAddress = Address.fromSerialized(syncTarget); + } + List attachments = PointerAttachment.forPointers(message.getAttachments()); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - database.beginTransaction(); + OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(), + attachments, + message.getTimestamp(), -1, + message.getExpiresInSeconds() * 1000, + ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(), + sharedContacts.or(Collections.emptyList()), + linkPreviews.or(Collections.emptyList()), + Collections.emptyList(), Collections.emptyList()); - // Ignore message if it has no body and no attachments - if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { - return; - } + if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) { + Log.d("Loki","Message already exists, don't insert again"); + return; + } - Optional insertResult; + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + database.beginTransaction(); - try { - if (message.isGroupMessage()) { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); - } else { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); + // Ignore message if it has no body and no attachments + if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { + return; + } + + Optional insertResult; + + try { + + // Check if we have the thread already + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize()); + + insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp()); + + if (insertResult.isPresent()) { + List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); + List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); + List dbAttachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); + + forceStickerDownloadIfNecessary(stickerAttachments); + + for (DatabaseAttachment attachment : dbAttachments) { + ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); + } + + if (smsMessageId.isPresent()) { + DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); + } + + database.setTransactionSuccessful(); + } + } catch (MmsException e) { + throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); + } finally { + database.endTransaction(); + } + + } else { + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, + message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), + quote, sharedContacts, linkPreviews, sticker); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + database.beginTransaction(); + + // Ignore message if it has no body and no attachments + if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { + return; + } + + Optional insertResult; + + try { + if (message.isGroupMessage()) { + insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); + } else { + insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); + } + + if (insertResult.isPresent()) { + List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); + List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); + List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); + + forceStickerDownloadIfNecessary(stickerAttachments); + + for (DatabaseAttachment attachment : attachments) { + ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); + } + + if (smsMessageId.isPresent()) { + DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); + } + + database.setTransactionSuccessful(); + } + } catch (MmsException e) { + throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); + } finally { + database.endTransaction(); } if (insertResult.isPresent()) { - List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); - List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); - List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); - - forceStickerDownloadIfNecessary(stickerAttachments); - - for (DatabaseAttachment attachment : attachments) { - ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); - } - - if (smsMessageId.isPresent()) { - DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); - } - - database.setTransactionSuccessful(); - } - } catch (MmsException e) { - throw new StorageFailedException(e, content.getSender(), content.getSenderDevice()); - } finally { - database.endTransaction(); - } - - if (insertResult.isPresent()) { - messageNotifier.updateNotification(context, insertResult.get().getThreadId()); - } - - if (insertResult.isPresent()) { - InsertResult result = insertResult.get(); - - // Loki - Cache the user hex encoded public key (for mentions) - MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); - MentionsManager.shared.cache(content.getSender(), result.getThreadId()); - - // Loki - Store message open group server ID if needed - if (messageServerIDOrNull.isPresent()) { - long messageID = result.getMessageId(); - long messageServerID = messageServerIDOrNull.get(); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - lokiMessageDatabase.setServerID(messageID, messageServerID); + messageNotifier.updateNotification(context, insertResult.get().getThreadId()); } - // Loki - Update mapping of message ID to original thread ID - if (result.getMessageId() > -1) { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); - lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); + if (insertResult.isPresent()) { + InsertResult result = insertResult.get(); + + // Loki - Cache the user hex encoded public key (for mentions) + MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); + MentionsManager.shared.cache(content.getSender(), result.getThreadId()); + + // Loki - Store message open group server ID if needed + if (messageServerIDOrNull.isPresent()) { + long messageID = result.getMessageId(); + long messageServerID = messageServerIDOrNull.get(); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + lokiMessageDatabase.setServerID(messageID, messageServerID); + } + + // Loki - Update mapping of message ID to original thread ID + if (result.getMessageId() > -1) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); + lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); + } } } } @@ -769,6 +730,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { String body = message.getBody().isPresent() ? message.getBody().get() : ""; Recipient originalRecipient = getMessageDestination(content, message); Recipient masterRecipient = getMessageMasterDestination(content.getSender()); + String syncTarget = message.getSyncTarget().orNull(); if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) { handleExpirationUpdate(content, message, Optional.absent()); @@ -778,15 +740,56 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; + } else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) { + Address targetAddress = masterRecipient.getAddress(); + if (message.getGroupInfo().isPresent()) { + targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())); + } else if (syncTarget != null && !syncTarget.isEmpty()) { + targetAddress = Address.fromSerialized(syncTarget); + } + + if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) { + Log.d("Loki","Message already exists, don't insert again"); + return; + } + + OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false), + body, message.getExpiresInSeconds(), -1); + + // Ignore the message if it has no body + if (tm.getMessageBody().length() == 0) { return; } + + // Check if we have the thread already + long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize()); + + + // Insert the message into the database + Optional insertResult; + insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp()); + + if (insertResult.isPresent()) { + threadId = insertResult.get().getThreadId(); + } + + if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); + + if (threadId != null) { + messageNotifier.updateNotification(context, threadId); + } + + if (insertResult.isPresent()) { + InsertResult result = insertResult.get(); + + // Loki - Cache the user hex encoded public key (for mentions) + MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context); + MentionsManager.shared.cache(content.getSender(), result.getThreadId()); + } + } else { notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); Address masterAddress = masterRecipient.getAddress(); - if (message.isGroupMessage()) { - masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); - } - IncomingTextMessage tm = new IncomingTextMessage(masterAddress, content.getSenderDevice(), message.getTimestamp(), body, @@ -1319,6 +1322,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { return true; } + if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) && + DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) { + Log.d("Loki", "Skipping message from self we already have"); + return true; + } + Recipient sender = Recipient.from(context, Address.Companion.fromSerialized(content.getSender()), false); if (content.getDeviceLink().isPresent()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 1c7df45f52..e5ef36cb0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -15,6 +15,7 @@ import org.session.libsession.messaging.threads.recipients.Recipient; import org.session.libsession.messaging.threads.Address; import org.session.libsession.utilities.GroupUtil; +import org.session.libsession.utilities.TextSecurePreferences; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -143,6 +144,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { List existingNetworkFailures = message.getNetworkFailures(); List existingIdentityMismatches = message.getIdentityKeyMismatches(); + String userPublicKey = TextSecurePreferences.getLocalNumber(context); + SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); + if (database.isSent(messageId)) { log(TAG, "Message " + messageId + " was already sent. Ignoring."); return; @@ -238,25 +242,18 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { // return results; // } - String groupId = address.toGroupString(); - Optional profileKey = getProfileKey(message.getRecipient()); - Optional quote = getQuoteFor(message); - Optional sticker = getStickerFor(message); - List sharedContacts = getSharedContactsFor(message); - List previews = getPreviewsFor(message); List addresses = Stream.of(destinations).map(this::getPushAddress).toList(); - List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); - List attachmentPointers = getAttachmentPointersFor(attachments); - List> unidentifiedAccess = Stream.of(addresses) .map(a -> Address.Companion.fromSerialized(a.getNumber())) .map(a -> Recipient.from(context, a, false)) .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) .toList(); - SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; - if (message.isGroup() && address.isClosedGroup()) { + SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; + String groupId = address.toGroupString(); + List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); + List attachmentPointers = getAttachmentPointersFor(attachments); // Loki - Only send GroupUpdate or GroupQuit messages to closed groups OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; GroupContext groupContext = groupMessage.getGroupContext(); @@ -271,25 +268,40 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage); } else { - SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType); - SignalServiceDataMessage groupMessage = 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()) - .withSticker(sticker.orNull()) - .withSharedContacts(sharedContacts) - .withPreviews(previews) - .build(); + SignalServiceDataMessage groupMessage = getDataMessage(address, message).build(); return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage); } } + public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) throws IOException { + + SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL; + + String groupId = address.toGroupString(); + Optional profileKey = getProfileKey(message.getRecipient()); + Optional quote = getQuoteFor(message); + Optional sticker = getStickerFor(message); + List sharedContacts = getSharedContactsFor(message); + List previews = getPreviewsFor(message); + List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); + List 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()) + .withSticker(sticker.orNull()) + .withSharedContacts(sharedContacts) + .withPreviews(previews); + } + public static class Factory implements Job.Factory { @Override public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 19254a7d74..c89b44f131 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -245,7 +245,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { { try { Recipient recipient = Recipient.from(context, destination, false); + String userPublicKey = TextSecurePreferences.getLocalNumber(context); SignalServiceAddress address = getPushAddress(recipient.getAddress()); + SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey); List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List serviceAttachments = getAttachmentPointersFor(attachments); Optional profileKey = getProfileKey(message.getRecipient()); @@ -254,6 +256,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { List sharedContacts = getSharedContactsFor(message); List previews = getPreviewsFor(message); + Optional unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient); + SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() .withBody(message.getBody()) .withAttachments(serviceAttachments) @@ -267,6 +271,20 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { .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()) + .withSticker(sticker.orNull()) + .withSharedContacts(sharedContacts) + .withPreviews(previews) + .asExpirationUpdate(message.isExpirationUpdate()) + .build(); + if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); @@ -275,11 +293,24 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { messageSender.sendMessage(syncMessage, syncAccess); return syncAccess.isPresent(); } else { - SendMessageResult result = messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage); + SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage); if (result.getLokiAPIError() != null) { throw result.getLokiAPIError(); } else { - return result.getSuccess().isUnidentified(); + boolean isUnidentified = result.getSuccess().isUnidentified(); + + try { + // send to ourselves to sync multi-device + Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); + SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage); + if (selfSendResult.getLokiAPIError() != null) { + throw selfSendResult.getLokiAPIError(); + } + } catch (Exception e) { + Log.e("Loki", "Error sending message to ourselves", e); + } + + return isUnidentified; } } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 00789a08ef..acac2e3d98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -3,6 +3,7 @@ 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; @@ -192,8 +193,10 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { throws UntrustedIdentityException, InsecureFallbackApprovalException, 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 profileKey = getProfileKey(recipient); Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); @@ -205,13 +208,21 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { // } SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getDateSent()) - .withBody(message.getBody()) - .withExpiration((int)(message.getExpiresIn() / 1000)) - .withProfileKey(profileKey.orNull()) -// .withPreKeyBundle(preKeyBundle) - .asEndSessionMessage(message.isEndSession()) - .build(); + .withTimestamp(message.getDateSent()) + .withBody(message.getBody()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .asEndSessionMessage(message.isEndSession()) + .build(); + + SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(message.getDateSent()) + .withBody(message.getBody()) + .withSyncTarget(destination.serialize()) + .withExpiration((int)(message.getExpiresIn() / 1000)) + .withProfileKey(profileKey.orNull()) + .asEndSessionMessage(message.isEndSession()) + .build(); if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { // Loki - Device link messages don't go through here @@ -225,7 +236,19 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { if (result.getLokiAPIError() != null) { throw result.getLokiAPIError(); } else { - return result.getSuccess().isUnidentified(); + boolean isUnidentified = result.getSuccess().isUnidentified(); + + try { + // send to ourselves to sync multi-device + Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); + SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage); + if (selfSendResult.getLokiAPIError() != null) { + throw selfSendResult.getLokiAPIError(); + } + } catch (Exception e) { + Log.e("Loki", "Error sending message to ourselves", e); + } + return isUnidentified; } } } catch (UnregisteredUserException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt index 8a8c18565d..e3c5b0aa6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt @@ -18,10 +18,11 @@ import org.thoughtcrime.securesms.loki.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 sodium = LazySodiumAndroid(SodiumAndroid()) val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey val signature = ByteArray(Sign.BYTES) @@ -47,7 +48,6 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol { val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey") - val sodium = LazySodiumAndroid(SodiumAndroid()) val signatureSize = Sign.BYTES val ed25519PublicKeySize = Sign.PUBLICKEYBYTES diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt index 108783a51f..66c1907405 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiMessageDatabase.kt @@ -29,7 +29,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab } override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? { - val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteePublicKey)) + val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey) return if (message != null) getServerID(message.getId()) else null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt index 7f24078034..28f4a5a864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol import com.google.protobuf.ByteString import org.session.libsession.messaging.jobs.Data +import org.session.libsignal.libsignal.util.guava.Optional import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.jobmanager.Job @@ -128,7 +129,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters // isClosedGroup can always be false as it's only used in the context of legacy closed groups messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, Date().time, serializedContentMessage, false, ttl, false, - useFallbackEncryption, false, false, false) + useFallbackEncryption, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt index 3fcb203e74..6b4060aa17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJobV2.kt @@ -9,6 +9,7 @@ import org.session.libsession.messaging.jobs.Data 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.loki.protocol.meta.TTLUtilities @@ -26,7 +27,7 @@ import org.session.libsignal.utilities.Hex import java.util.* import java.util.concurrent.TimeUnit -class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { +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, val admins: Collection) : Kind() @@ -60,20 +61,22 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } } - constructor(destination: String, kind: Kind) : this(Parameters.Builder() + 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) + 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") @@ -123,6 +126,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete 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" -> { @@ -161,7 +165,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete } else -> throw Exception("Invalid closed group update message kind: $rawKind.") } - return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind) + return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime) } } @@ -220,8 +224,8 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete try { // isClosedGroup can always be false as it's only used in the context of legacy closed groups messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, - Date().time, serializedContentMessage, false, ttl, false, - true, false, false, false) + sentTime, serializedContentMessage, false, ttl, false, + true, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 3dd347472c..6c47269505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -285,7 +285,7 @@ object ClosedGroupsProtocol { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) mmsDB.markAsSent(infoMessageID, true) @@ -324,6 +324,6 @@ object ClosedGroupsProtocol { .setId(decodedGroupId) .setType(GroupContext.Type.QUIT) .build() - return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, emptyList(), emptyList()) + return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, 0, null, emptyList(), emptyList()) } } \ No newline at end of file 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 1adc93d44d..7395fd2b9c 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 @@ -58,6 +58,7 @@ object ClosedGroupsProtocolV2 { 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 @@ -68,20 +69,20 @@ object ClosedGroupsProtocolV2 { null, null, LinkedList(admins.map { Address.fromSerialized(it!!) })) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) // Send a closed group update message to all members individually - val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) - for (member in members) { - if (member == userPublicKey) { continue } - val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind) - job.setContext(context) - job.onRun() // Run the job immediately to make all of this sync - } // 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) + 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 @@ -103,13 +104,14 @@ object ClosedGroupsProtocolV2 { 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) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Remove the group private key and unsubscribe from PNs @@ -117,7 +119,7 @@ object ClosedGroupsProtocolV2 { // Notify the user val infoType = GroupContext.Type.QUIT val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -141,6 +143,7 @@ object ClosedGroupsProtocolV2 { 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 = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") @@ -149,7 +152,7 @@ object ClosedGroupsProtocolV2 { val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Send closed group update messages to any new members individually @@ -157,13 +160,13 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") - val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind) + val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime) ApplicationContext.getInstance(context).jobManager.add(newMemberJob) } // 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) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } } @@ -184,6 +187,7 @@ object ClosedGroupsProtocolV2 { groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) 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.") @@ -196,7 +200,7 @@ object ClosedGroupsProtocolV2 { val name = group.title // Send the update to the group val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately val isCurrentUserAdmin = admins.contains(userPublicKey) @@ -206,7 +210,7 @@ object ClosedGroupsProtocolV2 { // 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) + insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime) } } @@ -219,13 +223,14 @@ object ClosedGroupsProtocolV2 { 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.") return@queue deferred.reject(Error.NoThread) } // Send the update to the group val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime) job.setContext(context) job.onRun() // Run the job immediately // Update the group @@ -233,7 +238,7 @@ object ClosedGroupsProtocolV2 { // 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) + insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -273,6 +278,7 @@ object ClosedGroupsProtocolV2 { Log.d("Loki", "Can't update nonexistent closed group.") return@queue deferred.reject(Error.NoThread) } + val sentTime = System.currentTimeMillis() val oldMembers = group.members.map { it.serialize() }.toSet() val newMembers = members.minus(oldMembers) val membersAsData = members.map { Hex.fromStringCondensed(it) } @@ -299,7 +305,7 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData) @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime) job.setContext(context) job.onRun() // Run the job immediately if (isUserLeaving) { @@ -323,7 +329,7 @@ object ClosedGroupsProtocolV2 { @Suppress("NAME_SHADOWING") val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) @Suppress("NAME_SHADOWING") - val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind) + val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime) ApplicationContext.getInstance(context).jobManager.add(job) } } @@ -336,7 +342,7 @@ object ClosedGroupsProtocolV2 { // Notify the user val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) - insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) + insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID, sentTime) deferred.resolve(Unit) } return deferred.promise @@ -368,7 +374,7 @@ object ClosedGroupsProtocolV2 { val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) } - val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers)) + val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis()) job.setContext(context) job.onRun() // Run the job immediately // Store it * after * having sent out the message to the group @@ -377,9 +383,9 @@ object ClosedGroupsProtocolV2 { @JvmStatic fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { - if (!isValid(closedGroupUpdate, senderPublicKey)) { return } + if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return } when (closedGroupUpdate.type) { - SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) @@ -392,7 +398,10 @@ object ClosedGroupsProtocolV2 { } } - private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean { + private fun isValid(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long): Boolean { + val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey) + if (record != null) return false + return when (closedGroupUpdate.type) { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> { (!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty @@ -414,7 +423,7 @@ object ClosedGroupsProtocolV2 { } } - public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) { + public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long) { // Prepare val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val apiDB = DatabaseFactory.getLokiAPIDatabase(context) @@ -427,7 +436,8 @@ object ClosedGroupsProtocolV2 { // Create the group val groupID = doubleEncodeGroupID(groupPublicKey) val groupDB = DatabaseFactory.getGroupDatabase(context) - if (groupDB.getGroup(groupID).orNull() != null) { + val prevGroup = groupDB.getGroup(groupID).orNull() + if (prevGroup != null) { // Update the group groupDB.updateTitle(groupID, name) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) @@ -441,8 +451,14 @@ object ClosedGroupsProtocolV2 { // Store the encryption key pair val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) - // Notify the user - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + // 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) + } 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) + } // Notify the PN server LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) } @@ -452,8 +468,8 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! @@ -492,16 +508,21 @@ object ClosedGroupsProtocolV2 { val (contextType, signalType) = if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE - - insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) + } } fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { @@ -518,16 +539,22 @@ object ClosedGroupsProtocolV2 { val newMembers = members + updateMembers groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + 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) + } } fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Check that the sender is a member of the group (before the update) + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } // Check common group update logic @@ -539,21 +566,23 @@ object ClosedGroupsProtocolV2 { val name = closedGroupUpdate.name groupDB.updateTitle(groupID, name) - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + 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) + } } private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { // Check the user leaving isn't us, will already be handled val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! - if (senderPublicKey == userPublicKey) { - return - } val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val name = group.title @@ -576,7 +605,12 @@ object ClosedGroupsProtocolV2 { generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList) } } - insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) + if (userPublicKey == senderPublicKey) { + val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID) + 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) + } } private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { @@ -589,8 +623,8 @@ object ClosedGroupsProtocolV2 { val groupDB = DatabaseFactory.getGroupDatabase(context) val groupID = doubleEncodeGroupID(groupPublicKey) val group = groupDB.getGroup(groupID).orNull() - if (group == null) { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + if (group == null || !group.isActive) { + Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.") return } val oldMembers = group.members.map { it.serialize() } @@ -624,7 +658,13 @@ object ClosedGroupsProtocolV2 { val wasSenderRemoved = !members.contains(senderPublicKey) val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE - insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }) + 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) + } else { + insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins) + } } private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { @@ -700,7 +740,8 @@ object ClosedGroupsProtocolV2 { } private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, - members: Collection, admins: Collection, threadID: Long) { + members: Collection, admins: Collection, threadID: Long, + sentTime: Long) { val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val groupContextBuilder = GroupContext.newBuilder() .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) @@ -708,9 +749,9 @@ object ClosedGroupsProtocolV2 { .setName(name) .addAllMembers(members) .addAllAdmins(admins) - val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf()) val mmsDB = DatabaseFactory.getMmsDatabase(context) - val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime) mmsDB.markAsSent(infoMessageID, true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt index 834c01ea23..9f0eff11c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.BaseJob import org.session.libsignal.utilities.logging.Log import org.session.libsession.messaging.threads.recipients.Recipient +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.loki.protocol.meta.TTLUtilities @@ -56,7 +57,7 @@ class NullMessageSendJob private constructor(parameters: Parameters, private val try { messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, Date().time, serializedContentMessage, false, ttl, false, - false, false, false, false) + false, false, false, Optional.absent()) } catch (e: Exception) { Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.") throw e diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt index e965beeb11..8acd255597 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/KeyPairUtilities.kt @@ -14,7 +14,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair object KeyPairUtilities { - private val sodium = LazySodiumAndroid(SodiumAndroid()) + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } fun generate(): KeyPairGenerationResult { val seed = sodium.randomBytesBuf(16) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index d34a14909c..4b1fbfb257 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -40,7 +40,6 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { public OutgoingGroupMediaMessage(@NonNull Recipient recipient, @NonNull GroupContext group, @Nullable final Attachment avatar, - long sentTimeMillis, long expireIn, @Nullable QuoteModel quote, @NonNull List contacts, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 680c9ac684..1ea22350a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -326,8 +326,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { String publicKey = recipient.getAddress().serialize(); - String hepk = (recipient.isLocalNumber() && publicKey != null) - ? TextSecurePreferences.getLocalNumber(context) + String hepk = (recipient.isLocalNumber() && publicKey == null) + ? TextSecurePreferences.getMasterHexEncodedPublicKey(context) : publicKey; String displayName = recipient.getName(); return AvatarPlaceholderGenerator.generate(context, 128, hepk, displayName); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java index 63489bf7bc..684329fc77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEncryptedMessage.java @@ -6,12 +6,7 @@ public class IncomingEncryptedMessage extends IncomingTextMessage { super(base, newBody); } - @Override - public IncomingTextMessage withMessageBody(String body) { - return new IncomingEncryptedMessage(this, body); - } - - @Override + @Override public boolean isSecureMessage() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java index 9277e989a7..f5b7b5e402 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingEndSessionMessage.java @@ -10,12 +10,7 @@ public class IncomingEndSessionMessage extends IncomingTextMessage { super(base, newBody); } - @Override - public IncomingEndSessionMessage withMessageBody(String messageBody) { - return new IncomingEndSessionMessage(this, messageBody); - } - - @Override + @Override public boolean isEndSession() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java index 546881ceba..af17d4b963 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java @@ -11,12 +11,7 @@ public class IncomingGroupMessage extends IncomingTextMessage { this.groupContext = groupContext; } - @Override - public IncomingGroupMessage withMessageBody(String body) { - return new IncomingGroupMessage(this, groupContext, body); - } - - @Override + @Override public boolean isGroup() { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java index 1910c366f4..39f75a5da0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingPreKeyBundleMessage.java @@ -9,12 +9,7 @@ public class IncomingPreKeyBundleMessage extends IncomingTextMessage { this.legacy = legacy; } - @Override - public IncomingPreKeyBundleMessage withMessageBody(String messageBody) { - return new IncomingPreKeyBundleMessage(this, messageBody, legacy); - } - - @Override + @Override public boolean isLegacyPreKeyBundle() { return legacy; } 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 36f01071d9..eaf06f80d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -175,10 +175,6 @@ public class IncomingTextMessage implements Parcelable { return message; } - public IncomingTextMessage withMessageBody(String message) { - return new IncomingTextMessage(this, message); - } - public Address getSender() { return sender; } @@ -250,7 +246,6 @@ public class IncomingTextMessage implements Parcelable { public boolean isUnidentified() { return unidentified; } - @Override public int describeContents() { return 0; diff --git a/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt index 72291a8a2c..3b2556272a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/threads/Address.kt @@ -152,6 +152,7 @@ class Address private constructor(address: String) : Parcelable, Comparable>() + @JvmStatic fun fromSerialized(serialized: String): Address { return Address(serialized) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt index 671189778f..56427bfb62 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt @@ -13,7 +13,7 @@ import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair object UnidentifiedAccessUtil { private val TAG = UnidentifiedAccessUtil::class.simpleName - private val sodium = LazySodiumAndroid(SodiumAndroid()) + private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? { try { diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index ea9866822c..17aad994ea 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -60,10 +60,12 @@ object GroupUtil { return groupId.startsWith(MMS_GROUP_PREFIX) } + @JvmStatic fun isOpenGroup(groupId: String): Boolean { return groupId.startsWith(OPEN_GROUP_PREFIX) } + @JvmStatic fun isClosedGroup(groupId: String): Boolean { return groupId.startsWith(CLOSED_GROUP_PREFIX) } diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java index bd23c1f91f..1b854b97a4 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/SignalServiceMessageSender.java @@ -26,10 +26,6 @@ import org.session.libsignal.service.api.messages.SignalServiceDataMessage; import org.session.libsignal.service.api.messages.SignalServiceGroup; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; -import org.session.libsignal.service.api.messages.calls.AnswerMessage; -import org.session.libsignal.service.api.messages.calls.IceUpdateMessage; -import org.session.libsignal.service.api.messages.calls.OfferMessage; -import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage; import org.session.libsignal.service.api.messages.multidevice.BlockedListMessage; import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage; import org.session.libsignal.service.api.messages.multidevice.ReadMessage; @@ -51,7 +47,6 @@ import org.session.libsignal.service.internal.push.PushServiceSocket; import org.session.libsignal.service.internal.push.PushTransportDetails; import org.session.libsignal.service.internal.push.SignalServiceProtos; import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer; -import org.session.libsignal.service.internal.push.SignalServiceProtos.CallMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.Content; import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; @@ -217,34 +212,14 @@ public class SignalServiceMessageSender { * @param recipient The sender of the received message you're acknowledging. * @param message The read receipt to deliver. * @throws IOException - * @throws UntrustedIdentityException */ public void sendReceipt(SignalServiceAddress recipient, Optional unidentifiedAccess, SignalServiceReceiptMessage message) - throws IOException, UntrustedIdentityException - { + throws IOException { byte[] content = createReceiptContent(message); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption, false); - } - - /** - * Send a typing indicator. - * - * @param recipient The destination - * @param message The typing indicator to deliver - * @throws IOException - * @throws UntrustedIdentityException - */ - public void sendTyping(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceTypingMessage message) - throws IOException, UntrustedIdentityException - { - byte[] content = createTypingContent(message); - boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), useFallbackEncryption, false); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption); } public void sendTyping(List recipients, @@ -256,42 +231,24 @@ public class SignalServiceMessageSender { sendMessage(0, recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), false, false); } - /** - * Send a call setup message to a single recipient. - * - * @param recipient The message's destination. - * @param message The call message. - * @throws IOException - */ - public void sendCallMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceCallMessage message) - throws IOException, UntrustedIdentityException - { - byte[] content = createCallContent(message); - boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, message.getTTL(), useFallbackEncryption, false); - } - /** * Send a message to a single recipient. * * @param recipient The message's destination. * @param message The message. - * @throws UntrustedIdentityException * @throws IOException */ public SendMessageResult sendMessage(long messageID, SignalServiceAddress recipient, Optional unidentifiedAccess, SignalServiceDataMessage message) - throws UntrustedIdentityException, IOException + throws IOException { byte[] content = createMessageContent(message, recipient); long timestamp = message.getTimestamp(); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL; - SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, false, message.hasVisibleContent()); + SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, message.hasVisibleContent(), message.getSyncTarget()); // // Loki - This shouldn't get invoked for note to self // boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent(); @@ -325,8 +282,7 @@ public class SignalServiceMessageSender { List recipients, List> unidentifiedAccess, SignalServiceDataMessage message) - throws IOException, UntrustedIdentityException - { + throws IOException { // Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine // whether an attachment is being sent to an open group or not. byte[] content = createMessageContent(message, recipients.get(0)); @@ -350,7 +306,7 @@ public class SignalServiceMessageSender { for (String device : linkedDevices) { SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(syncMessage, device, store); - sendMessage(deviceAsAddress, Optional.absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption, true); + sendMessage(deviceAsAddress, Optional.absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption); } } @@ -391,18 +347,10 @@ public class SignalServiceMessageSender { for (String device : linkedDevices) { SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store); - sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); + // sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); } } - public void setSoTimeoutMillis(long soTimeoutMillis) { - socket.setSoTimeoutMillis(soTimeoutMillis); - } - - public void cancelInFlightRequests() { - socket.cancelInFlightRequests(); - } - public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) { this.pipe.set(Optional.fromNullable(pipe)); this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe)); @@ -487,32 +435,6 @@ public class SignalServiceMessageSender { { Content.Builder container = Content.newBuilder(); -// if (message.getPreKeyBundle().isPresent()) { -// PreKeyBundle preKeyBundle = message.getPreKeyBundle().get(); -// PreKeyBundleMessage.Builder preKeyBundleMessageBuilder = PreKeyBundleMessage.newBuilder() -// .setDeviceId(preKeyBundle.getDeviceId()) -// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize())) -// .setPreKeyId(preKeyBundle.getPreKeyId()) -// .setPreKey(ByteString.copyFrom(preKeyBundle.getPreKey().serialize())) -// .setSignedKeyId(preKeyBundle.getSignedPreKeyId()) -// .setSignedKey(ByteString.copyFrom(preKeyBundle.getSignedPreKey().serialize())) -// .setSignature(ByteString.copyFrom(preKeyBundle.getSignedPreKeySignature())) -// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize())); -// container.setPreKeyBundleMessage(preKeyBundleMessageBuilder); -// } - -// if (message.getDeviceLink().isPresent()) { -// DeviceLink deviceLink = message.getDeviceLink().get(); -// SignalServiceProtos.DeviceLinkMessage.Builder deviceLinkMessageBuilder = SignalServiceProtos.DeviceLinkMessage.newBuilder() -// .setPrimaryPublicKey(deviceLink.getMasterPublicKey()) -// .setSecondaryPublicKey(deviceLink.getSlavePublicKey()) -// .setRequestSignature(ByteString.copyFrom(Objects.requireNonNull(deviceLink.getRequestSignature()))); -// if (deviceLink.getAuthorizationSignature() != null) { -// deviceLinkMessageBuilder.setAuthorizationSignature(ByteString.copyFrom(deviceLink.getAuthorizationSignature())); -// } -// container.setDeviceLinkMessage(deviceLinkMessageBuilder.build()); -// } - DataMessage.Builder builder = DataMessage.newBuilder(); List pointers = createAttachmentPointers(message.getAttachments(), recipient); @@ -552,6 +474,10 @@ public class SignalServiceMessageSender { builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get())); } + if (message.getSyncTarget().isPresent()) { + builder.setSyncTarget(message.getSyncTarget().get()); + } + if (message.getQuote().isPresent()) { DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder() .setId(message.getQuote().get().getId()) @@ -629,40 +555,6 @@ public class SignalServiceMessageSender { return container.build().toByteArray(); } - private byte[] createCallContent(SignalServiceCallMessage callMessage) { - Content.Builder container = Content.newBuilder(); - CallMessage.Builder builder = CallMessage.newBuilder(); - - if (callMessage.getOfferMessage().isPresent()) { - OfferMessage offer = callMessage.getOfferMessage().get(); - builder.setOffer(CallMessage.Offer.newBuilder() - .setId(offer.getId()) - .setDescription(offer.getDescription())); - } else if (callMessage.getAnswerMessage().isPresent()) { - AnswerMessage answer = callMessage.getAnswerMessage().get(); - builder.setAnswer(CallMessage.Answer.newBuilder() - .setId(answer.getId()) - .setDescription(answer.getDescription())); - } else if (callMessage.getIceUpdateMessages().isPresent()) { - List updates = callMessage.getIceUpdateMessages().get(); - - for (IceUpdateMessage update : updates) { - builder.addIceUpdate(CallMessage.IceUpdate.newBuilder() - .setId(update.getId()) - .setSdp(update.getSdp()) - .setSdpMid(update.getSdpMid()) - .setSdpMLineIndex(update.getSdpMLineIndex())); - } - } else if (callMessage.getHangupMessage().isPresent()) { - builder.setHangup(CallMessage.Hangup.newBuilder().setId(callMessage.getHangupMessage().get().getId())); - } else if (callMessage.getBusyMessage().isPresent()) { - builder.setBusy(CallMessage.Busy.newBuilder().setId(callMessage.getBusyMessage().get().getId())); - } - - container.setCallMessage(builder); - return container.build().toByteArray(); - } - private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException { @@ -980,6 +872,7 @@ public class SignalServiceMessageSender { throws IOException { List results = new LinkedList<>(); + SignalServiceAddress ownAddress = localAddress; Iterator recipientIterator = recipients.iterator(); Iterator> unidentifiedAccessIterator = unidentifiedAccess.iterator(); @@ -988,7 +881,7 @@ public class SignalServiceMessageSender { try { boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(content, recipient.getNumber(), store); - SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, false, notifyPNServer); + SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, notifyPNServer, Optional.absent()); results.add(result); } catch (UnregisteredUserException e) { Log.w(TAG, e); @@ -1002,41 +895,46 @@ public class SignalServiceMessageSender { return results; } - private SendMessageResult sendMessage(SignalServiceAddress recipient, + private SendMessageResult sendMessage(SignalServiceAddress recipient, Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean useFallbackEncryption, - boolean isSyncMessage) + long timestamp, + byte[] content, + boolean online, + int ttl, + boolean useFallbackEncryption) throws IOException { // Loki - This method is only invoked for various types of control messages - return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, isSyncMessage, false); + return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, false,Optional.absent()); } - public SendMessageResult sendMessage(final long messageID, - final SignalServiceAddress recipient, + public SendMessageResult sendMessage(final long messageID, + final SignalServiceAddress recipient, Optional unidentifiedAccess, - long timestamp, - byte[] content, - boolean online, - int ttl, - boolean isDeviceLinkMessage, - boolean useFallbackEncryption, - boolean isClosedGroup, - boolean isSyncMessage, - boolean notifyPNServer) + long timestamp, + byte[] content, + boolean online, + int ttl, + boolean isDeviceLinkMessage, + boolean useFallbackEncryption, + boolean isClosedGroup, + boolean notifyPNServer, + Optional syncTarget) throws IOException { - long threadID = threadDatabase.getThreadID(recipient.getNumber()); + boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty(); + long threadID; + if (isSelfSend) { + threadID = threadDatabase.getThreadID(syncTarget.get()); + } else { + threadID = threadDatabase.getThreadID(recipient.getNumber()); + } PublicChat publicChat = threadDatabase.getPublicChat(threadID); try { if (publicChat != null) { return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat); } else { - return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer); + return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget); } } catch (PushNetworkException e) { return SendMessageResult.networkFailure(recipient); @@ -1145,10 +1043,10 @@ public class SignalServiceMessageSender { int ttl, boolean useFallbackEncryption, boolean isClosedGroup, - final boolean notifyPNServer) + final boolean notifyPNServer, + Optional syncTarget) throws IOException, UntrustedIdentityException { -// if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); } final SettableFuture[] future = { new SettableFuture() }; OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); // Loki - Remove this when we have shared sender keys @@ -1214,14 +1112,10 @@ public class SignalServiceMessageSender { } return Unit.INSTANCE; } - }).fail(new Function1() { - - @Override - public Unit invoke(Exception exception) { - @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; - f.setException(exception); - return Unit.INSTANCE; - } + }).fail(exception -> { + @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; + f.setException(exception); + return Unit.INSTANCE; }); @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture)future[0]; @@ -1297,12 +1191,6 @@ public class SignalServiceMessageSender { return builder.build(); } - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment) - throws IOException - { - return createAttachmentPointer(attachment, false, null); - } - private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient) throws IOException { diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 7c7f575b63..51ca336605 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -342,7 +342,6 @@ public class SignalServiceCipher { kotlin.Pair plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl); paddedMessage = plaintextAndSenderPublicKey.getFirst(); String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); - if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isPreKeySignalMessage()) { @@ -400,6 +399,7 @@ public class SignalServiceCipher { ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate(); ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2(); boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0); + String syncTarget = content.getSyncTarget(); for (AttachmentPointer pointer : content.getAttachmentsList()) { attachments.add(createAttachmentPointer(pointer)); @@ -428,7 +428,8 @@ public class SignalServiceCipher { null, closedGroupUpdate, closedGroupUpdateV2, - isDeviceUnlinkingRequest); + isDeviceUnlinkingRequest, + syncTarget); } private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java index 14853977c5..151eac5696 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/messages/SignalServiceDataMessage.java @@ -41,6 +41,7 @@ public class SignalServiceDataMessage { private final Optional closedGroupUpdate; private final Optional closedGroupUpdateV2; private final boolean isDeviceUnlinkingRequest; + private final Optional syncTarget; /** * Construct a SignalServiceDataMessage with a body and no attachments. @@ -134,7 +135,7 @@ public class SignalServiceDataMessage { Quote quote, List sharedContacts, List previews, Sticker sticker) { - this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false); + this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false, null); } /** @@ -155,7 +156,7 @@ public class SignalServiceDataMessage { Quote quote, List sharedContacts, List previews, Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink, ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2, - boolean isDeviceUnlinkingRequest) + boolean isDeviceUnlinkingRequest, String syncTarget) { this.timestamp = timestamp; this.body = Optional.fromNullable(body); @@ -172,6 +173,7 @@ public class SignalServiceDataMessage { this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate); this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2); this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest; + this.syncTarget = Optional.fromNullable(syncTarget); if (attachments != null && !attachments.isEmpty()) { this.attachments = Optional.of(attachments); @@ -250,6 +252,10 @@ public class SignalServiceDataMessage { return profileKey; } + public Optional getSyncTarget() { + return syncTarget; + } + public Optional getQuote() { return quote; } @@ -307,6 +313,7 @@ public class SignalServiceDataMessage { private Sticker sticker; private PreKeyBundle preKeyBundle; private DeviceLink deviceLink; + private String syncTarget; private boolean isDeviceUnlinkingRequest; private Builder() {} @@ -336,6 +343,11 @@ public class SignalServiceDataMessage { return this; } + public Builder withSyncTarget(String syncTarget) { + this.syncTarget = syncTarget; + return this; + } + public Builder asEndSessionMessage() { return asEndSessionMessage(true); } @@ -417,7 +429,7 @@ public class SignalServiceDataMessage { profileKeyUpdate, quote, sharedContacts, previews, sticker, preKeyBundle, deviceLink, null, null, - isDeviceUnlinkingRequest); + isDeviceUnlinkingRequest, syncTarget); } } diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt index 1986d2c2db..a582c0fa31 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/utilities/DownloadUtilities.kt @@ -65,19 +65,20 @@ object DownloadUtilities { Log.d("Loki", "Attachment size limit exceeded.") throw PushNetworkException("Max response size exceeded.") } - val input = body.inputStream() - val buffer = ByteArray(32768) - var count = 0 - var bytes = input.read(buffer) - while (bytes >= 0) { - outputStream.write(buffer, 0, bytes) - count += bytes - if (count > maxSize) { - Log.d("Loki", "Attachment size limit exceeded.") - throw PushNetworkException("Max response size exceeded.") + body.inputStream().use { input -> + val buffer = ByteArray(32768) + var count = 0 + var bytes = input.read(buffer) + while (bytes >= 0) { + outputStream.write(buffer, 0, bytes) + count += bytes + if (count > maxSize) { + Log.d("Loki", "Attachment size limit exceeded.") + throw PushNetworkException("Max response size exceeded.") + } + listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) + bytes = input.read(buffer) } - listener?.onAttachmentProgress(body.size.toLong(), count.toLong()) - bytes = input.read(buffer) } } catch (e: Exception) { Log.d("Loki", "Couldn't download attachment due to error: $e.")