Add sent story syncing.
This commit is contained in:
parent
8ca0f4baf4
commit
af9465fefe
29 changed files with 1311 additions and 236 deletions
|
@ -1,21 +1,41 @@
|
||||||
package org.thoughtcrime.securesms.database
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import junit.framework.TestCase.assertNull
|
||||||
import org.hamcrest.MatcherAssert.assertThat
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
import org.hamcrest.Matchers.containsInAnyOrder
|
import org.hamcrest.Matchers.containsInAnyOrder
|
||||||
import org.hamcrest.Matchers.hasSize
|
import org.hamcrest.Matchers.hasSize
|
||||||
import org.hamcrest.Matchers.`is`
|
import org.hamcrest.Matchers.`is`
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.database.model.StoryType
|
import org.thoughtcrime.securesms.database.model.StoryType
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class StorySendsDatabaseTest {
|
class StorySendsDatabaseTest {
|
||||||
|
|
||||||
|
private val distributionId1 = DistributionId.from(UUID.randomUUID())
|
||||||
|
private val distributionId2 = DistributionId.from(UUID.randomUUID())
|
||||||
|
private val distributionId3 = DistributionId.from(UUID.randomUUID())
|
||||||
|
|
||||||
|
private lateinit var distributionList1: DistributionListId
|
||||||
|
private lateinit var distributionList2: DistributionListId
|
||||||
|
private lateinit var distributionList3: DistributionListId
|
||||||
|
|
||||||
|
private lateinit var distributionListRecipient1: Recipient
|
||||||
|
private lateinit var distributionListRecipient2: Recipient
|
||||||
|
private lateinit var distributionListRecipient3: Recipient
|
||||||
|
|
||||||
private lateinit var recipients1to10: List<RecipientId>
|
private lateinit var recipients1to10: List<RecipientId>
|
||||||
private lateinit var recipients11to20: List<RecipientId>
|
private lateinit var recipients11to20: List<RecipientId>
|
||||||
private lateinit var recipients6to15: List<RecipientId>
|
private lateinit var recipients6to15: List<RecipientId>
|
||||||
|
@ -31,22 +51,41 @@ class StorySendsDatabaseTest {
|
||||||
fun setup() {
|
fun setup() {
|
||||||
storySends = SignalDatabase.storySends
|
storySends = SignalDatabase.storySends
|
||||||
|
|
||||||
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
|
||||||
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
|
|
||||||
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
|
|
||||||
|
|
||||||
recipients1to10 = makeRecipients(10)
|
recipients1to10 = makeRecipients(10)
|
||||||
recipients11to20 = makeRecipients(10)
|
recipients11to20 = makeRecipients(10)
|
||||||
|
|
||||||
|
distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!!
|
||||||
|
distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!!
|
||||||
|
distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!!
|
||||||
|
|
||||||
|
distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1))
|
||||||
|
distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2))
|
||||||
|
distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3))
|
||||||
|
|
||||||
|
messageId1 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
messageId2 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient2,
|
||||||
|
storyType = StoryType.STORY_WITH_REPLIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
messageId3 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient3,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
)
|
||||||
|
|
||||||
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
|
||||||
recipients6to10 = recipients1to10.takeLast(5)
|
recipients6to10 = recipients1to10.takeLast(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRecipientsToSendTo_noOverlap() {
|
fun getRecipientsToSendTo_noOverlap() {
|
||||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||||
|
@ -60,8 +99,8 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRecipientsToSendTo_overlap() {
|
fun getRecipientsToSendTo_overlap() {
|
||||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||||
storySends.insert(messageId2, recipients6to15, 100, true)
|
storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||||
|
@ -78,9 +117,9 @@ class StorySendsDatabaseTest {
|
||||||
val recipient1 = recipients1to10.first()
|
val recipient1 = recipients1to10.first()
|
||||||
val recipient2 = recipients11to20.first()
|
val recipient2 = recipients11to20.first()
|
||||||
|
|
||||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
|
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
|
||||||
storySends.insert(messageId2, listOf(recipient1), 100, true)
|
storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
|
||||||
storySends.insert(messageId3, listOf(recipient2), 100, true)
|
storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||||
|
@ -97,8 +136,8 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||||
storySends.insert(messageId1, recipients6to15, 100, true)
|
storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
|
||||||
storySends.insert(messageId2, recipients1to10, 100, false)
|
storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||||
|
@ -112,9 +151,9 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRemoteDeleteRecipients_noOverlap() {
|
fun getRemoteDeleteRecipients_noOverlap() {
|
||||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||||
|
@ -128,8 +167,8 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||||
|
|
||||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||||
|
@ -143,10 +182,10 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||||
|
|
||||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||||
|
|
||||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||||
|
|
||||||
|
@ -156,7 +195,7 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun canReply_storyWithReplies() {
|
fun canReply_storyWithReplies() {
|
||||||
storySends.insert(messageId2, recipients1to10, 200, true)
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
|
||||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||||
|
|
||||||
|
@ -165,7 +204,7 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun canReply_storyWithoutReplies() {
|
fun canReply_storyWithoutReplies() {
|
||||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
|
||||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||||
|
|
||||||
|
@ -174,8 +213,8 @@ class StorySendsDatabaseTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
storySends.insert(messageId2, recipients6to10, 200, true)
|
storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
|
||||||
|
|
||||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||||
|
@ -184,6 +223,238 @@ class StorySendsDatabaseTest {
|
||||||
assertThat(message2RecipientCanReply, `is`(true))
|
assertThat(message2RecipientCanReply, `is`(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||||
|
|
||||||
|
assertNotNull(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, false, distributionId2)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)
|
||||||
|
|
||||||
|
assertNull(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||||
|
|
||||||
|
assertEquals(recipients1to10, manifest.entries.map { it.recipientId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||||
|
|
||||||
|
manifest.entries.forEach { entry ->
|
||||||
|
assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
|
||||||
|
|
||||||
|
manifest.entries.forEach { entry ->
|
||||||
|
assertTrue(entry.allowedToReply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||||
|
|
||||||
|
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
|
||||||
|
|
||||||
|
assertNotNull(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||||
|
|
||||||
|
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||||
|
|
||||||
|
assertEquals(recipients1to10.toHashSet(), recipientIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||||
|
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||||
|
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||||
|
|
||||||
|
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
|
||||||
|
|
||||||
|
val manifestRecipients = results.entries.map { it.recipientId }
|
||||||
|
assertEquals(recipients1to10, manifestRecipients)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||||
|
SignalDatabase.mms.markAsRemoteDelete(messageId2)
|
||||||
|
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
|
||||||
|
|
||||||
|
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
|
||||||
|
|
||||||
|
assertTrue(results.entries.all { it.allowedToReply })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
|
||||||
|
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||||
|
val expected = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||||
|
val emptyManifest = SentStorySyncManifest(emptyList())
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(emptyManifest, 200)
|
||||||
|
val result = storySends.getFullSentStorySyncManifest(messageId1, 200)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() {
|
||||||
|
val messageId4 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||||
|
val expected = storySends.getFullSentStorySyncManifest(messageId4, 200)
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(expected!!, 200)
|
||||||
|
val result = storySends.getFullSentStorySyncManifest(messageId4, 200)
|
||||||
|
|
||||||
|
assertEquals(expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
|
||||||
|
val messageId4 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
val messageId5 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient2,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||||
|
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
|
||||||
|
|
||||||
|
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(remote, 200)
|
||||||
|
|
||||||
|
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() {
|
||||||
|
val messageId4 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
val messageId5 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient2,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
|
||||||
|
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
|
||||||
|
|
||||||
|
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(remote, 200)
|
||||||
|
|
||||||
|
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() {
|
||||||
|
val messageId4 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 2000
|
||||||
|
)
|
||||||
|
|
||||||
|
val remote = SentStorySyncManifest(
|
||||||
|
recipients1to10.map {
|
||||||
|
SentStorySyncManifest.Entry(
|
||||||
|
recipientId = it,
|
||||||
|
allowedToReply = true,
|
||||||
|
distributionLists = listOf(distributionId1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(remote, 2000)
|
||||||
|
|
||||||
|
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
|
||||||
|
assertEquals(remote, local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() {
|
||||||
|
val messageId4 = MmsHelper.insert(
|
||||||
|
recipient = distributionListRecipient1,
|
||||||
|
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||||
|
sentTimeMillis = 2000
|
||||||
|
)
|
||||||
|
|
||||||
|
MmsHelper.insert(
|
||||||
|
recipient = Recipient.resolved(recipients1to10.first()),
|
||||||
|
sentTimeMillis = 2000
|
||||||
|
)
|
||||||
|
|
||||||
|
val remote = SentStorySyncManifest(
|
||||||
|
recipients1to10.map {
|
||||||
|
SentStorySyncManifest.Entry(
|
||||||
|
recipientId = it,
|
||||||
|
allowedToReply = true,
|
||||||
|
distributionLists = listOf(distributionId1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
storySends.applySentStoryManifest(remote, 2000)
|
||||||
|
|
||||||
|
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
|
||||||
|
assertEquals(remote, local)
|
||||||
|
}
|
||||||
|
|
||||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||||
return (1..count).map {
|
return (1..count).map {
|
||||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||||
|
|
|
@ -27,10 +27,8 @@ import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
|
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||||
import org.thoughtcrime.securesms.stories.Stories;
|
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||||
|
|
||||||
|
@ -57,7 +55,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||||
public static final int FLAG_HIDE_NEW = 1 << 6;
|
public static final int FLAG_HIDE_NEW = 1 << 6;
|
||||||
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
|
||||||
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
|
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
|
||||||
public static final int FLAG_STORIES = 1 << 9;
|
|
||||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +85,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||||
addRecentGroupsSection(cursorList);
|
addRecentGroupsSection(cursorList);
|
||||||
addGroupsSection(cursorList);
|
addGroupsSection(cursorList);
|
||||||
} else {
|
} else {
|
||||||
addStoriesSection(cursorList);
|
|
||||||
addRecentsSection(cursorList);
|
addRecentsSection(cursorList);
|
||||||
addContactsSection(cursorList);
|
addContactsSection(cursorList);
|
||||||
if (addGroupsAfterContacts(mode)) {
|
if (addGroupsAfterContacts(mode)) {
|
||||||
|
@ -167,19 +163,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addStoriesSection(@NonNull List<Cursor> cursorList) {
|
|
||||||
if (!Stories.isFeatureEnabled() ||!storiesEnabled(mode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor stories = getStoriesCursor();
|
|
||||||
|
|
||||||
if (stories.getCount() > 0) {
|
|
||||||
cursorList.add(ContactsCursorRows.forStoriesHeader(getContext()));
|
|
||||||
cursorList.add(stories);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addNewNumberSection(@NonNull List<Cursor> cursorList) {
|
private void addNewNumberSection(@NonNull List<Cursor> cursorList) {
|
||||||
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) {
|
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) {
|
||||||
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
|
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
|
||||||
|
@ -240,16 +223,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||||
return groupContacts;
|
return groupContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Cursor getStoriesCursor() {
|
|
||||||
MatrixCursor distributionListsCursor = ContactsCursorRows.createMatrixCursor();
|
|
||||||
List<DistributionListPartialRecord> distributionLists = SignalDatabase.distributionLists().getAllListsForContactSelectionUi(null, true);
|
|
||||||
for (final DistributionListPartialRecord distributionList : distributionLists) {
|
|
||||||
distributionListsCursor.addRow(ContactsCursorRows.forDistributionList(distributionList));
|
|
||||||
}
|
|
||||||
|
|
||||||
return distributionListsCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cursor getNewNumberCursor() {
|
private Cursor getNewNumberCursor() {
|
||||||
return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter());
|
return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter());
|
||||||
}
|
}
|
||||||
|
@ -320,10 +293,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
||||||
return flagSet(mode, DisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
return flagSet(mode, DisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean storiesEnabled(int mode) {
|
|
||||||
return flagSet(mode, DisplayMode.FLAG_STORIES);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean flagSet(int mode, int flag) {
|
private static boolean flagSet(int mode, int flag) {
|
||||||
return (mode & flag) > 0;
|
return (mode & flag) > 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,6 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
|
|
||||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||||
|
@ -86,16 +84,6 @@ public final class ContactsCursorRows {
|
||||||
""};
|
""};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull Object[] forDistributionList(@NonNull DistributionListPartialRecord distributionListPartialRecord) {
|
|
||||||
return new Object[]{ distributionListPartialRecord.getRecipientId().serialize(),
|
|
||||||
distributionListPartialRecord.getName(),
|
|
||||||
SignalDatabase.distributionLists().getMemberCount(distributionListPartialRecord.getId()),
|
|
||||||
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
|
|
||||||
"",
|
|
||||||
ContactRepository.NORMAL_TYPE,
|
|
||||||
""};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a row for a contacts cursor for a new number the user is entering or has entered.
|
* Create a row for a contacts cursor for a new number the user is entering or has entered.
|
||||||
*/
|
*/
|
||||||
|
@ -130,10 +118,6 @@ public final class ContactsCursorRows {
|
||||||
return matrixCursor;
|
return matrixCursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull MatrixCursor forStoriesHeader(@NonNull Context context) {
|
|
||||||
return forHeader(context.getString(R.string.ContactsCursorLoader_my_stories));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
|
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
|
||||||
return forHeader(context.getString(R.string.ContactsCursorLoader_username_search));
|
return forHeader(context.getString(R.string.ContactsCursorLoader_username_search));
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
|
import org.signal.core.util.select
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||||
|
@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.util.Base64
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import java.lang.AssertionError
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +36,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
|
||||||
|
|
||||||
const val RECIPIENT_ID = ListTable.RECIPIENT_ID
|
const val RECIPIENT_ID = ListTable.RECIPIENT_ID
|
||||||
|
const val DISTRIBUTION_ID = ListTable.DISTRIBUTION_ID
|
||||||
|
const val LIST_TABLE_NAME = ListTable.TABLE_NAME
|
||||||
|
|
||||||
fun insertInitialDistributionListAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
fun insertInitialDistributionListAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||||
val recipientId = db.insert(
|
val recipientId = db.insert(
|
||||||
|
@ -68,6 +70,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
const val RECIPIENT_ID = "recipient_id"
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
const val ALLOWS_REPLIES = "allows_replies"
|
const val ALLOWS_REPLIES = "allows_replies"
|
||||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||||
|
const val IS_UNKNOWN = "is_unknown"
|
||||||
|
|
||||||
const val CREATE_TABLE = """
|
const val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
@ -76,7 +79,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
||||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||||
$ALLOWS_REPLIES INTEGER DEFAULT 1,
|
$ALLOWS_REPLIES INTEGER DEFAULT 1,
|
||||||
$DELETION_TIMESTAMP INTEGER DEFAULT 0
|
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
|
||||||
|
$IS_UNKNOWN INTEGER DEFAULT 0
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -124,7 +128,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
|
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||||
|
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -135,13 +140,13 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
|
|
||||||
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
|
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
|
||||||
val db = readableDatabase
|
val db = readableDatabase
|
||||||
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
|
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||||
|
|
||||||
val where = when {
|
val where = when {
|
||||||
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
|
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
|
||||||
query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
|
query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
|
||||||
includeMyStory -> "(${ListTable.NAME} GLOB ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED}"
|
includeMyStory -> "(${ListTable.NAME} GLOB ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN}"
|
||||||
else -> "${ListTable.NAME} GLOB ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
|
else -> "${ListTable.NAME} GLOB ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val whereArgs = when {
|
val whereArgs = when {
|
||||||
|
@ -155,7 +160,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
|
|
||||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||||
val db = readableDatabase
|
val db = readableDatabase
|
||||||
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
|
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
|
||||||
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
|
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
|
||||||
|
|
||||||
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
|
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
|
||||||
|
@ -166,7 +171,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
|
||||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||||
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
|
||||||
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
|
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
|
||||||
|
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -175,6 +181,50 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or creates a distribution list for the given id.
|
||||||
|
*
|
||||||
|
* If the list does not exist, then a new list is created with a randomized name and populated with the members
|
||||||
|
* in the manifest.
|
||||||
|
*
|
||||||
|
* @return the recipient id of the list
|
||||||
|
*/
|
||||||
|
fun getOrCreateByDistributionId(distributionId: DistributionId, manifest: SentStorySyncManifest): RecipientId {
|
||||||
|
writableDatabase.beginTransaction()
|
||||||
|
try {
|
||||||
|
val distributionRecipientId = getRecipientIdByDistributionId(distributionId)
|
||||||
|
if (distributionRecipientId == null) {
|
||||||
|
val members: List<RecipientId> = manifest.entries
|
||||||
|
.filter { it.distributionLists.contains(distributionId) }
|
||||||
|
.map { it.recipientId }
|
||||||
|
|
||||||
|
val distributionListId = createList(
|
||||||
|
name = createUniqueNameForUnknownDistributionId(),
|
||||||
|
members = members,
|
||||||
|
distributionId = distributionId,
|
||||||
|
isUnknown = true
|
||||||
|
)
|
||||||
|
|
||||||
|
if (distributionListId == null) {
|
||||||
|
throw AssertionError("Failed to create distribution list for unknown id.")
|
||||||
|
} else {
|
||||||
|
val recipient = getRecipientId(distributionListId)
|
||||||
|
if (recipient == null) {
|
||||||
|
throw AssertionError("Failed to retrieve recipient for newly created list")
|
||||||
|
} else {
|
||||||
|
writableDatabase.setTransactionSuccessful()
|
||||||
|
return recipient
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writableDatabase.setTransactionSuccessful()
|
||||||
|
return distributionRecipientId
|
||||||
|
} finally {
|
||||||
|
writableDatabase.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict.
|
* @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict.
|
||||||
*/
|
*/
|
||||||
|
@ -184,7 +234,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
|
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
|
||||||
allowsReplies: Boolean = true,
|
allowsReplies: Boolean = true,
|
||||||
deletionTimestamp: Long = 0L,
|
deletionTimestamp: Long = 0L,
|
||||||
storageId: ByteArray? = null
|
storageId: ByteArray? = null,
|
||||||
|
isUnknown: Boolean = false
|
||||||
): DistributionListId? {
|
): DistributionListId? {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
|
|
||||||
|
@ -196,6 +247,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
|
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
|
||||||
putNull(ListTable.RECIPIENT_ID)
|
putNull(ListTable.RECIPIENT_ID)
|
||||||
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||||
|
put(ListTable.IS_UNKNOWN, isUnknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
|
||||||
|
@ -222,6 +274,21 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRecipientIdByDistributionId(distributionId: DistributionId): RecipientId? {
|
||||||
|
return readableDatabase
|
||||||
|
.select(ListTable.RECIPIENT_ID)
|
||||||
|
.from(ListTable.TABLE_NAME)
|
||||||
|
.where("${ListTable.DISTRIBUTION_ID} = ?", distributionId.toString())
|
||||||
|
.run()
|
||||||
|
.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
RecipientId.from(CursorUtil.requireLong(cursor, ListTable.RECIPIENT_ID))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getStoryType(listId: DistributionListId): StoryType {
|
fun getStoryType(listId: DistributionListId): StoryType {
|
||||||
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
|
||||||
return if (cursor.moveToFirst()) {
|
return if (cursor.moveToFirst()) {
|
||||||
|
@ -251,7 +318,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||||
members = getMembers(id),
|
members = getMembers(id),
|
||||||
deletedAtTimestamp = 0L
|
deletedAtTimestamp = 0L,
|
||||||
|
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -270,7 +338,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||||
members = getRawMembers(id),
|
members = getRawMembers(id),
|
||||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP)
|
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
|
||||||
|
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -461,7 +530,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
try {
|
try {
|
||||||
val listTableValues = contentValuesOf(
|
val listTableValues = contentValuesOf(
|
||||||
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
|
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
|
||||||
ListTable.NAME to update.new.name
|
ListTable.NAME to update.new.name,
|
||||||
|
ListTable.IS_UNKNOWN to false
|
||||||
)
|
)
|
||||||
|
|
||||||
writableDatabase.update(
|
writableDatabase.update(
|
||||||
|
@ -493,4 +563,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||||
private fun createUniqueNameForDeletedStory(): String {
|
private fun createUniqueNameForDeletedStory(): String {
|
||||||
return "DELETED-${UUID.randomUUID()}"
|
return "DELETED-${UUID.randomUUID()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createUniqueNameForUnknownDistributionId(): String {
|
||||||
|
return "DELETED-${UUID.randomUUID()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,6 +188,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||||
public abstract boolean isStory(long messageId);
|
public abstract boolean isStory(long messageId);
|
||||||
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
||||||
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
|
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
|
||||||
|
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
|
||||||
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
|
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
|
||||||
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
|
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
|
||||||
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
|
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
|
||||||
|
|
|
@ -584,6 +584,15 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
return new Reader(rawQuery(where, null, reverse, -1L));
|
return new Reader(rawQuery(where, null, reverse, -1L));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
|
||||||
|
String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")";
|
||||||
|
String[] whereArgs = SqlUtil.buildArgs(sentTimestamp);
|
||||||
|
Cursor cursor = rawQuery(where, whereArgs, false, -1L);
|
||||||
|
|
||||||
|
return new Reader(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
||||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
||||||
|
@ -932,7 +941,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
public boolean hasMeaningfulMessage(long threadId) {
|
public boolean hasMeaningfulMessage(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
|
||||||
|
|
||||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, THREAD_ID_WHERE, SqlUtil.buildArgs(threadId), null, null, null, "1")) {
|
try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?", SqlUtil.buildArgs(threadId, 0, 0), null, null, null, "1")) {
|
||||||
return cursor != null && cursor.moveToFirst();
|
return cursor != null && cursor.moveToFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a list of, or update to a list of, who can access a story through what
|
||||||
|
* distribution lists, and whether they can reply.
|
||||||
|
*/
|
||||||
|
data class SentStorySyncManifest(
|
||||||
|
val entries: List<Entry>
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an entry in the proto manifest.
|
||||||
|
*/
|
||||||
|
data class Entry(
|
||||||
|
val recipientId: RecipientId,
|
||||||
|
val allowedToReply: Boolean = false,
|
||||||
|
val distributionLists: List<DistributionId> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a flattened entry that is more convenient for detecting data changes.
|
||||||
|
*/
|
||||||
|
data class Row(
|
||||||
|
val recipientId: RecipientId,
|
||||||
|
val messageId: Long,
|
||||||
|
val allowsReplies: Boolean,
|
||||||
|
val distributionId: DistributionId
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getDistributionIdSet(): Set<DistributionId> {
|
||||||
|
return entries.map { it.distributionLists }.flatten().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toRecipientsSet(): Set<SignalServiceStoryMessageRecipient> {
|
||||||
|
val recipients = Recipient.resolvedList(entries.map { it.recipientId })
|
||||||
|
return recipients.map { recipient ->
|
||||||
|
val serviceId = recipient.requireServiceId()
|
||||||
|
val entry = entries.first { it.recipientId == recipient.id }
|
||||||
|
|
||||||
|
SignalServiceStoryMessageRecipient(
|
||||||
|
SignalServiceAddress(serviceId),
|
||||||
|
entry.distributionLists.map { it.toString() },
|
||||||
|
entry.allowedToReply
|
||||||
|
)
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flattenToRows(distributionIdToMessageIdMap: Map<DistributionId, Long>): Set<Row> {
|
||||||
|
return entries.flatMap { getRowsForEntry(it, distributionIdToMessageIdMap) }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRowsForEntry(entry: Entry, distributionIdToMessageIdMap: Map<DistributionId, Long>): List<Row> {
|
||||||
|
return entry.distributionLists.map {
|
||||||
|
Row(
|
||||||
|
recipientId = entry.recipientId,
|
||||||
|
allowsReplies = entry.allowedToReply,
|
||||||
|
messageId = distributionIdToMessageIdMap[it] ?: -1L,
|
||||||
|
distributionId = it
|
||||||
|
)
|
||||||
|
}.filterNot { it.messageId == -1L }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@WorkerThread
|
||||||
|
@JvmStatic
|
||||||
|
fun fromRecipientsSet(recipientsSet: Set<SignalServiceStoryMessageRecipient>): SentStorySyncManifest {
|
||||||
|
val entries = recipientsSet.map { recipient ->
|
||||||
|
Entry(
|
||||||
|
recipientId = RecipientId.from(recipient.signalServiceAddress),
|
||||||
|
allowedToReply = recipient.isAllowedToReply,
|
||||||
|
distributionLists = recipient.distributionListIds.map { DistributionId.from(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SentStorySyncManifest(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1401,6 +1401,11 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
|
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
|
@ -3,11 +3,15 @@ package org.thoughtcrime.securesms.database
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
|
import org.signal.core.util.CursorUtil
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
|
import org.signal.core.util.select
|
||||||
import org.signal.core.util.toInt
|
import org.signal.core.util.toInt
|
||||||
|
import org.signal.core.util.update
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to
|
* Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to
|
||||||
|
@ -26,6 +30,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||||
const val RECIPIENT_ID = "recipient_id"
|
const val RECIPIENT_ID = "recipient_id"
|
||||||
const val SENT_TIMESTAMP = "sent_timestamp"
|
const val SENT_TIMESTAMP = "sent_timestamp"
|
||||||
const val ALLOWS_REPLIES = "allows_replies"
|
const val ALLOWS_REPLIES = "allows_replies"
|
||||||
|
const val DISTRIBUTION_ID = "distribution_id"
|
||||||
|
|
||||||
val CREATE_TABLE = """
|
val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
@ -33,7 +38,8 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||||
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE,
|
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE,
|
||||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
||||||
$SENT_TIMESTAMP INTEGER NOT NULL,
|
$SENT_TIMESTAMP INTEGER NOT NULL,
|
||||||
$ALLOWS_REPLIES INTEGER NOT NULL
|
$ALLOWS_REPLIES INTEGER NOT NULL,
|
||||||
|
$DISTRIBUTION_ID TEXT NOT NULL REFERENCES ${DistributionListDatabase.LIST_TABLE_NAME} (${DistributionListDatabase.DISTRIBUTION_ID}) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
|
@ -42,7 +48,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean) {
|
fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean, distributionId: DistributionId) {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
|
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
|
@ -52,11 +58,12 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||||
MESSAGE_ID to messageId,
|
MESSAGE_ID to messageId,
|
||||||
RECIPIENT_ID to id.serialize(),
|
RECIPIENT_ID to id.serialize(),
|
||||||
SENT_TIMESTAMP to sentTimestamp,
|
SENT_TIMESTAMP to sentTimestamp,
|
||||||
ALLOWS_REPLIES to allowsReplies.toInt()
|
ALLOWS_REPLIES to allowsReplies.toInt(),
|
||||||
|
DISTRIBUTION_ID to distributionId.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES), insertValues)
|
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES, DISTRIBUTION_ID), insertValues)
|
||||||
.forEach { query -> db.execSQL(query.where, query.whereArgs) }
|
.forEach { query -> db.execSQL(query.where, query.whereArgs) }
|
||||||
|
|
||||||
db.setTransactionSuccessful()
|
db.setTransactionSuccessful()
|
||||||
|
@ -177,4 +184,208 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||||
|
|
||||||
writableDatabase.update(TABLE_NAME, values, query, args)
|
writableDatabase.update(TABLE_NAME, values, query, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the manifest for a given story, or null if the story should NOT be the one reporting the manifest.
|
||||||
|
*/
|
||||||
|
fun getFullSentStorySyncManifest(messageId: Long, sentTimestamp: Long): SentStorySyncManifest? {
|
||||||
|
val firstMessageId: Long = readableDatabase.select(MESSAGE_ID)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.where(
|
||||||
|
"""
|
||||||
|
$SENT_TIMESTAMP = ? AND
|
||||||
|
(SELECT ${MmsDatabase.REMOTE_DELETED} FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.ID} = $MESSAGE_ID) = 0
|
||||||
|
""".trimIndent(),
|
||||||
|
sentTimestamp
|
||||||
|
)
|
||||||
|
.orderBy(MESSAGE_ID)
|
||||||
|
.limit(1)
|
||||||
|
.run()
|
||||||
|
.use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
CursorUtil.requireLong(it, MESSAGE_ID)
|
||||||
|
} else {
|
||||||
|
-1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstMessageId == -1L || firstMessageId != messageId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLocalManifest(sentTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the manifest after a change to the available distribution lists occurs. This will only include the recipients
|
||||||
|
* as specified by onlyInclude, and is meant to represent a delta rather than an entire manifest.
|
||||||
|
*/
|
||||||
|
fun getSentStorySyncManifestForUpdate(sentTimestamp: Long, onlyInclude: Set<RecipientId>): SentStorySyncManifest {
|
||||||
|
val localManifest: SentStorySyncManifest = getLocalManifest(sentTimestamp)
|
||||||
|
val entries: List<SentStorySyncManifest.Entry> = localManifest.entries.filter { it.recipientId in onlyInclude }
|
||||||
|
|
||||||
|
return SentStorySyncManifest(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manifest updates should only include the specific recipients who have changes (normally, one less distribution list),
|
||||||
|
* and of those, only the ones that have a non-empty set of distribution lists.
|
||||||
|
*
|
||||||
|
* @return A set of recipients who were able to receive the deleted story, and still have other stories at the same timestamp.
|
||||||
|
*/
|
||||||
|
fun getRecipientIdsForManifestUpdate(sentTimestamp: Long, deletedMessageId: Long): Set<RecipientId> {
|
||||||
|
// language=sql
|
||||||
|
val query = """
|
||||||
|
SELECT $RECIPIENT_ID
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
WHERE $SENT_TIMESTAMP = ?
|
||||||
|
AND $RECIPIENT_ID IN (
|
||||||
|
SELECT $RECIPIENT_ID
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
WHERE $MESSAGE_ID = ?
|
||||||
|
)
|
||||||
|
AND $MESSAGE_ID IN (
|
||||||
|
SELECT ${MmsDatabase.ID}
|
||||||
|
FROM ${MmsDatabase.TABLE_NAME}
|
||||||
|
WHERE ${MmsDatabase.REMOTE_DELETED} = 0
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
return readableDatabase.rawQuery(query, arrayOf(sentTimestamp, deletedMessageId)).use { cursor ->
|
||||||
|
if (cursor.count == 0) emptyList<RecipientId>()
|
||||||
|
|
||||||
|
val results: MutableSet<RecipientId> = hashSetOf()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
results.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the given manifest to the local database. This method will:
|
||||||
|
*
|
||||||
|
* 1. Generate the local manifest
|
||||||
|
* 1. Gather the unique collective distribution id set from remote and local manifests
|
||||||
|
* 1. Flatten both manifests into a set of Rows
|
||||||
|
* 1. For each changed manifest row in remote, update the corresponding row in local
|
||||||
|
* 1. For each new manifest row in remote, update the corresponding row in local
|
||||||
|
* 1. For each unique message id in local not present in remote, we can assume that the message can be marked deleted.
|
||||||
|
*/
|
||||||
|
fun applySentStoryManifest(remoteManifest: SentStorySyncManifest, sentTimestamp: Long) {
|
||||||
|
if (remoteManifest.entries.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writableDatabase.beginTransaction()
|
||||||
|
try {
|
||||||
|
val localManifest: SentStorySyncManifest = getLocalManifest(sentTimestamp)
|
||||||
|
|
||||||
|
val query = """
|
||||||
|
SELECT ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID} as $MESSAGE_ID, ${DistributionListDatabase.DISTRIBUTION_ID}
|
||||||
|
FROM ${MmsDatabase.TABLE_NAME}
|
||||||
|
INNER JOIN ${DistributionListDatabase.LIST_TABLE_NAME} ON ${DistributionListDatabase.RECIPIENT_ID} = ${MmsDatabase.RECIPIENT_ID}
|
||||||
|
WHERE ${MmsDatabase.DATE_SENT} = $sentTimestamp AND ${DistributionListDatabase.DISTRIBUTION_ID} IS NOT NULL
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val distributionIdToMessageId = readableDatabase.query(query).use { cursor ->
|
||||||
|
val results: MutableMap<DistributionId, Long> = mutableMapOf()
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DistributionListDatabase.DISTRIBUTION_ID))
|
||||||
|
val messageId = CursorUtil.requireLong(cursor, MESSAGE_ID)
|
||||||
|
|
||||||
|
results[distributionId] = messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
val localRows: Set<SentStorySyncManifest.Row> = localManifest.flattenToRows(distributionIdToMessageId)
|
||||||
|
val remoteRows: Set<SentStorySyncManifest.Row> = remoteManifest.flattenToRows(distributionIdToMessageId)
|
||||||
|
|
||||||
|
if (localRows == remoteRows) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val remoteOnly: List<SentStorySyncManifest.Row> = remoteRows.filterNot { localRows.contains(it) }
|
||||||
|
val changedInRemoteManifest: List<SentStorySyncManifest.Row> = remoteOnly.filter { (recipientId, messageId) -> localRows.any { it.messageId == messageId && it.recipientId == recipientId } }
|
||||||
|
val newInRemoteManifest: List<SentStorySyncManifest.Row> = remoteOnly.filterNot { (recipientId, messageId) -> localRows.any { it.messageId == messageId && it.recipientId == recipientId } }
|
||||||
|
|
||||||
|
changedInRemoteManifest
|
||||||
|
.forEach { (recipientId, messageId, allowsReplies, distributionId) ->
|
||||||
|
writableDatabase.update(TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
contentValuesOf(
|
||||||
|
ALLOWS_REPLIES to allowsReplies,
|
||||||
|
RECIPIENT_ID to recipientId.toLong(),
|
||||||
|
SENT_TIMESTAMP to sentTimestamp,
|
||||||
|
MESSAGE_ID to messageId,
|
||||||
|
DISTRIBUTION_ID to distributionId.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
newInRemoteManifest
|
||||||
|
.forEach { (recipientId, messageId, allowsReplies, distributionId) ->
|
||||||
|
writableDatabase.insert(
|
||||||
|
TABLE_NAME,
|
||||||
|
null,
|
||||||
|
contentValuesOf(
|
||||||
|
ALLOWS_REPLIES to allowsReplies,
|
||||||
|
RECIPIENT_ID to recipientId.toLong(),
|
||||||
|
SENT_TIMESTAMP to sentTimestamp,
|
||||||
|
MESSAGE_ID to messageId,
|
||||||
|
DISTRIBUTION_ID to distributionId.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val messagesWithoutAnyReceivers = localRows.map { it.messageId }.distinct() - remoteRows.map { it.messageId }.distinct()
|
||||||
|
messagesWithoutAnyReceivers.forEach { SignalDatabase.mms.markAsRemoteDelete(it) }
|
||||||
|
|
||||||
|
writableDatabase.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
writableDatabase.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLocalManifest(sentTimestamp: Long): SentStorySyncManifest {
|
||||||
|
val entries = readableDatabase.rawQuery(
|
||||||
|
// language=sql
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
$RECIPIENT_ID,
|
||||||
|
$ALLOWS_REPLIES,
|
||||||
|
$DISTRIBUTION_ID
|
||||||
|
FROM $TABLE_NAME
|
||||||
|
WHERE $TABLE_NAME.$SENT_TIMESTAMP = ? AND (
|
||||||
|
SELECT ${MmsDatabase.REMOTE_DELETED}
|
||||||
|
FROM ${MmsDatabase.TABLE_NAME}
|
||||||
|
WHERE ${MmsDatabase.ID} = $TABLE_NAME.$MESSAGE_ID
|
||||||
|
) = 0
|
||||||
|
""".trimIndent(),
|
||||||
|
arrayOf(sentTimestamp)
|
||||||
|
).use { cursor ->
|
||||||
|
val results: MutableMap<RecipientId, SentStorySyncManifest.Entry> = mutableMapOf()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))
|
||||||
|
val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DISTRIBUTION_ID))
|
||||||
|
val allowsReplies = CursorUtil.requireBoolean(cursor, ALLOWS_REPLIES)
|
||||||
|
val entry = results[recipientId]?.let {
|
||||||
|
it.copy(
|
||||||
|
allowedToReply = it.allowedToReply or allowsReplies,
|
||||||
|
distributionLists = it.distributionLists + distributionId
|
||||||
|
)
|
||||||
|
} ?: SentStorySyncManifest.Entry(recipientId, canReply(recipientId, sentTimestamp), listOf(distributionId))
|
||||||
|
|
||||||
|
results[recipientId] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
return SentStorySyncManifest(entries.values.toList())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1300,19 +1300,23 @@ public class ThreadDatabase extends Database {
|
||||||
private boolean update(long threadId, boolean unarchive, boolean allowDeletion, boolean notifyListeners) {
|
private boolean update(long threadId, boolean unarchive, boolean allowDeletion, boolean notifyListeners) {
|
||||||
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
||||||
boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId);
|
boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId);
|
||||||
|
boolean isPinned = getPinnedThreadIds().contains(threadId);
|
||||||
|
boolean shouldDelete = allowDeletion && !isPinned && !SignalDatabase.mms().containsStories(threadId);
|
||||||
|
|
||||||
if (!meaningfulMessages) {
|
if (!meaningfulMessages) {
|
||||||
if (allowDeletion) {
|
if (shouldDelete) {
|
||||||
deleteConversation(threadId);
|
deleteConversation(threadId);
|
||||||
|
return true;
|
||||||
|
} else if (!isPinned) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageRecord record;
|
MessageRecord record;
|
||||||
try {
|
try {
|
||||||
record = mmsSmsDatabase.getConversationSnippet(threadId);
|
record = mmsSmsDatabase.getConversationSnippet(threadId);
|
||||||
} catch (NoSuchMessageException e) {
|
} catch (NoSuchMessageException e) {
|
||||||
if (allowDeletion && !SignalDatabase.mms().containsStories(threadId)) {
|
if (shouldDelete) {
|
||||||
deleteConversation(threadId);
|
deleteConversation(threadId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -196,8 +196,9 @@ object SignalDatabaseMigrations {
|
||||||
private const val CDS_V2 = 140
|
private const val CDS_V2 = 140
|
||||||
private const val GROUP_SERVICE_ID = 141
|
private const val GROUP_SERVICE_ID = 141
|
||||||
private const val QUOTE_TYPE = 142
|
private const val QUOTE_TYPE = 142
|
||||||
|
private const val STORY_SYNCS = 143
|
||||||
|
|
||||||
const val DATABASE_VERSION = 142
|
const val DATABASE_VERSION = 143
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -2533,6 +2534,39 @@ object SignalDatabaseMigrations {
|
||||||
if (oldVersion < QUOTE_TYPE) {
|
if (oldVersion < QUOTE_TYPE) {
|
||||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0")
|
db.execSQL("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < STORY_SYNCS) {
|
||||||
|
db.execSQL("ALTER TABLE distribution_list ADD COLUMN is_unknown INTEGER DEFAULT 0")
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE story_sends_tmp (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE,
|
||||||
|
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||||
|
sent_timestamp INTEGER NOT NULL,
|
||||||
|
allows_replies INTEGER NOT NULL,
|
||||||
|
distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO story_sends_tmp (_id, message_id, recipient_id, sent_timestamp, allows_replies, distribution_id)
|
||||||
|
SELECT story_sends._id, story_sends.message_id, story_sends.recipient_id, story_sends.sent_timestamp, story_sends.allows_replies, distribution_list.distribution_id
|
||||||
|
FROM story_sends
|
||||||
|
INNER JOIN mms ON story_sends.message_id = mms._id
|
||||||
|
INNER JOIN distribution_list ON distribution_list.recipient_id = mms.address
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL("DROP TABLE story_sends")
|
||||||
|
db.execSQL("DROP INDEX IF EXISTS story_sends_recipient_id_sent_timestamp_allows_replies_index")
|
||||||
|
|
||||||
|
db.execSQL("ALTER TABLE story_sends_tmp RENAME TO story_sends")
|
||||||
|
db.execSQL("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -6,5 +6,6 @@ data class DistributionListPartialRecord(
|
||||||
val id: DistributionListId,
|
val id: DistributionListId,
|
||||||
val name: CharSequence,
|
val name: CharSequence,
|
||||||
val recipientId: RecipientId,
|
val recipientId: RecipientId,
|
||||||
val allowsReplies: Boolean
|
val allowsReplies: Boolean,
|
||||||
|
val isUnknown: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,5 +12,6 @@ data class DistributionListRecord(
|
||||||
val distributionId: DistributionId,
|
val distributionId: DistributionId,
|
||||||
val allowsReplies: Boolean,
|
val allowsReplies: Boolean,
|
||||||
val members: List<RecipientId>,
|
val members: List<RecipientId>,
|
||||||
val deletedAtTimestamp: Long
|
val deletedAtTimestamp: Long,
|
||||||
|
val isUnknown: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -170,6 +170,7 @@ public final class JobManagerFactories {
|
||||||
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application));
|
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application));
|
||||||
put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory());
|
put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory());
|
||||||
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
|
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
|
||||||
|
put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory());
|
||||||
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
||||||
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
|
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
|
||||||
put(SmsSendJob.KEY, new SmsSendJob.Factory());
|
put(SmsSendJob.KEY, new SmsSendJob.Factory());
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
import java.lang.Exception
|
||||||
|
import java.util.Optional
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transmits a sent sync transcript to linked devices containing the story sync manifest for the given sent timestamp.
|
||||||
|
* The transmitted message is sent as a recipient update, and will only contain affected recipients that still have a
|
||||||
|
* live story for the given timestamp.
|
||||||
|
*/
|
||||||
|
class MultiDeviceStorySendSyncJob private constructor(parameters: Parameters, private val sentTimestamp: Long, private val deletedMessageId: Long) : BaseJob(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY = "MultiDeviceStorySendSyncJob"
|
||||||
|
|
||||||
|
private val TAG = Log.tag(MultiDeviceStorySendSyncJob::class.java)
|
||||||
|
|
||||||
|
private const val DATA_SENT_TIMESTAMP = "sent.timestamp"
|
||||||
|
private const val DATA_DELETED_MESSAGE_ID = "deleted.message.id"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun create(sentTimestamp: Long, deletedMessageId: Long): MultiDeviceStorySendSyncJob {
|
||||||
|
return MultiDeviceStorySendSyncJob(
|
||||||
|
parameters = Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setQueue(KEY)
|
||||||
|
.build(),
|
||||||
|
sentTimestamp = sentTimestamp,
|
||||||
|
deletedMessageId = deletedMessageId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putLong(DATA_SENT_TIMESTAMP, sentTimestamp)
|
||||||
|
.putLong(DATA_DELETED_MESSAGE_ID, deletedMessageId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
|
override fun onRun() {
|
||||||
|
val recipientIds = SignalDatabase.storySends.getRecipientIdsForManifestUpdate(sentTimestamp, deletedMessageId)
|
||||||
|
if (recipientIds.isEmpty()) {
|
||||||
|
Log.i(TAG, "No recipients requiring a manifest update. Dropping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateManifest = SignalDatabase.storySends.getSentStorySyncManifestForUpdate(sentTimestamp, recipientIds)
|
||||||
|
|
||||||
|
if (updateManifest.entries.isEmpty()) {
|
||||||
|
Log.i(TAG, "No entries in updated manifest. Dropping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val recipientsSet = updateManifest.toRecipientsSet()
|
||||||
|
val transcriptMessage: SignalServiceSyncMessage = SignalServiceSyncMessage.forSentTranscript(buildSentTranscript(recipientsSet))
|
||||||
|
val sendMessageResult = ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(transcriptMessage, Optional.empty())
|
||||||
|
|
||||||
|
if (!sendMessageResult.isSuccess) {
|
||||||
|
throw RetryableException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShouldRetry(e: Exception): Boolean {
|
||||||
|
return e is RetryableException
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSentTranscript(recipientsSet: Set<SignalServiceStoryMessageRecipient>): SentTranscriptMessage {
|
||||||
|
return SentTranscriptMessage(
|
||||||
|
Optional.of(SignalServiceAddress(Recipient.self().requireServiceId())),
|
||||||
|
sentTimestamp,
|
||||||
|
Optional.empty(),
|
||||||
|
0,
|
||||||
|
emptyMap(),
|
||||||
|
true,
|
||||||
|
Optional.empty(),
|
||||||
|
recipientsSet
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
|
class RetryableException : Exception()
|
||||||
|
|
||||||
|
class Factory : Job.Factory<MultiDeviceStorySendSyncJob> {
|
||||||
|
override fun create(parameters: Parameters, data: Data): MultiDeviceStorySendSyncJob {
|
||||||
|
return MultiDeviceStorySendSyncJob(
|
||||||
|
parameters = parameters,
|
||||||
|
sentTimestamp = data.getLong(DATA_SENT_TIMESTAMP),
|
||||||
|
deletedMessageId = data.getLong(DATA_DELETED_MESSAGE_ID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
|
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||||
|
@ -36,6 +37,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -175,6 +177,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
||||||
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
||||||
|
|
||||||
PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, targets, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches);
|
PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, targets, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches);
|
||||||
|
|
||||||
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
||||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
||||||
database.markAsSentFailed(messageId);
|
database.markAsSentFailed(messageId);
|
||||||
|
@ -206,7 +209,11 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
||||||
} else {
|
} else {
|
||||||
throw new UndeliverableMessageException("No attachment on non-text story.");
|
throw new UndeliverableMessageException("No attachment on non-text story.");
|
||||||
}
|
}
|
||||||
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
|
|
||||||
|
SentStorySyncManifest manifest = SignalDatabase.storySends().getFullSentStorySyncManifest(messageId, message.getSentTimeMillis());
|
||||||
|
Set<SignalServiceStoryMessageRecipient> manifestCollection = manifest != null ? manifest.toRecipientsSet() : Collections.emptySet();
|
||||||
|
|
||||||
|
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage, manifestCollection);
|
||||||
} catch (ServerRejectedException e) {
|
} catch (ServerRejectedException e) {
|
||||||
throw new UndeliverableMessageException(e);
|
throw new UndeliverableMessageException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.signal.core.util.SetUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -23,7 +24,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||||
import org.signal.core.util.SetUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
|
@ -53,9 +53,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||||
|
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static @NonNull RemoteDeleteSendJob create(@NonNull Context context,
|
public static @NonNull JobManager.Chain create(long messageId, boolean isMms)
|
||||||
long messageId,
|
|
||||||
boolean isMms)
|
|
||||||
throws NoSuchMessageException
|
throws NoSuchMessageException
|
||||||
{
|
{
|
||||||
MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId)
|
MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId)
|
||||||
|
@ -70,6 +68,9 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||||
List<RecipientId> recipients;
|
List<RecipientId> recipients;
|
||||||
if (conversationRecipient.isDistributionList()) {
|
if (conversationRecipient.isDistributionList()) {
|
||||||
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
|
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
|
||||||
|
if (recipients.isEmpty()) {
|
||||||
|
return ApplicationDependencies.getJobManager().startChain(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
||||||
: Stream.of(conversationRecipient.getId()).toList();
|
: Stream.of(conversationRecipient.getId()).toList();
|
||||||
|
@ -77,15 +78,23 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||||
|
|
||||||
recipients.remove(Recipient.self().getId());
|
recipients.remove(Recipient.self().getId());
|
||||||
|
|
||||||
return new RemoteDeleteSendJob(messageId,
|
RemoteDeleteSendJob sendJob = new RemoteDeleteSendJob(messageId,
|
||||||
isMms,
|
isMms,
|
||||||
recipients,
|
recipients,
|
||||||
recipients.size(),
|
recipients.size(),
|
||||||
new Parameters.Builder()
|
new Parameters.Builder()
|
||||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
.setMaxAttempts(Parameters.UNLIMITED)
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
if (conversationRecipient.isDistributionList()) {
|
||||||
|
return ApplicationDependencies.getJobManager()
|
||||||
|
.startChain(sendJob)
|
||||||
|
.then(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId));
|
||||||
|
} else {
|
||||||
|
return ApplicationDependencies.getJobManager().startChain(sendJob);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RemoteDeleteSendJob(long messageId,
|
private RemoteDeleteSendJob(long messageId,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
||||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||||
import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
|
import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||||
|
@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
@ -49,6 +51,7 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteLi
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -157,7 +160,8 @@ public final class GroupSendUtil {
|
||||||
boolean isRecipientUpdate,
|
boolean isRecipientUpdate,
|
||||||
@NonNull MessageId messageId,
|
@NonNull MessageId messageId,
|
||||||
long sentTimestamp,
|
long sentTimestamp,
|
||||||
@NonNull SignalServiceStoryMessage message)
|
@NonNull SignalServiceStoryMessage message,
|
||||||
|
@NonNull Set<SignalServiceStoryMessageRecipient> manifest)
|
||||||
throws IOException, UntrustedIdentityException
|
throws IOException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
return sendMessage(
|
return sendMessage(
|
||||||
|
@ -167,7 +171,7 @@ public final class GroupSendUtil {
|
||||||
messageId,
|
messageId,
|
||||||
allTargets,
|
allTargets,
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
new StorySendOperation(messageId, null, sentTimestamp, message),
|
new StorySendOperation(messageId, null, sentTimestamp, message, manifest),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +197,7 @@ public final class GroupSendUtil {
|
||||||
messageId,
|
messageId,
|
||||||
allTargets,
|
allTargets,
|
||||||
isRecipientUpdate,
|
isRecipientUpdate,
|
||||||
new StorySendOperation(messageId, groupId, sentTimestamp, message),
|
new StorySendOperation(messageId, groupId, sentTimestamp, message, Collections.emptySet()),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,16 +609,23 @@ public final class GroupSendUtil {
|
||||||
|
|
||||||
public static class StorySendOperation implements SendOperation {
|
public static class StorySendOperation implements SendOperation {
|
||||||
|
|
||||||
private final MessageId relatedMessageId;
|
private final MessageId relatedMessageId;
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
private final long sentTimestamp;
|
private final long sentTimestamp;
|
||||||
private final SignalServiceStoryMessage message;
|
private final SignalServiceStoryMessage message;
|
||||||
|
private final Set<SignalServiceStoryMessageRecipient> manifest;
|
||||||
|
|
||||||
public StorySendOperation(@NonNull MessageId relatedMessageId, @Nullable GroupId groupId, long sentTimestamp, @NonNull SignalServiceStoryMessage message) {
|
public StorySendOperation(@NonNull MessageId relatedMessageId,
|
||||||
|
@Nullable GroupId groupId,
|
||||||
|
long sentTimestamp,
|
||||||
|
@NonNull SignalServiceStoryMessage message,
|
||||||
|
@NonNull Set<SignalServiceStoryMessageRecipient> manifest)
|
||||||
|
{
|
||||||
this.relatedMessageId = relatedMessageId;
|
this.relatedMessageId = relatedMessageId;
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.sentTimestamp = sentTimestamp;
|
this.sentTimestamp = sentTimestamp;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
this.manifest = manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -625,7 +636,7 @@ public final class GroupSendUtil {
|
||||||
boolean isRecipientUpdate)
|
boolean isRecipientUpdate)
|
||||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
|
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
|
||||||
{
|
{
|
||||||
return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, message, getSentTimestamp());
|
return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, message, getSentTimestamp(), manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -637,7 +648,7 @@ public final class GroupSendUtil {
|
||||||
@Nullable CancelationSignal cancelationSignal)
|
@Nullable CancelationSignal cancelationSignal)
|
||||||
throws IOException, UntrustedIdentityException
|
throws IOException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
return messageSender.sendStory(targets, access, message, getSentTimestamp());
|
return messageSender.sendStory(targets, access, message, getSentTimestamp(), manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -39,14 +39,17 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
||||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||||
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||||
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.PaymentMetaDataUtil;
|
import org.thoughtcrime.securesms.database.PaymentMetaDataUtil;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||||
import org.thoughtcrime.securesms.database.model.Mention;
|
import org.thoughtcrime.securesms.database.model.Mention;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageLogEntry;
|
import org.thoughtcrime.securesms.database.model.MessageLogEntry;
|
||||||
|
@ -513,11 +516,11 @@ public final class MessageContentProcessor {
|
||||||
private static @Nullable SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) {
|
private static @Nullable SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) {
|
||||||
if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) {
|
if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) {
|
||||||
return content.getDataMessage().get().getGroupContext().get();
|
return content.getDataMessage().get().getGroupContext().get();
|
||||||
} else if (content.getSyncMessage().isPresent() &&
|
} else if (content.getSyncMessage().isPresent() &&
|
||||||
content.getSyncMessage().get().getSent().isPresent() &&
|
content.getSyncMessage().get().getSent().isPresent() &&
|
||||||
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent())
|
content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().isPresent())
|
||||||
{
|
{
|
||||||
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get();
|
return content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().get();
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1203,9 +1206,15 @@ public final class MessageContentProcessor {
|
||||||
try {
|
try {
|
||||||
GroupDatabase groupDatabase = SignalDatabase.groups();
|
GroupDatabase groupDatabase = SignalDatabase.groups();
|
||||||
|
|
||||||
if (message.getMessage().isGroupV2Message()) {
|
if (message.getStoryMessage().isPresent() || !message.getStoryMessageRecipients().isEmpty()) {
|
||||||
GroupId.V2 groupId = GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey());
|
handleSynchronizeSentStoryMessage(message, content.getTimestamp());
|
||||||
if (handleGv2PreProcessing(groupId, content, message.getMessage().getGroupContext().get().getGroupV2().get(), senderRecipient)) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceDataMessage dataMessage = message.getDataMessage().get();
|
||||||
|
if (dataMessage.isGroupV2Message()) {
|
||||||
|
GroupId.V2 groupId = GroupId.v2(dataMessage.getGroupContext().get().getGroupV2().get().getMasterKey());
|
||||||
|
if (handleGv2PreProcessing(groupId, content, dataMessage.getGroupContext().get().getGroupV2().get(), senderRecipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1214,38 +1223,38 @@ public final class MessageContentProcessor {
|
||||||
|
|
||||||
if (message.isRecipientUpdate()) {
|
if (message.isRecipientUpdate()) {
|
||||||
handleGroupRecipientUpdate(message, content.getTimestamp());
|
handleGroupRecipientUpdate(message, content.getTimestamp());
|
||||||
} else if (message.getMessage().isEndSession()) {
|
} else if (dataMessage.isEndSession()) {
|
||||||
threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp());
|
threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp());
|
||||||
} else if (message.getMessage().isGroupV1Update()) {
|
} else if (dataMessage.isGroupV1Update()) {
|
||||||
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true);
|
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, dataMessage, true);
|
||||||
threadId = gv1ThreadId == null ? -1 : gv1ThreadId;
|
threadId = gv1ThreadId == null ? -1 : gv1ThreadId;
|
||||||
} else if (message.getMessage().isGroupV2Update()) {
|
} else if (dataMessage.isGroupV2Update()) {
|
||||||
handleSynchronizeSentGv2Update(content, message);
|
handleSynchronizeSentGv2Update(content, message);
|
||||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
||||||
} else if (Build.VERSION.SDK_INT > 19 && message.getMessage().getGroupCallUpdate().isPresent()) {
|
} else if (Build.VERSION.SDK_INT > 19 && dataMessage.getGroupCallUpdate().isPresent()) {
|
||||||
handleGroupCallUpdateMessage(content, message.getMessage(), GroupUtil.idFromGroupContext(message.getMessage().getGroupContext()), senderRecipient);
|
handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient);
|
||||||
} else if (message.getMessage().isEmptyGroupV2Message()) {
|
} else if (dataMessage.isEmptyGroupV2Message()) {
|
||||||
warn(content.getTimestamp(), "Empty GV2 message! Doing nothing.");
|
warn(content.getTimestamp(), "Empty GV2 message! Doing nothing.");
|
||||||
} else if (message.getMessage().isExpirationUpdate()) {
|
} else if (dataMessage.isExpirationUpdate()) {
|
||||||
threadId = handleSynchronizeSentExpirationUpdate(message);
|
threadId = handleSynchronizeSentExpirationUpdate(message);
|
||||||
} else if (message.getMessage().getStoryContext().isPresent()) {
|
} else if (dataMessage.getStoryContext().isPresent()) {
|
||||||
threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp());
|
threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp());
|
||||||
} else if (message.getMessage().getReaction().isPresent()) {
|
} else if (dataMessage.getReaction().isPresent()) {
|
||||||
handleReaction(content, message.getMessage(), senderRecipient);
|
handleReaction(content, dataMessage, senderRecipient);
|
||||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
||||||
} else if (message.getMessage().getRemoteDelete().isPresent()) {
|
} else if (dataMessage.getRemoteDelete().isPresent()) {
|
||||||
handleRemoteDelete(content, message.getMessage(), senderRecipient);
|
handleRemoteDelete(content, dataMessage, senderRecipient);
|
||||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce() || message.getMessage().getMentions().isPresent()) {
|
} else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent()) {
|
||||||
threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp());
|
threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp());
|
||||||
} else {
|
} else {
|
||||||
threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp());
|
threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
|
if (dataMessage.getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(dataMessage.getGroupContext().get()))) {
|
||||||
handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get(), senderRecipient);
|
handleUnknownGroupMessage(content, dataMessage.getGroupContext().get(), senderRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.getMessage().getProfileKey().isPresent()) {
|
if (dataMessage.getProfileKey().isPresent()) {
|
||||||
Recipient recipient = getSyncMessageDestination(message);
|
Recipient recipient = getSyncMessageDestination(message);
|
||||||
|
|
||||||
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
||||||
|
@ -1275,8 +1284,9 @@ public final class MessageContentProcessor {
|
||||||
{
|
{
|
||||||
log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp());
|
log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp());
|
||||||
|
|
||||||
SignalServiceGroupV2 signalServiceGroupV2 = message.getMessage().getGroupContext().get().getGroupV2().get();
|
SignalServiceDataMessage dataMessage = message.getDataMessage().get();
|
||||||
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey());
|
SignalServiceGroupV2 signalServiceGroupV2 = dataMessage.getGroupContext().get().getGroupV2().get();
|
||||||
|
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey());
|
||||||
|
|
||||||
if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) {
|
if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) {
|
||||||
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
|
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
|
||||||
|
@ -1873,14 +1883,14 @@ public final class MessageContentProcessor {
|
||||||
|
|
||||||
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
|
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
|
||||||
message.getTimestamp(),
|
message.getTimestamp(),
|
||||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||||
|
|
||||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||||
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
||||||
|
|
||||||
database.markAsSent(messageId, true);
|
database.markAsSent(messageId, true);
|
||||||
|
|
||||||
SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getMessage().getExpiresInSeconds());
|
SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getDataMessage().get().getExpiresInSeconds());
|
||||||
|
|
||||||
return threadId;
|
return threadId;
|
||||||
}
|
}
|
||||||
|
@ -1899,9 +1909,9 @@ public final class MessageContentProcessor {
|
||||||
log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp());
|
log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Optional<SignalServiceDataMessage.Reaction> reaction = message.getMessage().getReaction();
|
Optional<SignalServiceDataMessage.Reaction> reaction = message.getDataMessage().get().getReaction();
|
||||||
ParentStoryId parentStoryId;
|
ParentStoryId parentStoryId;
|
||||||
SignalServiceDataMessage.StoryContext storyContext = message.getMessage().getStoryContext().get();
|
SignalServiceDataMessage.StoryContext storyContext = message.getDataMessage().get().getStoryContext().get();
|
||||||
MessageDatabase database = SignalDatabase.mms();
|
MessageDatabase database = SignalDatabase.mms();
|
||||||
Recipient recipient = getSyncMessageDestination(message);
|
Recipient recipient = getSyncMessageDestination(message);
|
||||||
QuoteModel quoteModel = null;
|
QuoteModel quoteModel = null;
|
||||||
|
@ -1916,10 +1926,10 @@ public final class MessageContentProcessor {
|
||||||
if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) {
|
if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) {
|
||||||
body = reaction.get().getEmoji();
|
body = reaction.get().getEmoji();
|
||||||
} else {
|
} else {
|
||||||
body = message.getMessage().getBody().orElse(null);
|
body = message.getDataMessage().get().getBody().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.getMessage().getGroupContext().isPresent()) {
|
if (message.getDataMessage().get().getGroupContext().isPresent()) {
|
||||||
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
|
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
|
||||||
} else if (groupStory || SignalDatabase.storySends().canReply(storyAuthorRecipient, storyContext.getSentTimestamp())) {
|
} else if (groupStory || SignalDatabase.storySends().canReply(storyAuthorRecipient, storyContext.getSentTimestamp())) {
|
||||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||||
|
@ -1946,18 +1956,18 @@ public final class MessageContentProcessor {
|
||||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||||
StoryType.NONE,
|
StoryType.NONE,
|
||||||
parentStoryId,
|
parentStoryId,
|
||||||
message.getMessage().getReaction().isPresent(),
|
message.getDataMessage().get().getReaction().isPresent(),
|
||||||
quoteModel,
|
quoteModel,
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
getMentions(message.getMessage().getMentions()).orElse(Collections.emptyList()),
|
getMentions(message.getDataMessage().get().getMentions()).orElse(Collections.emptyList()),
|
||||||
Collections.emptySet(),
|
Collections.emptySet(),
|
||||||
Collections.emptySet(),
|
Collections.emptySet(),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||||
|
|
||||||
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||||
handleSynchronizeSentExpirationUpdate(message);
|
handleSynchronizeSentExpirationUpdate(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1976,13 +1986,13 @@ public final class MessageContentProcessor {
|
||||||
|
|
||||||
database.markAsSent(messageId, true);
|
database.markAsSent(messageId, true);
|
||||||
|
|
||||||
if (message.getMessage().getExpiresInSeconds() > 0) {
|
if (message.getDataMessage().get().getExpiresInSeconds() > 0) {
|
||||||
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
||||||
ApplicationDependencies.getExpiringMessageManager()
|
ApplicationDependencies.getExpiringMessageManager()
|
||||||
.scheduleDeletion(messageId,
|
.scheduleDeletion(messageId,
|
||||||
true,
|
true,
|
||||||
message.getExpirationStartTimestamp(),
|
message.getExpirationStartTimestamp(),
|
||||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipient.isSelf()) {
|
if (recipient.isSelf()) {
|
||||||
|
@ -2003,6 +2013,129 @@ public final class MessageContentProcessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleSynchronizeSentStoryMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException {
|
||||||
|
log(envelopeTimestamp, "Synchronize sent story message for " + message.getTimestamp());
|
||||||
|
|
||||||
|
SentStorySyncManifest manifest = SentStorySyncManifest.fromRecipientsSet(message.getStoryMessageRecipients());
|
||||||
|
|
||||||
|
if (message.isRecipientUpdate()) {
|
||||||
|
log(envelopeTimestamp, "Processing recipient update for story message and exiting...");
|
||||||
|
SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalServiceStoryMessage storyMessage = message.getStoryMessage().get();
|
||||||
|
Set<DistributionId> distributionIds = manifest.getDistributionIdSet();
|
||||||
|
Optional<GroupId> groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey()));
|
||||||
|
String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null);
|
||||||
|
StoryType storyType = getStoryType(storyMessage);
|
||||||
|
List<LinkPreview> linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)),
|
||||||
|
"",
|
||||||
|
true).orElse(Collections.emptyList());
|
||||||
|
List<Attachment> attachments = PointerAttachment.forPointers(storyMessage.getFileAttachment()
|
||||||
|
.map(SignalServiceAttachment::asPointer)
|
||||||
|
.map(Collections::singletonList));
|
||||||
|
|
||||||
|
for (final DistributionId distributionId : distributionIds) {
|
||||||
|
RecipientId distributionRecipientId = SignalDatabase.distributionLists().getOrCreateByDistributionId(distributionId, manifest);
|
||||||
|
Recipient distributionListRecipient = Recipient.resolved(distributionRecipientId);
|
||||||
|
insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId.isPresent()) {
|
||||||
|
Optional<RecipientId> groupRecipient = SignalDatabase.recipients().getByGroupId(groupId.get());
|
||||||
|
if (groupRecipient.isPresent()) {
|
||||||
|
insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertSentStoryMessage(@NonNull SentTranscriptMessage message,
|
||||||
|
@NonNull Recipient recipient,
|
||||||
|
@Nullable String textStoryBody,
|
||||||
|
@NonNull List<Attachment> pendingAttachments,
|
||||||
|
long sentAtTimestamp,
|
||||||
|
@NonNull StoryType storyType,
|
||||||
|
@NonNull List<LinkPreview> linkPreviews)
|
||||||
|
throws MmsException
|
||||||
|
{
|
||||||
|
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
|
||||||
|
textStoryBody,
|
||||||
|
pendingAttachments,
|
||||||
|
sentAtTimestamp,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||||
|
storyType,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
Collections.emptyList(),
|
||||||
|
linkPreviews,
|
||||||
|
Collections.emptyList(),
|
||||||
|
Collections.emptySet(),
|
||||||
|
Collections.emptySet(),
|
||||||
|
null);
|
||||||
|
|
||||||
|
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||||
|
|
||||||
|
MmsDatabase database = SignalDatabase.mms();
|
||||||
|
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||||
|
|
||||||
|
long messageId;
|
||||||
|
List<DatabaseAttachment> attachments;
|
||||||
|
|
||||||
|
database.beginTransaction();
|
||||||
|
try {
|
||||||
|
messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
|
||||||
|
|
||||||
|
if (recipient.isGroup()) {
|
||||||
|
updateGroupReceiptStatus(message, messageId, recipient.requireGroupId());
|
||||||
|
} else {
|
||||||
|
database.markUnidentified(messageId, isUnidentified(message, recipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
database.markAsSent(messageId, true);
|
||||||
|
|
||||||
|
List<DatabaseAttachment> allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId);
|
||||||
|
|
||||||
|
attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||||
|
|
||||||
|
if (recipient.isSelf()) {
|
||||||
|
SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp());
|
||||||
|
SignalDatabase.mmsSms().incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
||||||
|
SignalDatabase.mmsSms().incrementReadReceiptCount(id, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
database.setTransactionSuccessful();
|
||||||
|
} finally {
|
||||||
|
database.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DatabaseAttachment attachment : attachments) {
|
||||||
|
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull StoryType getStoryType(SignalServiceStoryMessage storyMessage) {
|
||||||
|
if (storyMessage.getAllowsReplies().orElse(false)) {
|
||||||
|
if (storyMessage.getTextAttachment().isPresent()) {
|
||||||
|
return StoryType.TEXT_STORY_WITH_REPLIES;
|
||||||
|
} else {
|
||||||
|
return StoryType.STORY_WITH_REPLIES;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (storyMessage.getTextAttachment().isPresent()) {
|
||||||
|
return StoryType.TEXT_STORY_WITHOUT_REPLIES;
|
||||||
|
} else {
|
||||||
|
return StoryType.STORY_WITHOUT_REPLIES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp)
|
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp)
|
||||||
throws MmsException, BadGroupIdException
|
throws MmsException, BadGroupIdException
|
||||||
{
|
{
|
||||||
|
@ -2010,26 +2143,26 @@ public final class MessageContentProcessor {
|
||||||
|
|
||||||
MessageDatabase database = SignalDatabase.mms();
|
MessageDatabase database = SignalDatabase.mms();
|
||||||
Recipient recipients = getSyncMessageDestination(message);
|
Recipient recipients = getSyncMessageDestination(message);
|
||||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
Optional<QuoteModel> quote = getValidatedQuote(message.getDataMessage().get().getQuote());
|
||||||
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
Optional<Attachment> sticker = getStickerAttachment(message.getDataMessage().get().getSticker());
|
||||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
Optional<List<Contact>> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts());
|
||||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().orElse(""), false);
|
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false);
|
||||||
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions());
|
Optional<List<Mention>> mentions = getMentions(message.getDataMessage().get().getMentions());
|
||||||
Optional<GiftBadge> giftBadge = getGiftBadge(message.getMessage().getGiftBadge());
|
Optional<GiftBadge> giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge());
|
||||||
boolean viewOnce = message.getMessage().isViewOnce();
|
boolean viewOnce = message.getDataMessage().get().isViewOnce();
|
||||||
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
||||||
: PointerAttachment.forPointers(message.getMessage().getAttachments());
|
: PointerAttachment.forPointers(message.getDataMessage().get().getAttachments());
|
||||||
|
|
||||||
if (sticker.isPresent()) {
|
if (sticker.isPresent()) {
|
||||||
syncAttachments.add(sticker.get());
|
syncAttachments.add(sticker.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients,
|
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients,
|
||||||
message.getMessage().getBody().orElse(null),
|
message.getDataMessage().get().getBody().orElse(null),
|
||||||
syncAttachments,
|
syncAttachments,
|
||||||
message.getTimestamp(),
|
message.getTimestamp(),
|
||||||
-1,
|
-1,
|
||||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()),
|
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()),
|
||||||
viewOnce,
|
viewOnce,
|
||||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||||
StoryType.NONE,
|
StoryType.NONE,
|
||||||
|
@ -2045,7 +2178,7 @@ public final class MessageContentProcessor {
|
||||||
|
|
||||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||||
|
|
||||||
if (recipients.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||||
handleSynchronizeSentExpirationUpdate(message);
|
handleSynchronizeSentExpirationUpdate(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2072,13 +2205,13 @@ public final class MessageContentProcessor {
|
||||||
stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
||||||
attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||||
|
|
||||||
if (message.getMessage().getExpiresInSeconds() > 0) {
|
if (message.getDataMessage().get().getExpiresInSeconds() > 0) {
|
||||||
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
||||||
ApplicationDependencies.getExpiringMessageManager()
|
ApplicationDependencies.getExpiringMessageManager()
|
||||||
.scheduleDeletion(messageId,
|
.scheduleDeletion(messageId,
|
||||||
true,
|
true,
|
||||||
message.getExpirationStartTimestamp(),
|
message.getExpirationStartTimestamp(),
|
||||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipients.isSelf()) {
|
if (recipients.isSelf()) {
|
||||||
|
@ -2204,10 +2337,10 @@ public final class MessageContentProcessor {
|
||||||
log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp());
|
log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp());
|
||||||
|
|
||||||
Recipient recipient = getSyncMessageDestination(message);
|
Recipient recipient = getSyncMessageDestination(message);
|
||||||
String body = message.getMessage().getBody().orElse("");
|
String body = message.getDataMessage().get().getBody().orElse("");
|
||||||
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds());
|
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||||
|
|
||||||
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||||
handleSynchronizeSentExpirationUpdate(message);
|
handleSynchronizeSentExpirationUpdate(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2862,7 +2995,7 @@ public final class MessageContentProcessor {
|
||||||
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
|
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
|
||||||
throws BadGroupIdException
|
throws BadGroupIdException
|
||||||
{
|
{
|
||||||
return getGroupRecipient(message.getMessage().getGroupContext()).orElseGet(() -> Recipient.externalPush(message.getDestination().get()));
|
return getGroupRecipient(message.getDataMessage().get().getGroupContext()).orElseGet(() -> Recipient.externalPush(message.getDestination().get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Recipient getMessageDestination(@NonNull SignalServiceContent content) throws BadGroupIdException {
|
private Recipient getMessageDestination(@NonNull SignalServiceContent content) throws BadGroupIdException {
|
||||||
|
|
|
@ -251,7 +251,7 @@ public final class LiveRecipient {
|
||||||
|
|
||||||
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
|
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
|
||||||
if (groupRecord != null) {
|
if (groupRecord != null) {
|
||||||
String title = groupRecord.getName();
|
String title = groupRecord.isUnknown() ? null : groupRecord.getName();
|
||||||
List<Recipient> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList();
|
List<Recipient> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList();
|
||||||
|
|
||||||
return RecipientDetails.forDistributionList(title, members, record);
|
return RecipientDetails.forDistributionList(title, members, record);
|
||||||
|
|
|
@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -86,6 +87,7 @@ import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -164,8 +166,9 @@ public class MessageSender {
|
||||||
Recipient recipient = message.getRecipient();
|
Recipient recipient = message.getRecipient();
|
||||||
|
|
||||||
if (recipient.isDistributionList()) {
|
if (recipient.isDistributionList()) {
|
||||||
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
|
||||||
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies());
|
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
||||||
|
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,8 +384,9 @@ public class MessageSender {
|
||||||
Recipient recipient = message.getRecipient();
|
Recipient recipient = message.getRecipient();
|
||||||
|
|
||||||
if (recipient.isDistributionList()) {
|
if (recipient.isDistributionList()) {
|
||||||
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
||||||
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies());
|
DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
|
||||||
|
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,7 +475,7 @@ public class MessageSender {
|
||||||
db.markAsSending(messageId);
|
db.markAsSending(messageId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ApplicationDependencies.getJobManager().add(RemoteDeleteSendJob.create(context, messageId, isMms));
|
RemoteDeleteSendJob.create(messageId, isMms).enqueue();
|
||||||
onMessageSent();
|
onMessageSent();
|
||||||
} catch (NoSuchMessageException e) {
|
} catch (NoSuchMessageException e) {
|
||||||
Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring.");
|
Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring.");
|
||||||
|
|
|
@ -118,7 +118,7 @@ object PrivateStoryItem {
|
||||||
|
|
||||||
override fun bind(model: PartialModel) {
|
override fun bind(model: PartialModel) {
|
||||||
itemView.setOnClickListener { model.onClick(model) }
|
itemView.setOnClickListener { model.onClick(model) }
|
||||||
label.text = model.privateStoryItemData.name
|
label.text = if (model.privateStoryItemData.isUnknown) context.getString(R.string.MessageRecord_unknown) else model.privateStoryItemData.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,9 +51,10 @@ public final class GroupUtil {
|
||||||
return content.getDataMessage().get().getGroupContext().get();
|
return content.getDataMessage().get().getGroupContext().get();
|
||||||
} else if (content.getSyncMessage().isPresent() &&
|
} else if (content.getSyncMessage().isPresent() &&
|
||||||
content.getSyncMessage().get().getSent().isPresent() &&
|
content.getSyncMessage().get().getSent().isPresent() &&
|
||||||
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent())
|
content.getSyncMessage().get().getSent().get().getDataMessage().isPresent() &&
|
||||||
|
content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().isPresent())
|
||||||
{
|
{
|
||||||
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get();
|
return content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().get();
|
||||||
} else if (content.getStoryMessage().isPresent() && content.getStoryMessage().get().getGroupContext().isPresent()) {
|
} else if (content.getStoryMessage().isPresent() && content.getStoryMessage().get().getGroupContext().isPresent()) {
|
||||||
try {
|
try {
|
||||||
return SignalServiceGroupContext.create(null, content.getStoryMessage().get().getGroupContext().get());
|
return SignalServiceGroupContext.create(null, content.getStoryMessage().get().getGroupContext().get());
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||||
|
@ -252,32 +253,43 @@ public class SignalServiceMessageSender {
|
||||||
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY);
|
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
|
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
|
||||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||||
SignalServiceStoryMessage message,
|
SignalServiceStoryMessage message,
|
||||||
long timestamp)
|
long timestamp,
|
||||||
|
Set<SignalServiceStoryMessageRecipient> manifest)
|
||||||
throws IOException, UntrustedIdentityException
|
throws IOException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
Content content = createStoryContent(message);
|
Content content = createStoryContent(message);
|
||||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty());
|
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty());
|
||||||
|
List<SendMessageResult> sendMessageResults = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null);
|
||||||
|
SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, manifest);
|
||||||
|
|
||||||
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null);
|
sendSyncMessage(syncMessage, Optional.empty());
|
||||||
|
|
||||||
|
return sendMessageResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
|
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
||||||
Optional<byte[]> groupId,
|
Optional<byte[]> groupId,
|
||||||
List<SignalServiceAddress> recipients,
|
List<SignalServiceAddress> recipients,
|
||||||
List<UnidentifiedAccess> unidentifiedAccess,
|
List<UnidentifiedAccess> unidentifiedAccess,
|
||||||
SignalServiceStoryMessage message,
|
SignalServiceStoryMessage message,
|
||||||
long timestamp)
|
long timestamp,
|
||||||
|
Set<SignalServiceStoryMessageRecipient> manifest)
|
||||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
||||||
{
|
{
|
||||||
Content content = createStoryContent(message);
|
Content content = createStoryContent(message);
|
||||||
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY);
|
List<SendMessageResult> sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY);
|
||||||
|
SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, manifest);
|
||||||
|
|
||||||
|
sendSyncMessage(syncMessage, Optional.empty());
|
||||||
|
|
||||||
|
return sendMessageResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -362,7 +374,7 @@ public class SignalServiceMessageSender {
|
||||||
sendEvents.onMessageSent();
|
sendEvents.onMessageSent();
|
||||||
|
|
||||||
if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
|
if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
|
||||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
|
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet());
|
||||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||||
|
|
||||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
||||||
|
@ -446,7 +458,7 @@ public class SignalServiceMessageSender {
|
||||||
sendEvents.onMessageSent();
|
sendEvents.onMessageSent();
|
||||||
|
|
||||||
if (store.isMultiDevice()) {
|
if (store.isMultiDevice()) {
|
||||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate);
|
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet());
|
||||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||||
|
|
||||||
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null);
|
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null);
|
||||||
|
@ -496,7 +508,7 @@ public class SignalServiceMessageSender {
|
||||||
recipient = Optional.of(recipients.get(0));
|
recipient = Optional.of(recipients.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate);
|
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
|
||||||
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
|
||||||
|
|
||||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
||||||
|
@ -791,6 +803,16 @@ public class SignalServiceMessageSender {
|
||||||
return container.setReceiptMessage(builder).build();
|
return container.setReceiptMessage(builder).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Content createMessageContent(SentTranscriptMessage transcriptMessage) throws IOException {
|
||||||
|
if (transcriptMessage.getStoryMessage().isPresent()) {
|
||||||
|
return createStoryContent(transcriptMessage.getStoryMessage().get());
|
||||||
|
} else if (transcriptMessage.getDataMessage().isPresent()) {
|
||||||
|
return createMessageContent(transcriptMessage.getDataMessage().get());
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
|
private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
|
||||||
Content.Builder container = Content.newBuilder();
|
Content.Builder container = Content.newBuilder();
|
||||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||||
|
@ -1108,27 +1130,30 @@ public class SignalServiceMessageSender {
|
||||||
|
|
||||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
|
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
|
||||||
SignalServiceAddress address = transcript.getDestination().get();
|
SignalServiceAddress address = transcript.getDestination().get();
|
||||||
Content content = createMessageContent(transcript.getMessage());
|
Content content = createMessageContent(transcript);
|
||||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.of(content));
|
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.ofNullable(content));
|
||||||
|
|
||||||
|
|
||||||
return createMultiDeviceSentTranscriptContent(content,
|
return createMultiDeviceSentTranscriptContent(content,
|
||||||
Optional.of(address),
|
Optional.of(address),
|
||||||
transcript.getTimestamp(),
|
transcript.getTimestamp(),
|
||||||
Collections.singletonList(result),
|
Collections.singletonList(result),
|
||||||
false);
|
transcript.isRecipientUpdate(),
|
||||||
|
transcript.getStoryMessageRecipients());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
|
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
|
||||||
long timestamp, List<SendMessageResult> sendMessageResults,
|
long timestamp, List<SendMessageResult> sendMessageResults,
|
||||||
boolean isRecipientUpdate)
|
boolean isRecipientUpdate,
|
||||||
|
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
|
||||||
{
|
{
|
||||||
Content.Builder container = Content.newBuilder();
|
Content.Builder container = Content.newBuilder();
|
||||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||||
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
||||||
DataMessage dataMessage = content.getDataMessage();
|
DataMessage dataMessage = content != null && content.hasDataMessage() ? content.getDataMessage() : null;
|
||||||
|
StoryMessage storyMessage = content != null && content.hasStoryMessage() ? content.getStoryMessage() : null;
|
||||||
|
|
||||||
sentMessage.setTimestamp(timestamp);
|
sentMessage.setTimestamp(timestamp);
|
||||||
sentMessage.setMessage(dataMessage);
|
|
||||||
|
|
||||||
for (SendMessageResult result : sendMessageResults) {
|
for (SendMessageResult result : sendMessageResults) {
|
||||||
if (result.getSuccess() != null) {
|
if (result.getSuccess() != null) {
|
||||||
|
@ -1144,20 +1169,39 @@ public class SignalServiceMessageSender {
|
||||||
sentMessage.setDestinationUuid(recipient.get().getServiceId().toString());
|
sentMessage.setDestinationUuid(recipient.get().getServiceId().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataMessage.getExpireTimer() > 0) {
|
if (dataMessage != null) {
|
||||||
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
|
sentMessage.setMessage(dataMessage);
|
||||||
|
if (dataMessage.getExpireTimer() > 0) {
|
||||||
|
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataMessage.getIsViewOnce()) {
|
||||||
|
dataMessage = dataMessage.toBuilder().clearAttachments().build();
|
||||||
|
sentMessage.setMessage(dataMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataMessage.getIsViewOnce()) {
|
if (storyMessage != null) {
|
||||||
dataMessage = dataMessage.toBuilder().clearAttachments().build();
|
sentMessage.setStoryMessage(storyMessage);
|
||||||
sentMessage.setMessage(dataMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sentMessage.addAllStoryMessageRecipients(storyMessageRecipients.stream()
|
||||||
|
.map(this::createStoryMessageRecipient)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
|
|
||||||
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
|
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
|
||||||
|
|
||||||
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build();
|
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SyncMessage.Sent.StoryMessageRecipient createStoryMessageRecipient(SignalServiceStoryMessageRecipient storyMessageRecipient) {
|
||||||
|
return SyncMessage.Sent.StoryMessageRecipient.newBuilder()
|
||||||
|
.addAllDistributionListIds(storyMessageRecipient.getDistributionListIds())
|
||||||
|
.setDestinationUuid(storyMessageRecipient.getSignalServiceAddress().getIdentifier())
|
||||||
|
.setIsAllowedToReply(storyMessageRecipient.isAllowedToReply())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) {
|
private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) {
|
||||||
Content.Builder container = Content.newBuilder();
|
Content.Builder container = Content.newBuilder();
|
||||||
SyncMessage.Builder builder = createSyncMessageBuilder();
|
SyncMessage.Builder builder = createSyncMessageBuilder();
|
||||||
|
@ -1580,13 +1624,28 @@ public class SignalServiceMessageSender {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SignalServiceSyncMessage createSelfSendSyncMessageForStory(SignalServiceStoryMessage message, long sentTimestamp, Set<SignalServiceStoryMessageRecipient> manifest) {
|
||||||
|
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
|
||||||
|
sentTimestamp,
|
||||||
|
Optional.empty(),
|
||||||
|
0,
|
||||||
|
Collections.singletonMap(localAddress, false),
|
||||||
|
false,
|
||||||
|
Optional.of(message),
|
||||||
|
manifest);
|
||||||
|
|
||||||
|
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
||||||
|
}
|
||||||
|
|
||||||
private SignalServiceSyncMessage createSelfSendSyncMessage(SignalServiceDataMessage message) {
|
private SignalServiceSyncMessage createSelfSendSyncMessage(SignalServiceDataMessage message) {
|
||||||
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
|
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
|
||||||
message.getTimestamp(),
|
message.getTimestamp(),
|
||||||
message,
|
Optional.of(message),
|
||||||
message.getExpiresInSeconds(),
|
message.getExpiresInSeconds(),
|
||||||
Collections.singletonMap(localAddress, false),
|
Collections.singletonMap(localAddress, false),
|
||||||
false);
|
false,
|
||||||
|
Optional.empty(),
|
||||||
|
Collections.emptySet());
|
||||||
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,11 +58,14 @@ import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadata
|
||||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto;
|
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class SignalServiceContent {
|
public final class SignalServiceContent {
|
||||||
|
|
||||||
|
@ -674,14 +677,21 @@ public final class SignalServiceContent {
|
||||||
throws ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException
|
throws ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException
|
||||||
{
|
{
|
||||||
if (content.hasSent()) {
|
if (content.hasSent()) {
|
||||||
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
|
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
|
||||||
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
|
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
|
||||||
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
|
Optional<SignalServiceDataMessage> dataMessage = sentContent.hasMessage() ? Optional.of(createSignalServiceMessage(metadata, sentContent.getMessage())) : Optional.empty();
|
||||||
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
|
Optional<SignalServiceStoryMessage> storyMessage = sentContent.hasStoryMessage() ? Optional.of(createStoryMessage(sentContent.getStoryMessage())) : Optional.empty();
|
||||||
? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid())))
|
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
|
||||||
: Optional.empty();
|
? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid())))
|
||||||
|
: Optional.empty();
|
||||||
|
Set<SignalServiceStoryMessageRecipient> recipientManifest = sentContent.getStoryMessageRecipientsList()
|
||||||
|
.stream()
|
||||||
|
.map(SignalServiceContent::createSignalServiceStoryMessageRecipient)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) {
|
if (!address.isPresent() &&
|
||||||
|
!dataMessage.flatMap(SignalServiceDataMessage::getGroupContext).isPresent() &&
|
||||||
|
!storyMessage.flatMap(SignalServiceStoryMessage::getGroupContext).isPresent()) {
|
||||||
throw new InvalidMessageStructureException("SyncMessage missing both destination and group ID!");
|
throw new InvalidMessageStructureException("SyncMessage missing both destination and group ID!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -699,7 +709,9 @@ public final class SignalServiceContent {
|
||||||
dataMessage,
|
dataMessage,
|
||||||
sentContent.getExpirationStartTimestamp(),
|
sentContent.getExpirationStartTimestamp(),
|
||||||
unidentifiedStatuses,
|
unidentifiedStatuses,
|
||||||
sentContent.getIsRecipientUpdate()));
|
sentContent.getIsRecipientUpdate(),
|
||||||
|
storyMessage,
|
||||||
|
recipientManifest));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.hasRequest()) {
|
if (content.hasRequest()) {
|
||||||
|
@ -913,6 +925,14 @@ public final class SignalServiceContent {
|
||||||
return SignalServiceSyncMessage.empty();
|
return SignalServiceSyncMessage.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SignalServiceStoryMessageRecipient createSignalServiceStoryMessageRecipient(SignalServiceProtos.SyncMessage.Sent.StoryMessageRecipient storyMessageRecipient) {
|
||||||
|
return new SignalServiceStoryMessageRecipient(
|
||||||
|
new SignalServiceAddress(ServiceId.parseOrThrow(storyMessageRecipient.getDestinationUuid())),
|
||||||
|
storyMessageRecipient.getDistributionListIdsList(),
|
||||||
|
storyMessageRecipient.getIsAllowedToReply()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) {
|
private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) {
|
||||||
boolean isMultiRing = content.getMultiRing();
|
boolean isMultiRing = content.getMultiRing();
|
||||||
Integer destinationDeviceId = content.hasDestinationDeviceId() ? content.getDestinationDeviceId() : null;
|
Integer destinationDeviceId = content.hasDestinationDeviceId() ? content.getDestinationDeviceId() : null;
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.whispersystems.signalservice.api.messages;
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SignalServiceStoryMessageRecipient {
|
||||||
|
|
||||||
|
private final SignalServiceAddress signalServiceAddress;
|
||||||
|
private final List<String> distributionListIds;
|
||||||
|
private final boolean isAllowedToReply;
|
||||||
|
|
||||||
|
public SignalServiceStoryMessageRecipient(SignalServiceAddress signalServiceAddress,
|
||||||
|
List<String> distributionListIds,
|
||||||
|
boolean isAllowedToReply)
|
||||||
|
{
|
||||||
|
this.signalServiceAddress = signalServiceAddress;
|
||||||
|
this.distributionListIds = distributionListIds;
|
||||||
|
this.isAllowedToReply = isAllowedToReply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getDistributionListIds() {
|
||||||
|
return distributionListIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignalServiceAddress getSignalServiceAddress() {
|
||||||
|
return signalServiceAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowedToReply() {
|
||||||
|
return isAllowedToReply;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ package org.whispersystems.signalservice.api.messages.multidevice;
|
||||||
|
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
@ -18,21 +20,25 @@ import java.util.Set;
|
||||||
|
|
||||||
public class SentTranscriptMessage {
|
public class SentTranscriptMessage {
|
||||||
|
|
||||||
private final Optional<SignalServiceAddress> destination;
|
private final Optional<SignalServiceAddress> destination;
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final long expirationStartTimestamp;
|
private final long expirationStartTimestamp;
|
||||||
private final SignalServiceDataMessage message;
|
private final Optional<SignalServiceDataMessage> message;
|
||||||
private final Map<String, Boolean> unidentifiedStatusBySid;
|
private final Map<String, Boolean> unidentifiedStatusBySid;
|
||||||
private final Map<String, Boolean> unidentifiedStatusByE164;
|
private final Map<String, Boolean> unidentifiedStatusByE164;
|
||||||
private final Set<SignalServiceAddress> recipients;
|
private final Set<SignalServiceAddress> recipients;
|
||||||
private final boolean isRecipientUpdate;
|
private final boolean isRecipientUpdate;
|
||||||
|
private final Optional<SignalServiceStoryMessage> storyMessage;
|
||||||
|
private final Set<SignalServiceStoryMessageRecipient> storyMessageRecipients;
|
||||||
|
|
||||||
public SentTranscriptMessage(Optional<SignalServiceAddress> destination,
|
public SentTranscriptMessage(Optional<SignalServiceAddress> destination,
|
||||||
long timestamp,
|
long timestamp,
|
||||||
SignalServiceDataMessage message,
|
Optional<SignalServiceDataMessage> message,
|
||||||
long expirationStartTimestamp,
|
long expirationStartTimestamp,
|
||||||
Map<SignalServiceAddress, Boolean> unidentifiedStatus,
|
Map<SignalServiceAddress, Boolean> unidentifiedStatus,
|
||||||
boolean isRecipientUpdate)
|
boolean isRecipientUpdate,
|
||||||
|
Optional<SignalServiceStoryMessage> storyMessage,
|
||||||
|
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
|
||||||
{
|
{
|
||||||
this.destination = destination;
|
this.destination = destination;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
@ -42,6 +48,8 @@ public class SentTranscriptMessage {
|
||||||
this.unidentifiedStatusByE164 = new HashMap<>();
|
this.unidentifiedStatusByE164 = new HashMap<>();
|
||||||
this.recipients = unidentifiedStatus.keySet();
|
this.recipients = unidentifiedStatus.keySet();
|
||||||
this.isRecipientUpdate = isRecipientUpdate;
|
this.isRecipientUpdate = isRecipientUpdate;
|
||||||
|
this.storyMessage = storyMessage;
|
||||||
|
this.storyMessageRecipients = storyMessageRecipients;
|
||||||
|
|
||||||
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
|
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
|
||||||
unidentifiedStatusBySid.put(entry.getKey().getServiceId().toString(), entry.getValue());
|
unidentifiedStatusBySid.put(entry.getKey().getServiceId().toString(), entry.getValue());
|
||||||
|
@ -64,10 +72,18 @@ public class SentTranscriptMessage {
|
||||||
return expirationStartTimestamp;
|
return expirationStartTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceDataMessage getMessage() {
|
public Optional<SignalServiceDataMessage> getDataMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<SignalServiceStoryMessage> getStoryMessage() {
|
||||||
|
return storyMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<SignalServiceStoryMessageRecipient> getStoryMessageRecipients() {
|
||||||
|
return storyMessageRecipients;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isUnidentified(ServiceId serviceId) {
|
public boolean isUnidentified(ServiceId serviceId) {
|
||||||
return isUnidentified(serviceId.toString());
|
return isUnidentified(serviceId.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.push;
|
||||||
|
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,4 +41,17 @@ public final class DistributionId {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return uuid.toString();
|
return uuid.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
final DistributionId that = (DistributionId) o;
|
||||||
|
return Objects.equals(uuid, that.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(uuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,6 +427,12 @@ message SyncMessage {
|
||||||
optional bool unidentified = 2;
|
optional bool unidentified = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StoryMessageRecipient {
|
||||||
|
optional string destinationUuid = 1;
|
||||||
|
repeated string distributionListIds = 2;
|
||||||
|
optional bool isAllowedToReply = 3;
|
||||||
|
}
|
||||||
|
|
||||||
reserved /*destinationE164*/ 1;
|
reserved /*destinationE164*/ 1;
|
||||||
optional string destinationUuid = 7;
|
optional string destinationUuid = 7;
|
||||||
optional uint64 timestamp = 2;
|
optional uint64 timestamp = 2;
|
||||||
|
@ -434,6 +440,8 @@ message SyncMessage {
|
||||||
optional uint64 expirationStartTimestamp = 4;
|
optional uint64 expirationStartTimestamp = 4;
|
||||||
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
|
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
|
||||||
optional bool isRecipientUpdate = 6 [default = false];
|
optional bool isRecipientUpdate = 6 [default = false];
|
||||||
|
optional StoryMessage storyMessage = 8;
|
||||||
|
repeated StoryMessageRecipient storyMessageRecipients = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contacts {
|
message Contacts {
|
||||||
|
|
Loading…
Add table
Reference in a new issue