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
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.containsInAnyOrder
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
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 recipients11to20: List<RecipientId>
|
||||
private lateinit var recipients6to15: List<RecipientId>
|
||||
|
@ -31,22 +51,41 @@ class StorySendsDatabaseTest {
|
|||
fun setup() {
|
||||
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)
|
||||
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)
|
||||
recipients6to10 = recipients1to10.takeLast(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
|
||||
|
@ -60,8 +99,8 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true)
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
@ -78,9 +117,9 @@ class StorySendsDatabaseTest {
|
|||
val recipient1 = recipients1to10.first()
|
||||
val recipient2 = recipients11to20.first()
|
||||
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true)
|
||||
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
|
||||
storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
|
||||
storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
|
||||
|
@ -97,8 +136,8 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
|
||||
storySends.insert(messageId1, recipients6to15, 100, true)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false)
|
||||
storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
|
||||
storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
|
||||
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
|
||||
|
@ -112,9 +151,9 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_noOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 100, false)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false)
|
||||
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
|
||||
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
@ -128,8 +167,8 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
@ -143,10 +182,10 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
SignalDatabase.mms.markAsRemoteDelete(messageId1)
|
||||
|
||||
storySends.insert(messageId2, recipients6to15, 200, true)
|
||||
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
|
||||
|
||||
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
|
||||
|
||||
|
@ -156,7 +195,7 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun canReply_storyWithReplies() {
|
||||
storySends.insert(messageId2, recipients1to10, 200, true)
|
||||
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
|
@ -165,7 +204,7 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun canReply_storyWithoutReplies() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
|
||||
val canReply = storySends.canReply(recipients1to10[0], 200)
|
||||
|
||||
|
@ -174,8 +213,8 @@ class StorySendsDatabaseTest {
|
|||
|
||||
@Test
|
||||
fun canReply_storyWithAndWithoutRepliesOverlap() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true)
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
|
||||
|
||||
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
|
||||
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
|
||||
|
@ -184,6 +223,238 @@ class StorySendsDatabaseTest {
|
|||
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> {
|
||||
return (1..count).map {
|
||||
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.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
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_RECENT_HEADER = 1 << 7;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -88,7 +85,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
|||
addRecentGroupsSection(cursorList);
|
||||
addGroupsSection(cursorList);
|
||||
} else {
|
||||
addStoriesSection(cursorList);
|
||||
addRecentsSection(cursorList);
|
||||
addContactsSection(cursorList);
|
||||
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) {
|
||||
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) {
|
||||
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
|
||||
|
@ -240,16 +223,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
|||
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() {
|
||||
return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter());
|
||||
}
|
||||
|
@ -320,10 +293,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
|
|||
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) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.recipients.Recipient;
|
||||
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.
|
||||
*/
|
||||
|
@ -130,10 +118,6 @@ public final class ContactsCursorRows {
|
|||
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) {
|
||||
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.requireNonNullString
|
||||
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.DistributionListPartialRecord
|
||||
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.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.lang.AssertionError
|
||||
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)
|
||||
|
||||
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) {
|
||||
val recipientId = db.insert(
|
||||
|
@ -68,6 +70,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
const val IS_UNKNOWN = "is_unknown"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
|
@ -76,7 +79,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
$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)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
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? {
|
||||
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 {
|
||||
query.isNullOrEmpty() && includeMyStory -> 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}"
|
||||
else -> "${ListTable.NAME} GLOB ? AND ${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} AND NOT ${ListTable.IS_UNKNOWN}"
|
||||
}
|
||||
|
||||
val whereArgs = when {
|
||||
|
@ -155,7 +160,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
|
||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||
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}"
|
||||
|
||||
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)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -184,7 +234,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
|
||||
allowsReplies: Boolean = true,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: ByteArray? = null
|
||||
storageId: ByteArray? = null,
|
||||
isUnknown: Boolean = false
|
||||
): DistributionListId? {
|
||||
val db = writableDatabase
|
||||
|
||||
|
@ -196,6 +247,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
|
||||
putNull(ListTable.RECIPIENT_ID)
|
||||
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||
put(ListTable.IS_UNKNOWN, isUnknown)
|
||||
}
|
||||
|
||||
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 {
|
||||
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()) {
|
||||
|
@ -251,7 +318,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
@ -270,7 +338,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getRawMembers(id),
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP)
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
@ -461,7 +530,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
try {
|
||||
val listTableValues = contentValuesOf(
|
||||
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(
|
||||
|
@ -493,4 +563,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
|||
private fun createUniqueNameForDeletedStory(): String {
|
||||
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 @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
||||
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
|
||||
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
|
||||
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
|
||||
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
|
||||
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));
|
||||
}
|
||||
|
||||
@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
|
||||
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
||||
|
@ -932,7 +941,7 @@ public class MmsDatabase extends MessageDatabase {
|
|||
public boolean hasMeaningfulMessage(long threadId) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
|
|
@ -3,11 +3,15 @@ package org.thoughtcrime.securesms.database
|
|||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
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
|
||||
|
@ -26,6 +30,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
|||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val SENT_TIMESTAMP = "sent_timestamp"
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DISTRIBUTION_ID = "distribution_id"
|
||||
|
||||
val CREATE_TABLE = """
|
||||
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,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
||||
$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()
|
||||
|
||||
|
@ -42,7 +48,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
|||
""".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
|
||||
|
||||
db.beginTransaction()
|
||||
|
@ -52,11 +58,12 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
|||
MESSAGE_ID to messageId,
|
||||
RECIPIENT_ID to id.serialize(),
|
||||
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) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
@ -177,4 +184,208 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
|||
|
||||
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) {
|
||||
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
||||
boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId);
|
||||
boolean isPinned = getPinnedThreadIds().contains(threadId);
|
||||
boolean shouldDelete = allowDeletion && !isPinned && !SignalDatabase.mms().containsStories(threadId);
|
||||
|
||||
if (!meaningfulMessages) {
|
||||
if (allowDeletion) {
|
||||
if (shouldDelete) {
|
||||
deleteConversation(threadId);
|
||||
return true;
|
||||
} else if (!isPinned) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
MessageRecord record;
|
||||
try {
|
||||
record = mmsSmsDatabase.getConversationSnippet(threadId);
|
||||
} catch (NoSuchMessageException e) {
|
||||
if (allowDeletion && !SignalDatabase.mms().containsStories(threadId)) {
|
||||
if (shouldDelete) {
|
||||
deleteConversation(threadId);
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -196,8 +196,9 @@ object SignalDatabaseMigrations {
|
|||
private const val CDS_V2 = 140
|
||||
private const val GROUP_SERVICE_ID = 141
|
||||
private const val QUOTE_TYPE = 142
|
||||
private const val STORY_SYNCS = 143
|
||||
|
||||
const val DATABASE_VERSION = 142
|
||||
const val DATABASE_VERSION = 143
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
@ -2533,6 +2534,39 @@ object SignalDatabaseMigrations {
|
|||
if (oldVersion < QUOTE_TYPE) {
|
||||
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
|
||||
|
|
|
@ -6,5 +6,6 @@ data class DistributionListPartialRecord(
|
|||
val id: DistributionListId,
|
||||
val name: CharSequence,
|
||||
val recipientId: RecipientId,
|
||||
val allowsReplies: Boolean
|
||||
val allowsReplies: Boolean,
|
||||
val isUnknown: Boolean
|
||||
)
|
||||
|
|
|
@ -12,5 +12,6 @@ data class DistributionListRecord(
|
|||
val distributionId: DistributionId,
|
||||
val allowsReplies: Boolean,
|
||||
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(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory());
|
||||
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
|
||||
put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory());
|
||||
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
|
||||
put(SmsReceiveJob.KEY, new SmsReceiveJob.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.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
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.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -175,6 +177,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
||||
|
||||
PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, targets, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches);
|
||||
|
||||
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
||||
database.markAsSentFailed(messageId);
|
||||
|
@ -206,7 +209,11 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
} else {
|
||||
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) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil;
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
|
||||
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.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
|
@ -53,9 +53,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull RemoteDeleteSendJob create(@NonNull Context context,
|
||||
long messageId,
|
||||
boolean isMms)
|
||||
public static @NonNull JobManager.Chain create(long messageId, boolean isMms)
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId)
|
||||
|
@ -70,6 +68,9 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
List<RecipientId> recipients;
|
||||
if (conversationRecipient.isDistributionList()) {
|
||||
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
|
||||
if (recipients.isEmpty()) {
|
||||
return ApplicationDependencies.getJobManager().startChain(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId));
|
||||
}
|
||||
} else {
|
||||
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
||||
: Stream.of(conversationRecipient.getId()).toList();
|
||||
|
@ -77,15 +78,23 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
|
||||
recipients.remove(Recipient.self().getId());
|
||||
|
||||
return new RemoteDeleteSendJob(messageId,
|
||||
isMms,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
new Parameters.Builder()
|
||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
RemoteDeleteSendJob sendJob = new RemoteDeleteSendJob(messageId,
|
||||
isMms,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
new Parameters.Builder()
|
||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.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,
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
|
|||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||
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.SignalServiceDataMessage;
|
||||
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.calls.SignalServiceCallMessage;
|
||||
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.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
@ -157,7 +160,8 @@ public final class GroupSendUtil {
|
|||
boolean isRecipientUpdate,
|
||||
@NonNull MessageId messageId,
|
||||
long sentTimestamp,
|
||||
@NonNull SignalServiceStoryMessage message)
|
||||
@NonNull SignalServiceStoryMessage message,
|
||||
@NonNull Set<SignalServiceStoryMessageRecipient> manifest)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return sendMessage(
|
||||
|
@ -167,7 +171,7 @@ public final class GroupSendUtil {
|
|||
messageId,
|
||||
allTargets,
|
||||
isRecipientUpdate,
|
||||
new StorySendOperation(messageId, null, sentTimestamp, message),
|
||||
new StorySendOperation(messageId, null, sentTimestamp, message, manifest),
|
||||
null);
|
||||
}
|
||||
|
||||
|
@ -193,7 +197,7 @@ public final class GroupSendUtil {
|
|||
messageId,
|
||||
allTargets,
|
||||
isRecipientUpdate,
|
||||
new StorySendOperation(messageId, groupId, sentTimestamp, message),
|
||||
new StorySendOperation(messageId, groupId, sentTimestamp, message, Collections.emptySet()),
|
||||
null);
|
||||
}
|
||||
|
||||
|
@ -605,16 +609,23 @@ public final class GroupSendUtil {
|
|||
|
||||
public static class StorySendOperation implements SendOperation {
|
||||
|
||||
private final MessageId relatedMessageId;
|
||||
private final GroupId groupId;
|
||||
private final long sentTimestamp;
|
||||
private final SignalServiceStoryMessage message;
|
||||
private final MessageId relatedMessageId;
|
||||
private final GroupId groupId;
|
||||
private final long sentTimestamp;
|
||||
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.groupId = groupId;
|
||||
this.sentTimestamp = sentTimestamp;
|
||||
this.message = message;
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -625,7 +636,7 @@ public final class GroupSendUtil {
|
|||
boolean isRecipientUpdate)
|
||||
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
|
||||
|
@ -637,7 +648,7 @@ public final class GroupSendUtil {
|
|||
@Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return messageSender.sendStory(targets, access, message, getSentTimestamp());
|
||||
return messageSender.sendStory(targets, access, message, getSentTimestamp(), manifest);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -39,14 +39,17 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
|
|||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
||||
import org.thoughtcrime.securesms.database.PaymentMetaDataUtil;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
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.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageLogEntry;
|
||||
|
@ -513,11 +516,11 @@ public final class MessageContentProcessor {
|
|||
private static @Nullable SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) {
|
||||
if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) {
|
||||
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().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 {
|
||||
return null;
|
||||
}
|
||||
|
@ -1203,9 +1206,15 @@ public final class MessageContentProcessor {
|
|||
try {
|
||||
GroupDatabase groupDatabase = SignalDatabase.groups();
|
||||
|
||||
if (message.getMessage().isGroupV2Message()) {
|
||||
GroupId.V2 groupId = GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey());
|
||||
if (handleGv2PreProcessing(groupId, content, message.getMessage().getGroupContext().get().getGroupV2().get(), senderRecipient)) {
|
||||
if (message.getStoryMessage().isPresent() || !message.getStoryMessageRecipients().isEmpty()) {
|
||||
handleSynchronizeSentStoryMessage(message, content.getTimestamp());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1214,38 +1223,38 @@ public final class MessageContentProcessor {
|
|||
|
||||
if (message.isRecipientUpdate()) {
|
||||
handleGroupRecipientUpdate(message, content.getTimestamp());
|
||||
} else if (message.getMessage().isEndSession()) {
|
||||
} else if (dataMessage.isEndSession()) {
|
||||
threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp());
|
||||
} else if (message.getMessage().isGroupV1Update()) {
|
||||
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true);
|
||||
} else if (dataMessage.isGroupV1Update()) {
|
||||
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, dataMessage, true);
|
||||
threadId = gv1ThreadId == null ? -1 : gv1ThreadId;
|
||||
} else if (message.getMessage().isGroupV2Update()) {
|
||||
} else if (dataMessage.isGroupV2Update()) {
|
||||
handleSynchronizeSentGv2Update(content, message);
|
||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
||||
} else if (Build.VERSION.SDK_INT > 19 && message.getMessage().getGroupCallUpdate().isPresent()) {
|
||||
handleGroupCallUpdateMessage(content, message.getMessage(), GroupUtil.idFromGroupContext(message.getMessage().getGroupContext()), senderRecipient);
|
||||
} else if (message.getMessage().isEmptyGroupV2Message()) {
|
||||
} else if (Build.VERSION.SDK_INT > 19 && dataMessage.getGroupCallUpdate().isPresent()) {
|
||||
handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient);
|
||||
} else if (dataMessage.isEmptyGroupV2Message()) {
|
||||
warn(content.getTimestamp(), "Empty GV2 message! Doing nothing.");
|
||||
} else if (message.getMessage().isExpirationUpdate()) {
|
||||
} else if (dataMessage.isExpirationUpdate()) {
|
||||
threadId = handleSynchronizeSentExpirationUpdate(message);
|
||||
} else if (message.getMessage().getStoryContext().isPresent()) {
|
||||
} else if (dataMessage.getStoryContext().isPresent()) {
|
||||
threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp());
|
||||
} else if (message.getMessage().getReaction().isPresent()) {
|
||||
handleReaction(content, message.getMessage(), senderRecipient);
|
||||
} else if (dataMessage.getReaction().isPresent()) {
|
||||
handleReaction(content, dataMessage, senderRecipient);
|
||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
||||
} else if (message.getMessage().getRemoteDelete().isPresent()) {
|
||||
handleRemoteDelete(content, message.getMessage(), 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.getRemoteDelete().isPresent()) {
|
||||
handleRemoteDelete(content, dataMessage, senderRecipient);
|
||||
} else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp());
|
||||
} else {
|
||||
threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp());
|
||||
}
|
||||
|
||||
if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
|
||||
handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get(), senderRecipient);
|
||||
if (dataMessage.getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(dataMessage.getGroupContext().get()))) {
|
||||
handleUnknownGroupMessage(content, dataMessage.getGroupContext().get(), senderRecipient);
|
||||
}
|
||||
|
||||
if (message.getMessage().getProfileKey().isPresent()) {
|
||||
if (dataMessage.getProfileKey().isPresent()) {
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
|
||||
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());
|
||||
|
||||
SignalServiceGroupV2 signalServiceGroupV2 = message.getMessage().getGroupContext().get().getGroupV2().get();
|
||||
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey());
|
||||
SignalServiceDataMessage dataMessage = message.getDataMessage().get();
|
||||
SignalServiceGroupV2 signalServiceGroupV2 = dataMessage.getGroupContext().get().getGroupV2().get();
|
||||
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey());
|
||||
|
||||
if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) {
|
||||
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,
|
||||
message.getTimestamp(),
|
||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
||||
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
|
||||
SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getMessage().getExpiresInSeconds());
|
||||
SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getDataMessage().get().getExpiresInSeconds());
|
||||
|
||||
return threadId;
|
||||
}
|
||||
|
@ -1899,9 +1909,9 @@ public final class MessageContentProcessor {
|
|||
log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp());
|
||||
|
||||
try {
|
||||
Optional<SignalServiceDataMessage.Reaction> reaction = message.getMessage().getReaction();
|
||||
Optional<SignalServiceDataMessage.Reaction> reaction = message.getDataMessage().get().getReaction();
|
||||
ParentStoryId parentStoryId;
|
||||
SignalServiceDataMessage.StoryContext storyContext = message.getMessage().getStoryContext().get();
|
||||
SignalServiceDataMessage.StoryContext storyContext = message.getDataMessage().get().getStoryContext().get();
|
||||
MessageDatabase database = SignalDatabase.mms();
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
QuoteModel quoteModel = null;
|
||||
|
@ -1916,10 +1926,10 @@ public final class MessageContentProcessor {
|
|||
if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) {
|
||||
body = reaction.get().getEmoji();
|
||||
} 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());
|
||||
} else if (groupStory || SignalDatabase.storySends().canReply(storyAuthorRecipient, storyContext.getSentTimestamp())) {
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
|
@ -1946,18 +1956,18 @@ public final class MessageContentProcessor {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
parentStoryId,
|
||||
message.getMessage().getReaction().isPresent(),
|
||||
message.getDataMessage().get().getReaction().isPresent(),
|
||||
quoteModel,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
getMentions(message.getMessage().getMentions()).orElse(Collections.emptyList()),
|
||||
getMentions(message.getDataMessage().get().getMentions()).orElse(Collections.emptyList()),
|
||||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null);
|
||||
|
||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
}
|
||||
|
||||
|
@ -1976,13 +1986,13 @@ public final class MessageContentProcessor {
|
|||
|
||||
database.markAsSent(messageId, true);
|
||||
|
||||
if (message.getMessage().getExpiresInSeconds() > 0) {
|
||||
if (message.getDataMessage().get().getExpiresInSeconds() > 0) {
|
||||
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
||||
ApplicationDependencies.getExpiringMessageManager()
|
||||
.scheduleDeletion(messageId,
|
||||
true,
|
||||
message.getExpirationStartTimestamp(),
|
||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
||||
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||
}
|
||||
|
||||
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)
|
||||
throws MmsException, BadGroupIdException
|
||||
{
|
||||
|
@ -2010,26 +2143,26 @@ public final class MessageContentProcessor {
|
|||
|
||||
MessageDatabase database = SignalDatabase.mms();
|
||||
Recipient recipients = getSyncMessageDestination(message);
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().orElse(""), false);
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions());
|
||||
Optional<GiftBadge> giftBadge = getGiftBadge(message.getMessage().getGiftBadge());
|
||||
boolean viewOnce = message.getMessage().isViewOnce();
|
||||
Optional<QuoteModel> quote = getValidatedQuote(message.getDataMessage().get().getQuote());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getDataMessage().get().getSticker());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false);
|
||||
Optional<List<Mention>> mentions = getMentions(message.getDataMessage().get().getMentions());
|
||||
Optional<GiftBadge> giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge());
|
||||
boolean viewOnce = message.getDataMessage().get().isViewOnce();
|
||||
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()) {
|
||||
syncAttachments.add(sticker.get());
|
||||
}
|
||||
|
||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients,
|
||||
message.getMessage().getBody().orElse(null),
|
||||
message.getDataMessage().get().getBody().orElse(null),
|
||||
syncAttachments,
|
||||
message.getTimestamp(),
|
||||
-1,
|
||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()),
|
||||
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()),
|
||||
viewOnce,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
|
@ -2045,7 +2178,7 @@ public final class MessageContentProcessor {
|
|||
|
||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||
|
||||
if (recipients.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
||||
if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
}
|
||||
|
||||
|
@ -2072,13 +2205,13 @@ public final class MessageContentProcessor {
|
|||
stickerAttachments = Stream.of(allAttachments).filter(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());
|
||||
ApplicationDependencies.getExpiringMessageManager()
|
||||
.scheduleDeletion(messageId,
|
||||
true,
|
||||
message.getExpirationStartTimestamp(),
|
||||
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()));
|
||||
TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
|
||||
}
|
||||
|
||||
if (recipients.isSelf()) {
|
||||
|
@ -2204,10 +2337,10 @@ public final class MessageContentProcessor {
|
|||
log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp());
|
||||
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
String body = message.getMessage().getBody().orElse("");
|
||||
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds());
|
||||
String body = message.getDataMessage().get().getBody().orElse("");
|
||||
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) {
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
}
|
||||
|
||||
|
@ -2862,7 +2995,7 @@ public final class MessageContentProcessor {
|
|||
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
|
||||
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 {
|
||||
|
|
|
@ -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.
|
||||
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();
|
||||
|
||||
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.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -86,6 +87,7 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -164,8 +166,9 @@ public class MessageSender {
|
|||
Recipient recipient = message.getRecipient();
|
||||
|
||||
if (recipient.isDistributionList()) {
|
||||
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()));
|
||||
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();
|
||||
|
||||
if (recipient.isDistributionList()) {
|
||||
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
||||
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies());
|
||||
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(), distributionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -471,7 +475,7 @@ public class MessageSender {
|
|||
db.markAsSending(messageId);
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(RemoteDeleteSendJob.create(context, messageId, isMms));
|
||||
RemoteDeleteSendJob.create(messageId, isMms).enqueue();
|
||||
onMessageSent();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring.");
|
||||
|
|
|
@ -118,7 +118,7 @@ object PrivateStoryItem {
|
|||
|
||||
override fun bind(model: PartialModel) {
|
||||
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();
|
||||
} else if (content.getSyncMessage().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()) {
|
||||
try {
|
||||
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.SignalServiceReceiptMessage;
|
||||
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.SignalServiceTypingMessage;
|
||||
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);
|
||||
}
|
||||
|
||||
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp)
|
||||
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp,
|
||||
Set<SignalServiceStoryMessageRecipient> manifest)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Content content = createStoryContent(message);
|
||||
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty());
|
||||
Content content = createStoryContent(message);
|
||||
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.
|
||||
* @return
|
||||
*/
|
||||
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
||||
Optional<byte[]> groupId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp)
|
||||
public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
|
||||
Optional<byte[]> groupId,
|
||||
List<SignalServiceAddress> recipients,
|
||||
List<UnidentifiedAccess> unidentifiedAccess,
|
||||
SignalServiceStoryMessage message,
|
||||
long timestamp,
|
||||
Set<SignalServiceStoryMessageRecipient> manifest)
|
||||
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
|
||||
{
|
||||
Content content = createStoryContent(message);
|
||||
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY);
|
||||
Content content = createStoryContent(message);
|
||||
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();
|
||||
|
||||
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());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
||||
|
@ -446,7 +458,7 @@ public class SignalServiceMessageSender {
|
|||
sendEvents.onMessageSent();
|
||||
|
||||
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());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null);
|
||||
|
@ -496,7 +508,7 @@ public class SignalServiceMessageSender {
|
|||
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());
|
||||
|
||||
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
|
||||
|
@ -791,6 +803,16 @@ public class SignalServiceMessageSender {
|
|||
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 {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
DataMessage.Builder builder = DataMessage.newBuilder();
|
||||
|
@ -1108,27 +1130,30 @@ public class SignalServiceMessageSender {
|
|||
|
||||
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
|
||||
SignalServiceAddress address = transcript.getDestination().get();
|
||||
Content content = createMessageContent(transcript.getMessage());
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.of(content));
|
||||
Content content = createMessageContent(transcript);
|
||||
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.ofNullable(content));
|
||||
|
||||
|
||||
return createMultiDeviceSentTranscriptContent(content,
|
||||
Optional.of(address),
|
||||
transcript.getTimestamp(),
|
||||
Collections.singletonList(result),
|
||||
false);
|
||||
transcript.isRecipientUpdate(),
|
||||
transcript.getStoryMessageRecipients());
|
||||
}
|
||||
|
||||
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
|
||||
long timestamp, List<SendMessageResult> sendMessageResults,
|
||||
boolean isRecipientUpdate)
|
||||
boolean isRecipientUpdate,
|
||||
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
|
||||
{
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
||||
DataMessage dataMessage = content.getDataMessage();
|
||||
Content.Builder container = Content.newBuilder();
|
||||
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
|
||||
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
|
||||
DataMessage dataMessage = content != null && content.hasDataMessage() ? content.getDataMessage() : null;
|
||||
StoryMessage storyMessage = content != null && content.hasStoryMessage() ? content.getStoryMessage() : null;
|
||||
|
||||
sentMessage.setTimestamp(timestamp);
|
||||
sentMessage.setMessage(dataMessage);
|
||||
|
||||
for (SendMessageResult result : sendMessageResults) {
|
||||
if (result.getSuccess() != null) {
|
||||
|
@ -1144,19 +1169,38 @@ public class SignalServiceMessageSender {
|
|||
sentMessage.setDestinationUuid(recipient.get().getServiceId().toString());
|
||||
}
|
||||
|
||||
if (dataMessage.getExpireTimer() > 0) {
|
||||
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
|
||||
if (dataMessage != null) {
|
||||
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()) {
|
||||
dataMessage = dataMessage.toBuilder().clearAttachments().build();
|
||||
sentMessage.setMessage(dataMessage);
|
||||
if (storyMessage != null) {
|
||||
sentMessage.setStoryMessage(storyMessage);
|
||||
}
|
||||
|
||||
sentMessage.addAllStoryMessageRecipients(storyMessageRecipients.stream()
|
||||
.map(this::createStoryMessageRecipient)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
|
||||
|
||||
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) {
|
||||
Content.Builder container = Content.newBuilder();
|
||||
|
@ -1580,13 +1624,28 @@ public class SignalServiceMessageSender {
|
|||
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) {
|
||||
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
|
||||
message.getTimestamp(),
|
||||
message,
|
||||
Optional.of(message),
|
||||
message.getExpiresInSeconds(),
|
||||
Collections.singletonMap(localAddress, false),
|
||||
false);
|
||||
false,
|
||||
Optional.empty(),
|
||||
Collections.emptySet());
|
||||
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,11 +58,14 @@ import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadata
|
|||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class SignalServiceContent {
|
||||
|
||||
|
@ -674,14 +677,21 @@ public final class SignalServiceContent {
|
|||
throws ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException
|
||||
{
|
||||
if (content.hasSent()) {
|
||||
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
|
||||
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
|
||||
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage());
|
||||
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
|
||||
? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid())))
|
||||
: Optional.empty();
|
||||
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
|
||||
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
|
||||
Optional<SignalServiceDataMessage> dataMessage = sentContent.hasMessage() ? Optional.of(createSignalServiceMessage(metadata, sentContent.getMessage())) : Optional.empty();
|
||||
Optional<SignalServiceStoryMessage> storyMessage = sentContent.hasStoryMessage() ? Optional.of(createStoryMessage(sentContent.getStoryMessage())) : Optional.empty();
|
||||
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
|
||||
? 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!");
|
||||
}
|
||||
|
||||
|
@ -699,7 +709,9 @@ public final class SignalServiceContent {
|
|||
dataMessage,
|
||||
sentContent.getExpirationStartTimestamp(),
|
||||
unidentifiedStatuses,
|
||||
sentContent.getIsRecipientUpdate()));
|
||||
sentContent.getIsRecipientUpdate(),
|
||||
storyMessage,
|
||||
recipientManifest));
|
||||
}
|
||||
|
||||
if (content.hasRequest()) {
|
||||
|
@ -913,6 +925,14 @@ public final class SignalServiceContent {
|
|||
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) {
|
||||
boolean isMultiRing = content.getMultiRing();
|
||||
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.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
|
@ -18,21 +20,25 @@ import java.util.Set;
|
|||
|
||||
public class SentTranscriptMessage {
|
||||
|
||||
private final Optional<SignalServiceAddress> destination;
|
||||
private final long timestamp;
|
||||
private final long expirationStartTimestamp;
|
||||
private final SignalServiceDataMessage message;
|
||||
private final Map<String, Boolean> unidentifiedStatusBySid;
|
||||
private final Map<String, Boolean> unidentifiedStatusByE164;
|
||||
private final Set<SignalServiceAddress> recipients;
|
||||
private final boolean isRecipientUpdate;
|
||||
private final Optional<SignalServiceAddress> destination;
|
||||
private final long timestamp;
|
||||
private final long expirationStartTimestamp;
|
||||
private final Optional<SignalServiceDataMessage> message;
|
||||
private final Map<String, Boolean> unidentifiedStatusBySid;
|
||||
private final Map<String, Boolean> unidentifiedStatusByE164;
|
||||
private final Set<SignalServiceAddress> recipients;
|
||||
private final boolean isRecipientUpdate;
|
||||
private final Optional<SignalServiceStoryMessage> storyMessage;
|
||||
private final Set<SignalServiceStoryMessageRecipient> storyMessageRecipients;
|
||||
|
||||
public SentTranscriptMessage(Optional<SignalServiceAddress> destination,
|
||||
long timestamp,
|
||||
SignalServiceDataMessage message,
|
||||
Optional<SignalServiceDataMessage> message,
|
||||
long expirationStartTimestamp,
|
||||
Map<SignalServiceAddress, Boolean> unidentifiedStatus,
|
||||
boolean isRecipientUpdate)
|
||||
boolean isRecipientUpdate,
|
||||
Optional<SignalServiceStoryMessage> storyMessage,
|
||||
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
|
||||
{
|
||||
this.destination = destination;
|
||||
this.timestamp = timestamp;
|
||||
|
@ -42,6 +48,8 @@ public class SentTranscriptMessage {
|
|||
this.unidentifiedStatusByE164 = new HashMap<>();
|
||||
this.recipients = unidentifiedStatus.keySet();
|
||||
this.isRecipientUpdate = isRecipientUpdate;
|
||||
this.storyMessage = storyMessage;
|
||||
this.storyMessageRecipients = storyMessageRecipients;
|
||||
|
||||
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
|
||||
unidentifiedStatusBySid.put(entry.getKey().getServiceId().toString(), entry.getValue());
|
||||
|
@ -64,10 +72,18 @@ public class SentTranscriptMessage {
|
|||
return expirationStartTimestamp;
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage getMessage() {
|
||||
public Optional<SignalServiceDataMessage> getDataMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public Optional<SignalServiceStoryMessage> getStoryMessage() {
|
||||
return storyMessage;
|
||||
}
|
||||
|
||||
public Set<SignalServiceStoryMessageRecipient> getStoryMessageRecipients() {
|
||||
return storyMessageRecipients;
|
||||
}
|
||||
|
||||
public boolean isUnidentified(ServiceId serviceId) {
|
||||
return isUnidentified(serviceId.toString());
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.push;
|
|||
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
@ -40,4 +41,17 @@ public final class DistributionId {
|
|||
public String 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;
|
||||
}
|
||||
|
||||
message StoryMessageRecipient {
|
||||
optional string destinationUuid = 1;
|
||||
repeated string distributionListIds = 2;
|
||||
optional bool isAllowedToReply = 3;
|
||||
}
|
||||
|
||||
reserved /*destinationE164*/ 1;
|
||||
optional string destinationUuid = 7;
|
||||
optional uint64 timestamp = 2;
|
||||
|
@ -434,6 +440,8 @@ message SyncMessage {
|
|||
optional uint64 expirationStartTimestamp = 4;
|
||||
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
|
||||
optional bool isRecipientUpdate = 6 [default = false];
|
||||
optional StoryMessage storyMessage = 8;
|
||||
repeated StoryMessageRecipient storyMessageRecipients = 9;
|
||||
}
|
||||
|
||||
message Contacts {
|
||||
|
|
Loading…
Add table
Reference in a new issue