Add sent story syncing.

This commit is contained in:
Alex Hart 2022-05-10 11:01:51 -03:00
parent 8ca0f4baf4
commit af9465fefe
29 changed files with 1311 additions and 236 deletions

View file

@ -1,21 +1,41 @@
package org.thoughtcrime.securesms.database package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertNull
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.containsInAnyOrder
import org.hamcrest.Matchers.hasSize import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID import java.util.UUID
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest { class StorySendsDatabaseTest {
private val distributionId1 = DistributionId.from(UUID.randomUUID())
private val distributionId2 = DistributionId.from(UUID.randomUUID())
private val distributionId3 = DistributionId.from(UUID.randomUUID())
private lateinit var distributionList1: DistributionListId
private lateinit var distributionList2: DistributionListId
private lateinit var distributionList3: DistributionListId
private lateinit var distributionListRecipient1: Recipient
private lateinit var distributionListRecipient2: Recipient
private lateinit var distributionListRecipient3: Recipient
private lateinit var recipients1to10: List<RecipientId> private lateinit var recipients1to10: List<RecipientId>
private lateinit var recipients11to20: List<RecipientId> private lateinit var recipients11to20: List<RecipientId>
private lateinit var recipients6to15: List<RecipientId> private lateinit var recipients6to15: List<RecipientId>
@ -31,22 +51,41 @@ class StorySendsDatabaseTest {
fun setup() { fun setup() {
storySends = SignalDatabase.storySends storySends = SignalDatabase.storySends
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
recipients1to10 = makeRecipients(10) recipients1to10 = makeRecipients(10)
recipients11to20 = makeRecipients(10) recipients11to20 = makeRecipients(10)
distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!!
distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!!
distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!!
distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1))
distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2))
distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3))
messageId1 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
messageId2 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITH_REPLIES,
)
messageId3 = MmsHelper.insert(
recipient = distributionListRecipient3,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5) recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
recipients6to10 = recipients1to10.takeLast(5) recipients6to10 = recipients1to10.takeLast(5)
} }
@Test @Test
fun getRecipientsToSendTo_noOverlap() { fun getRecipientsToSendTo_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false) storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true) storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false) storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
@ -60,8 +99,8 @@ class StorySendsDatabaseTest {
@Test @Test
fun getRecipientsToSendTo_overlap() { fun getRecipientsToSendTo_overlap() {
storySends.insert(messageId1, recipients1to10, 100, false) storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 100, true) storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
@ -78,9 +117,9 @@ class StorySendsDatabaseTest {
val recipient1 = recipients1to10.first() val recipient1 = recipients1to10.first()
val recipient2 = recipients11to20.first() val recipient2 = recipients11to20.first()
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false) storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
storySends.insert(messageId2, listOf(recipient1), 100, true) storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
storySends.insert(messageId3, listOf(recipient2), 100, true) storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
@ -97,8 +136,8 @@ class StorySendsDatabaseTest {
@Test @Test
fun getRecipientsToSendTo_overlapWithEarlierMessage() { fun getRecipientsToSendTo_overlapWithEarlierMessage() {
storySends.insert(messageId1, recipients6to15, 100, true) storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
storySends.insert(messageId2, recipients1to10, 100, false) storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true) val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false) val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
@ -112,9 +151,9 @@ class StorySendsDatabaseTest {
@Test @Test
fun getRemoteDeleteRecipients_noOverlap() { fun getRemoteDeleteRecipients_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false) storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true) storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false) storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100) val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
@ -128,8 +167,8 @@ class StorySendsDatabaseTest {
@Test @Test
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() { fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false) storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 200, true) storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200) val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
@ -143,10 +182,10 @@ class StorySendsDatabaseTest {
@Test @Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() { fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false) storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
SignalDatabase.mms.markAsRemoteDelete(messageId1) SignalDatabase.mms.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true) storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200) val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
@ -156,7 +195,7 @@ class StorySendsDatabaseTest {
@Test @Test
fun canReply_storyWithReplies() { fun canReply_storyWithReplies() {
storySends.insert(messageId2, recipients1to10, 200, true) storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val canReply = storySends.canReply(recipients1to10[0], 200) val canReply = storySends.canReply(recipients1to10[0], 200)
@ -165,7 +204,7 @@ class StorySendsDatabaseTest {
@Test @Test
fun canReply_storyWithoutReplies() { fun canReply_storyWithoutReplies() {
storySends.insert(messageId1, recipients1to10, 200, false) storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val canReply = storySends.canReply(recipients1to10[0], 200) val canReply = storySends.canReply(recipients1to10[0], 200)
@ -174,8 +213,8 @@ class StorySendsDatabaseTest {
@Test @Test
fun canReply_storyWithAndWithoutRepliesOverlap() { fun canReply_storyWithAndWithoutRepliesOverlap() {
storySends.insert(messageId1, recipients1to10, 200, false) storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to10, 200, true) storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200) val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200) val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
@ -184,6 +223,238 @@ class StorySendsDatabaseTest {
assertThat(message2RecipientCanReply, `is`(true)) assertThat(message2RecipientCanReply, `is`(true))
} }
@Test
fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertNotNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, false, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)
assertNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
assertEquals(recipients1to10, manifest.entries.map { it.recipientId })
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists)
}
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertTrue(entry.allowedToReply)
}
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
assertNotNull(manifest)
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
assertEquals(recipients1to10.toHashSet(), recipientIds)
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
val manifestRecipients = results.entries.map { it.recipientId }
assertEquals(recipients1to10, manifestRecipients)
}
@Test
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId2)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
assertTrue(results.entries.all { it.allowedToReply })
}
@Test
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val expected = storySends.getFullSentStorySyncManifest(messageId1, 200)
val emptyManifest = SentStorySyncManifest(emptyList())
storySends.applySentStoryManifest(emptyManifest, 200)
val result = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertEquals(expected, result)
}
@Test
fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val expected = storySends.getFullSentStorySyncManifest(messageId4, 200)
storySends.applySentStoryManifest(expected!!, 200)
val result = storySends.getFullSentStorySyncManifest(messageId4, 200)
assertEquals(expected, result)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
}
@Test
fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
@Test
fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients1to10.first()),
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
private fun makeRecipients(count: Int): List<RecipientId> { private fun makeRecipients(count: Int): List<RecipientId> {
return (1..count).map { return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))

View file

@ -27,10 +27,8 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
@ -57,7 +55,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
public static final int FLAG_HIDE_NEW = 1 << 6; public static final int FLAG_HIDE_NEW = 1 << 6;
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7; public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8; public static final int FLAG_GROUPS_AFTER_CONTACTS = 1 << 8;
public static final int FLAG_STORIES = 1 << 9;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF; public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
} }
@ -88,7 +85,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
addRecentGroupsSection(cursorList); addRecentGroupsSection(cursorList);
addGroupsSection(cursorList); addGroupsSection(cursorList);
} else { } else {
addStoriesSection(cursorList);
addRecentsSection(cursorList); addRecentsSection(cursorList);
addContactsSection(cursorList); addContactsSection(cursorList);
if (addGroupsAfterContacts(mode)) { if (addGroupsAfterContacts(mode)) {
@ -167,19 +163,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
} }
} }
private void addStoriesSection(@NonNull List<Cursor> cursorList) {
if (!Stories.isFeatureEnabled() ||!storiesEnabled(mode)) {
return;
}
Cursor stories = getStoriesCursor();
if (stories.getCount() > 0) {
cursorList.add(ContactsCursorRows.forStoriesHeader(getContext()));
cursorList.add(stories);
}
}
private void addNewNumberSection(@NonNull List<Cursor> cursorList) { private void addNewNumberSection(@NonNull List<Cursor> cursorList) {
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) { if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(getFilter())) {
cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext())); cursorList.add(ContactsCursorRows.forPhoneNumberSearchHeader(getContext()));
@ -240,16 +223,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
return groupContacts; return groupContacts;
} }
private Cursor getStoriesCursor() {
MatrixCursor distributionListsCursor = ContactsCursorRows.createMatrixCursor();
List<DistributionListPartialRecord> distributionLists = SignalDatabase.distributionLists().getAllListsForContactSelectionUi(null, true);
for (final DistributionListPartialRecord distributionList : distributionLists) {
distributionListsCursor.addRow(ContactsCursorRows.forDistributionList(distributionList));
}
return distributionListsCursor;
}
private Cursor getNewNumberCursor() { private Cursor getNewNumberCursor() {
return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter()); return ContactsCursorRows.forNewNumber(getUnknownContactTitle(), getFilter());
} }
@ -320,10 +293,6 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
return flagSet(mode, DisplayMode.FLAG_GROUPS_AFTER_CONTACTS); return flagSet(mode, DisplayMode.FLAG_GROUPS_AFTER_CONTACTS);
} }
private static boolean storiesEnabled(int mode) {
return flagSet(mode, DisplayMode.FLAG_STORIES);
}
private static boolean flagSet(int mode, int flag) { private static boolean flagSet(int mode, int flag) {
return (mode & flag) > 0; return (mode & flag) > 0;
} }

View file

@ -9,8 +9,6 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.OptionalUtil;
@ -86,16 +84,6 @@ public final class ContactsCursorRows {
""}; ""};
} }
public static @NonNull Object[] forDistributionList(@NonNull DistributionListPartialRecord distributionListPartialRecord) {
return new Object[]{ distributionListPartialRecord.getRecipientId().serialize(),
distributionListPartialRecord.getName(),
SignalDatabase.distributionLists().getMemberCount(distributionListPartialRecord.getId()),
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"",
ContactRepository.NORMAL_TYPE,
""};
}
/** /**
* Create a row for a contacts cursor for a new number the user is entering or has entered. * Create a row for a contacts cursor for a new number the user is entering or has entered.
*/ */
@ -130,10 +118,6 @@ public final class ContactsCursorRows {
return matrixCursor; return matrixCursor;
} }
public static @NonNull MatrixCursor forStoriesHeader(@NonNull Context context) {
return forHeader(context.getString(R.string.ContactsCursorLoader_my_stories));
}
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) { public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
return forHeader(context.getString(R.string.ContactsCursorLoader_username_search)); return forHeader(context.getString(R.string.ContactsCursorLoader_username_search));
} }

View file

@ -10,6 +10,7 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.database.model.DistributionListRecord import org.thoughtcrime.securesms.database.model.DistributionListRecord
@ -21,7 +22,6 @@ import org.thoughtcrime.securesms.util.Base64
import org.whispersystems.signalservice.api.push.DistributionId import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.util.UuidUtil
import java.lang.AssertionError
import java.util.UUID import java.util.UUID
/** /**
@ -36,6 +36,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE) val CREATE_TABLE: Array<String> = arrayOf(ListTable.CREATE_TABLE, MembershipTable.CREATE_TABLE)
const val RECIPIENT_ID = ListTable.RECIPIENT_ID const val RECIPIENT_ID = ListTable.RECIPIENT_ID
const val DISTRIBUTION_ID = ListTable.DISTRIBUTION_ID
const val LIST_TABLE_NAME = ListTable.TABLE_NAME
fun insertInitialDistributionListAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) { fun insertInitialDistributionListAtCreationTime(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
val recipientId = db.insert( val recipientId = db.insert(
@ -68,6 +70,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
const val RECIPIENT_ID = "recipient_id" const val RECIPIENT_ID = "recipient_id"
const val ALLOWS_REPLIES = "allows_replies" const val ALLOWS_REPLIES = "allows_replies"
const val DELETION_TIMESTAMP = "deletion_timestamp" const val DELETION_TIMESTAMP = "deletion_timestamp"
const val IS_UNKNOWN = "is_unknown"
const val CREATE_TABLE = """ const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
@ -76,7 +79,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL, $DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}), $RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
$ALLOWS_REPLIES INTEGER DEFAULT 1, $ALLOWS_REPLIES INTEGER DEFAULT 1,
$DELETION_TIMESTAMP INTEGER DEFAULT 0 $DELETION_TIMESTAMP INTEGER DEFAULT 0,
$IS_UNKNOWN INTEGER DEFAULT 0
) )
""" """
@ -124,7 +128,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)), id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
name = CursorUtil.requireString(it, ListTable.NAME), name = CursorUtil.requireString(it, ListTable.NAME),
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES), allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)) recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
) )
) )
} }
@ -135,13 +140,13 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? { fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
val db = readableDatabase val db = readableDatabase
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES) val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
val where = when { val where = when {
query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED query.isNullOrEmpty() && includeMyStory -> ListTable.IS_NOT_DELETED
query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}" query.isNullOrEmpty() -> "${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}"
includeMyStory -> "(${ListTable.NAME} GLOB ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED}" includeMyStory -> "(${ListTable.NAME} GLOB ? OR ${ListTable.ID} == ?) AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN}"
else -> "${ListTable.NAME} GLOB ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED}" else -> "${ListTable.NAME} GLOB ? AND ${ListTable.ID} != ? AND ${ListTable.IS_NOT_DELETED} AND NOT ${ListTable.IS_UNKNOWN}"
} }
val whereArgs = when { val whereArgs = when {
@ -155,7 +160,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
fun getCustomListsForUi(): List<DistributionListPartialRecord> { fun getCustomListsForUi(): List<DistributionListPartialRecord> {
val db = readableDatabase val db = readableDatabase
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES) val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES, ListTable.IS_UNKNOWN)
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}" val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID} AND ${ListTable.IS_NOT_DELETED}"
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use { return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
@ -166,7 +171,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)), id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
name = CursorUtil.requireString(it, ListTable.NAME), name = CursorUtil.requireString(it, ListTable.NAME),
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES), allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)) recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID)),
isUnknown = CursorUtil.requireBoolean(it, ListTable.IS_UNKNOWN)
) )
) )
} }
@ -175,6 +181,50 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
} ?: emptyList() } ?: emptyList()
} }
/**
* Gets or creates a distribution list for the given id.
*
* If the list does not exist, then a new list is created with a randomized name and populated with the members
* in the manifest.
*
* @return the recipient id of the list
*/
fun getOrCreateByDistributionId(distributionId: DistributionId, manifest: SentStorySyncManifest): RecipientId {
writableDatabase.beginTransaction()
try {
val distributionRecipientId = getRecipientIdByDistributionId(distributionId)
if (distributionRecipientId == null) {
val members: List<RecipientId> = manifest.entries
.filter { it.distributionLists.contains(distributionId) }
.map { it.recipientId }
val distributionListId = createList(
name = createUniqueNameForUnknownDistributionId(),
members = members,
distributionId = distributionId,
isUnknown = true
)
if (distributionListId == null) {
throw AssertionError("Failed to create distribution list for unknown id.")
} else {
val recipient = getRecipientId(distributionListId)
if (recipient == null) {
throw AssertionError("Failed to retrieve recipient for newly created list")
} else {
writableDatabase.setTransactionSuccessful()
return recipient
}
}
}
writableDatabase.setTransactionSuccessful()
return distributionRecipientId
} finally {
writableDatabase.endTransaction()
}
}
/** /**
* @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict. * @return The id of the list if successful, otherwise null. If not successful, you can assume it was a name conflict.
*/ */
@ -184,7 +234,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()), distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
allowsReplies: Boolean = true, allowsReplies: Boolean = true,
deletionTimestamp: Long = 0L, deletionTimestamp: Long = 0L,
storageId: ByteArray? = null storageId: ByteArray? = null,
isUnknown: Boolean = false
): DistributionListId? { ): DistributionListId? {
val db = writableDatabase val db = writableDatabase
@ -196,6 +247,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false) put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
putNull(ListTable.RECIPIENT_ID) putNull(ListTable.RECIPIENT_ID)
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp) put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
put(ListTable.IS_UNKNOWN, isUnknown)
} }
val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values) val id = writableDatabase.insert(ListTable.TABLE_NAME, null, values)
@ -222,6 +274,21 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
} }
} }
fun getRecipientIdByDistributionId(distributionId: DistributionId): RecipientId? {
return readableDatabase
.select(ListTable.RECIPIENT_ID)
.from(ListTable.TABLE_NAME)
.where("${ListTable.DISTRIBUTION_ID} = ?", distributionId.toString())
.run()
.use { cursor ->
if (cursor.moveToFirst()) {
RecipientId.from(CursorUtil.requireLong(cursor, ListTable.RECIPIENT_ID))
} else {
null
}
}
}
fun getStoryType(listId: DistributionListId): StoryType { fun getStoryType(listId: DistributionListId): StoryType {
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor -> readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ? AND ${ListTable.IS_NOT_DELETED}", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) { return if (cursor.moveToFirst()) {
@ -251,7 +318,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)), distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES), allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
members = getMembers(id), members = getMembers(id),
deletedAtTimestamp = 0L deletedAtTimestamp = 0L,
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
) )
} else { } else {
null null
@ -270,7 +338,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)), distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES), allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
members = getRawMembers(id), members = getRawMembers(id),
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP) deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
) )
} else { } else {
null null
@ -461,7 +530,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
try { try {
val listTableValues = contentValuesOf( val listTableValues = contentValuesOf(
ListTable.ALLOWS_REPLIES to update.new.allowsReplies(), ListTable.ALLOWS_REPLIES to update.new.allowsReplies(),
ListTable.NAME to update.new.name ListTable.NAME to update.new.name,
ListTable.IS_UNKNOWN to false
) )
writableDatabase.update( writableDatabase.update(
@ -493,4 +563,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
private fun createUniqueNameForDeletedStory(): String { private fun createUniqueNameForDeletedStory(): String {
return "DELETED-${UUID.randomUUID()}" return "DELETED-${UUID.randomUUID()}"
} }
private fun createUniqueNameForUnknownDistributionId(): String {
return "DELETED-${UUID.randomUUID()}"
}
} }

View file

@ -188,6 +188,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract boolean isStory(long messageId); public abstract boolean isStory(long messageId);
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse); public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(); public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId); public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;

View file

@ -584,6 +584,15 @@ public class MmsDatabase extends MessageDatabase {
return new Reader(rawQuery(where, null, reverse, -1L)); return new Reader(rawQuery(where, null, reverse, -1L));
} }
@Override
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")";
String[] whereArgs = SqlUtil.buildArgs(sentTimestamp);
Cursor cursor = rawQuery(where, whereArgs, false, -1L);
return new Reader(cursor);
}
@Override @Override
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) { public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
@ -932,7 +941,7 @@ public class MmsDatabase extends MessageDatabase {
public boolean hasMeaningfulMessage(long threadId) { public boolean hasMeaningfulMessage(long threadId) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, THREAD_ID_WHERE, SqlUtil.buildArgs(threadId), null, null, null, "1")) { try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?", SqlUtil.buildArgs(threadId, 0, 0), null, null, null, "1")) {
return cursor != null && cursor.moveToFirst(); return cursor != null && cursor.moveToFirst();
} }
} }

View file

@ -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)
}
}
}

View file

@ -1401,6 +1401,11 @@ public class SmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
throw new UnsupportedOperationException();
}
@Override @Override
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() { public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View file

@ -3,11 +3,15 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil import org.signal.core.util.SqlUtil
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.signal.core.util.toInt import org.signal.core.util.toInt
import org.signal.core.util.update
import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
/** /**
* Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to * Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to
@ -26,6 +30,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
const val RECIPIENT_ID = "recipient_id" const val RECIPIENT_ID = "recipient_id"
const val SENT_TIMESTAMP = "sent_timestamp" const val SENT_TIMESTAMP = "sent_timestamp"
const val ALLOWS_REPLIES = "allows_replies" const val ALLOWS_REPLIES = "allows_replies"
const val DISTRIBUTION_ID = "distribution_id"
val CREATE_TABLE = """ val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
@ -33,7 +38,8 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE, $MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE, $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
$SENT_TIMESTAMP INTEGER NOT NULL, $SENT_TIMESTAMP INTEGER NOT NULL,
$ALLOWS_REPLIES INTEGER NOT NULL $ALLOWS_REPLIES INTEGER NOT NULL,
$DISTRIBUTION_ID TEXT NOT NULL REFERENCES ${DistributionListDatabase.LIST_TABLE_NAME} (${DistributionListDatabase.DISTRIBUTION_ID}) ON DELETE CASCADE
) )
""".trimIndent() """.trimIndent()
@ -42,7 +48,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
""".trimIndent() """.trimIndent()
} }
fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean) { fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean, distributionId: DistributionId) {
val db = writableDatabase val db = writableDatabase
db.beginTransaction() db.beginTransaction()
@ -52,11 +58,12 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
MESSAGE_ID to messageId, MESSAGE_ID to messageId,
RECIPIENT_ID to id.serialize(), RECIPIENT_ID to id.serialize(),
SENT_TIMESTAMP to sentTimestamp, SENT_TIMESTAMP to sentTimestamp,
ALLOWS_REPLIES to allowsReplies.toInt() ALLOWS_REPLIES to allowsReplies.toInt(),
DISTRIBUTION_ID to distributionId.toString()
) )
} }
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES), insertValues) SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES, DISTRIBUTION_ID), insertValues)
.forEach { query -> db.execSQL(query.where, query.whereArgs) } .forEach { query -> db.execSQL(query.where, query.whereArgs) }
db.setTransactionSuccessful() db.setTransactionSuccessful()
@ -177,4 +184,208 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
writableDatabase.update(TABLE_NAME, values, query, args) writableDatabase.update(TABLE_NAME, values, query, args)
} }
/**
* Gets the manifest for a given story, or null if the story should NOT be the one reporting the manifest.
*/
fun getFullSentStorySyncManifest(messageId: Long, sentTimestamp: Long): SentStorySyncManifest? {
val firstMessageId: Long = readableDatabase.select(MESSAGE_ID)
.from(TABLE_NAME)
.where(
"""
$SENT_TIMESTAMP = ? AND
(SELECT ${MmsDatabase.REMOTE_DELETED} FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.ID} = $MESSAGE_ID) = 0
""".trimIndent(),
sentTimestamp
)
.orderBy(MESSAGE_ID)
.limit(1)
.run()
.use {
if (it.moveToFirst()) {
CursorUtil.requireLong(it, MESSAGE_ID)
} else {
-1L
}
}
if (firstMessageId == -1L || firstMessageId != messageId) {
return null
}
return getLocalManifest(sentTimestamp)
}
/**
* Gets the manifest after a change to the available distribution lists occurs. This will only include the recipients
* as specified by onlyInclude, and is meant to represent a delta rather than an entire manifest.
*/
fun getSentStorySyncManifestForUpdate(sentTimestamp: Long, onlyInclude: Set<RecipientId>): SentStorySyncManifest {
val localManifest: SentStorySyncManifest = getLocalManifest(sentTimestamp)
val entries: List<SentStorySyncManifest.Entry> = localManifest.entries.filter { it.recipientId in onlyInclude }
return SentStorySyncManifest(entries)
}
/**
* Manifest updates should only include the specific recipients who have changes (normally, one less distribution list),
* and of those, only the ones that have a non-empty set of distribution lists.
*
* @return A set of recipients who were able to receive the deleted story, and still have other stories at the same timestamp.
*/
fun getRecipientIdsForManifestUpdate(sentTimestamp: Long, deletedMessageId: Long): Set<RecipientId> {
// language=sql
val query = """
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE $SENT_TIMESTAMP = ?
AND $RECIPIENT_ID IN (
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE $MESSAGE_ID = ?
)
AND $MESSAGE_ID IN (
SELECT ${MmsDatabase.ID}
FROM ${MmsDatabase.TABLE_NAME}
WHERE ${MmsDatabase.REMOTE_DELETED} = 0
)
""".trimIndent()
return readableDatabase.rawQuery(query, arrayOf(sentTimestamp, deletedMessageId)).use { cursor ->
if (cursor.count == 0) emptyList<RecipientId>()
val results: MutableSet<RecipientId> = hashSetOf()
while (cursor.moveToNext()) {
results.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)))
}
results
}
}
/**
* Applies the given manifest to the local database. This method will:
*
* 1. Generate the local manifest
* 1. Gather the unique collective distribution id set from remote and local manifests
* 1. Flatten both manifests into a set of Rows
* 1. For each changed manifest row in remote, update the corresponding row in local
* 1. For each new manifest row in remote, update the corresponding row in local
* 1. For each unique message id in local not present in remote, we can assume that the message can be marked deleted.
*/
fun applySentStoryManifest(remoteManifest: SentStorySyncManifest, sentTimestamp: Long) {
if (remoteManifest.entries.isEmpty()) {
return
}
writableDatabase.beginTransaction()
try {
val localManifest: SentStorySyncManifest = getLocalManifest(sentTimestamp)
val query = """
SELECT ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID} as $MESSAGE_ID, ${DistributionListDatabase.DISTRIBUTION_ID}
FROM ${MmsDatabase.TABLE_NAME}
INNER JOIN ${DistributionListDatabase.LIST_TABLE_NAME} ON ${DistributionListDatabase.RECIPIENT_ID} = ${MmsDatabase.RECIPIENT_ID}
WHERE ${MmsDatabase.DATE_SENT} = $sentTimestamp AND ${DistributionListDatabase.DISTRIBUTION_ID} IS NOT NULL
""".trimIndent()
val distributionIdToMessageId = readableDatabase.query(query).use { cursor ->
val results: MutableMap<DistributionId, Long> = mutableMapOf()
while (cursor.moveToNext()) {
val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DistributionListDatabase.DISTRIBUTION_ID))
val messageId = CursorUtil.requireLong(cursor, MESSAGE_ID)
results[distributionId] = messageId
}
results
}
val localRows: Set<SentStorySyncManifest.Row> = localManifest.flattenToRows(distributionIdToMessageId)
val remoteRows: Set<SentStorySyncManifest.Row> = remoteManifest.flattenToRows(distributionIdToMessageId)
if (localRows == remoteRows) {
return
}
val remoteOnly: List<SentStorySyncManifest.Row> = remoteRows.filterNot { localRows.contains(it) }
val changedInRemoteManifest: List<SentStorySyncManifest.Row> = remoteOnly.filter { (recipientId, messageId) -> localRows.any { it.messageId == messageId && it.recipientId == recipientId } }
val newInRemoteManifest: List<SentStorySyncManifest.Row> = remoteOnly.filterNot { (recipientId, messageId) -> localRows.any { it.messageId == messageId && it.recipientId == recipientId } }
changedInRemoteManifest
.forEach { (recipientId, messageId, allowsReplies, distributionId) ->
writableDatabase.update(TABLE_NAME)
.values(
contentValuesOf(
ALLOWS_REPLIES to allowsReplies,
RECIPIENT_ID to recipientId.toLong(),
SENT_TIMESTAMP to sentTimestamp,
MESSAGE_ID to messageId,
DISTRIBUTION_ID to distributionId.toString()
)
)
}
newInRemoteManifest
.forEach { (recipientId, messageId, allowsReplies, distributionId) ->
writableDatabase.insert(
TABLE_NAME,
null,
contentValuesOf(
ALLOWS_REPLIES to allowsReplies,
RECIPIENT_ID to recipientId.toLong(),
SENT_TIMESTAMP to sentTimestamp,
MESSAGE_ID to messageId,
DISTRIBUTION_ID to distributionId.toString()
)
)
}
val messagesWithoutAnyReceivers = localRows.map { it.messageId }.distinct() - remoteRows.map { it.messageId }.distinct()
messagesWithoutAnyReceivers.forEach { SignalDatabase.mms.markAsRemoteDelete(it) }
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
private fun getLocalManifest(sentTimestamp: Long): SentStorySyncManifest {
val entries = readableDatabase.rawQuery(
// language=sql
"""
SELECT
$RECIPIENT_ID,
$ALLOWS_REPLIES,
$DISTRIBUTION_ID
FROM $TABLE_NAME
WHERE $TABLE_NAME.$SENT_TIMESTAMP = ? AND (
SELECT ${MmsDatabase.REMOTE_DELETED}
FROM ${MmsDatabase.TABLE_NAME}
WHERE ${MmsDatabase.ID} = $TABLE_NAME.$MESSAGE_ID
) = 0
""".trimIndent(),
arrayOf(sentTimestamp)
).use { cursor ->
val results: MutableMap<RecipientId, SentStorySyncManifest.Entry> = mutableMapOf()
while (cursor.moveToNext()) {
val recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))
val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DISTRIBUTION_ID))
val allowsReplies = CursorUtil.requireBoolean(cursor, ALLOWS_REPLIES)
val entry = results[recipientId]?.let {
it.copy(
allowedToReply = it.allowedToReply or allowsReplies,
distributionLists = it.distributionLists + distributionId
)
} ?: SentStorySyncManifest.Entry(recipientId, canReply(recipientId, sentTimestamp), listOf(distributionId))
results[recipientId] = entry
}
results
}
return SentStorySyncManifest(entries.values.toList())
}
} }

View file

@ -1300,19 +1300,23 @@ public class ThreadDatabase extends Database {
private boolean update(long threadId, boolean unarchive, boolean allowDeletion, boolean notifyListeners) { private boolean update(long threadId, boolean unarchive, boolean allowDeletion, boolean notifyListeners) {
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms(); MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId); boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId);
boolean isPinned = getPinnedThreadIds().contains(threadId);
boolean shouldDelete = allowDeletion && !isPinned && !SignalDatabase.mms().containsStories(threadId);
if (!meaningfulMessages) { if (!meaningfulMessages) {
if (allowDeletion) { if (shouldDelete) {
deleteConversation(threadId); deleteConversation(threadId);
return true;
} else if (!isPinned) {
return false;
} }
return true;
} }
MessageRecord record; MessageRecord record;
try { try {
record = mmsSmsDatabase.getConversationSnippet(threadId); record = mmsSmsDatabase.getConversationSnippet(threadId);
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
if (allowDeletion && !SignalDatabase.mms().containsStories(threadId)) { if (shouldDelete) {
deleteConversation(threadId); deleteConversation(threadId);
} }
return true; return true;

View file

@ -196,8 +196,9 @@ object SignalDatabaseMigrations {
private const val CDS_V2 = 140 private const val CDS_V2 = 140
private const val GROUP_SERVICE_ID = 141 private const val GROUP_SERVICE_ID = 141
private const val QUOTE_TYPE = 142 private const val QUOTE_TYPE = 142
private const val STORY_SYNCS = 143
const val DATABASE_VERSION = 142 const val DATABASE_VERSION = 143
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -2533,6 +2534,39 @@ object SignalDatabaseMigrations {
if (oldVersion < QUOTE_TYPE) { if (oldVersion < QUOTE_TYPE) {
db.execSQL("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0") db.execSQL("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0")
} }
if (oldVersion < STORY_SYNCS) {
db.execSQL("ALTER TABLE distribution_list ADD COLUMN is_unknown INTEGER DEFAULT 0")
db.execSQL(
"""
CREATE TABLE story_sends_tmp (
_id INTEGER PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE,
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
sent_timestamp INTEGER NOT NULL,
allows_replies INTEGER NOT NULL,
distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO story_sends_tmp (_id, message_id, recipient_id, sent_timestamp, allows_replies, distribution_id)
SELECT story_sends._id, story_sends.message_id, story_sends.recipient_id, story_sends.sent_timestamp, story_sends.allows_replies, distribution_list.distribution_id
FROM story_sends
INNER JOIN mms ON story_sends.message_id = mms._id
INNER JOIN distribution_list ON distribution_list.recipient_id = mms.address
""".trimIndent()
)
db.execSQL("DROP TABLE story_sends")
db.execSQL("DROP INDEX IF EXISTS story_sends_recipient_id_sent_timestamp_allows_replies_index")
db.execSQL("ALTER TABLE story_sends_tmp RENAME TO story_sends")
db.execSQL("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")
}
} }
@JvmStatic @JvmStatic

View file

@ -6,5 +6,6 @@ data class DistributionListPartialRecord(
val id: DistributionListId, val id: DistributionListId,
val name: CharSequence, val name: CharSequence,
val recipientId: RecipientId, val recipientId: RecipientId,
val allowsReplies: Boolean val allowsReplies: Boolean,
val isUnknown: Boolean
) )

View file

@ -12,5 +12,6 @@ data class DistributionListRecord(
val distributionId: DistributionId, val distributionId: DistributionId,
val allowsReplies: Boolean, val allowsReplies: Boolean,
val members: List<RecipientId>, val members: List<RecipientId>,
val deletedAtTimestamp: Long val deletedAtTimestamp: Long,
val isUnknown: Boolean
) )

View file

@ -170,6 +170,7 @@ public final class JobManagerFactories {
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application));
put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory());
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
put(MultiDeviceStorySendSyncJob.KEY, new MultiDeviceStorySendSyncJob.Factory());
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory()); put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
put(SmsSendJob.KEY, new SmsSendJob.Factory()); put(SmsSendJob.KEY, new SmsSendJob.Factory());

View file

@ -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)
)
}
}
}

View file

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
@ -36,6 +37,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException; import java.io.IOException;
@ -175,6 +177,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
Log.i(TAG, JobLogger.format(this, "Finished send.")); Log.i(TAG, JobLogger.format(this, "Finished send."));
PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, targets, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches); PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, targets, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches);
} catch (UntrustedIdentityException | UndeliverableMessageException e) { } catch (UntrustedIdentityException | UndeliverableMessageException e) {
warn(TAG, String.valueOf(message.getSentTimeMillis()), e); warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
database.markAsSentFailed(messageId); database.markAsSentFailed(messageId);
@ -206,7 +209,11 @@ public final class PushDistributionListSendJob extends PushSendJob {
} else { } else {
throw new UndeliverableMessageException("No attachment on non-text story."); throw new UndeliverableMessageException("No attachment on non-text story.");
} }
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
SentStorySyncManifest manifest = SignalDatabase.storySends().getFullSentStorySyncManifest(messageId, message.getSentTimeMillis());
Set<SignalServiceStoryMessageRecipient> manifestCollection = manifest != null ? manifest.toRecipientsSet() : Collections.emptySet();
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage, manifestCollection);
} catch (ServerRejectedException e) { } catch (ServerRejectedException e) {
throw new UndeliverableMessageException(e); throw new UndeliverableMessageException(e);
} }

View file

@ -1,21 +1,22 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.signal.core.util.SetUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.messages.GroupSendUtil;
import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -23,7 +24,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -53,9 +53,7 @@ public class RemoteDeleteSendJob extends BaseJob {
@WorkerThread @WorkerThread
public static @NonNull RemoteDeleteSendJob create(@NonNull Context context, public static @NonNull JobManager.Chain create(long messageId, boolean isMms)
long messageId,
boolean isMms)
throws NoSuchMessageException throws NoSuchMessageException
{ {
MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId) MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId)
@ -70,6 +68,9 @@ public class RemoteDeleteSendJob extends BaseJob {
List<RecipientId> recipients; List<RecipientId> recipients;
if (conversationRecipient.isDistributionList()) { if (conversationRecipient.isDistributionList()) {
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp()); recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
if (recipients.isEmpty()) {
return ApplicationDependencies.getJobManager().startChain(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId));
}
} else { } else {
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList() recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList(); : Stream.of(conversationRecipient.getId()).toList();
@ -77,15 +78,23 @@ public class RemoteDeleteSendJob extends BaseJob {
recipients.remove(Recipient.self().getId()); recipients.remove(Recipient.self().getId());
return new RemoteDeleteSendJob(messageId, RemoteDeleteSendJob sendJob = new RemoteDeleteSendJob(messageId,
isMms, isMms,
recipients, recipients,
recipients.size(), recipients.size(),
new Parameters.Builder() new Parameters.Builder()
.setQueue(conversationRecipient.getId().toQueueKey()) .setQueue(conversationRecipient.getId().toQueueKey())
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED) .setMaxAttempts(Parameters.UNLIMITED)
.build()); .build());
if (conversationRecipient.isDistributionList()) {
return ApplicationDependencies.getJobManager()
.startChain(sendJob)
.then(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId));
} else {
return ApplicationDependencies.getJobManager().startChain(sendJob);
}
} }
private RemoteDeleteSendJob(long messageId, private RemoteDeleteSendJob(long messageId,

View file

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.MessageSendLogDatabase; import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId; import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
@ -38,6 +39,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.DistributionId;
@ -49,6 +51,7 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteLi
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -157,7 +160,8 @@ public final class GroupSendUtil {
boolean isRecipientUpdate, boolean isRecipientUpdate,
@NonNull MessageId messageId, @NonNull MessageId messageId,
long sentTimestamp, long sentTimestamp,
@NonNull SignalServiceStoryMessage message) @NonNull SignalServiceStoryMessage message,
@NonNull Set<SignalServiceStoryMessageRecipient> manifest)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
return sendMessage( return sendMessage(
@ -167,7 +171,7 @@ public final class GroupSendUtil {
messageId, messageId,
allTargets, allTargets,
isRecipientUpdate, isRecipientUpdate,
new StorySendOperation(messageId, null, sentTimestamp, message), new StorySendOperation(messageId, null, sentTimestamp, message, manifest),
null); null);
} }
@ -193,7 +197,7 @@ public final class GroupSendUtil {
messageId, messageId,
allTargets, allTargets,
isRecipientUpdate, isRecipientUpdate,
new StorySendOperation(messageId, groupId, sentTimestamp, message), new StorySendOperation(messageId, groupId, sentTimestamp, message, Collections.emptySet()),
null); null);
} }
@ -605,16 +609,23 @@ public final class GroupSendUtil {
public static class StorySendOperation implements SendOperation { public static class StorySendOperation implements SendOperation {
private final MessageId relatedMessageId; private final MessageId relatedMessageId;
private final GroupId groupId; private final GroupId groupId;
private final long sentTimestamp; private final long sentTimestamp;
private final SignalServiceStoryMessage message; private final SignalServiceStoryMessage message;
private final Set<SignalServiceStoryMessageRecipient> manifest;
public StorySendOperation(@NonNull MessageId relatedMessageId, @Nullable GroupId groupId, long sentTimestamp, @NonNull SignalServiceStoryMessage message) { public StorySendOperation(@NonNull MessageId relatedMessageId,
@Nullable GroupId groupId,
long sentTimestamp,
@NonNull SignalServiceStoryMessage message,
@NonNull Set<SignalServiceStoryMessageRecipient> manifest)
{
this.relatedMessageId = relatedMessageId; this.relatedMessageId = relatedMessageId;
this.groupId = groupId; this.groupId = groupId;
this.sentTimestamp = sentTimestamp; this.sentTimestamp = sentTimestamp;
this.message = message; this.message = message;
this.manifest = manifest;
} }
@Override @Override
@ -625,7 +636,7 @@ public final class GroupSendUtil {
boolean isRecipientUpdate) boolean isRecipientUpdate)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
{ {
return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, message, getSentTimestamp()); return messageSender.sendGroupStory(distributionId, Optional.ofNullable(groupId).map(GroupId::getDecodedId), targets, access, message, getSentTimestamp(), manifest);
} }
@Override @Override
@ -637,7 +648,7 @@ public final class GroupSendUtil {
@Nullable CancelationSignal cancelationSignal) @Nullable CancelationSignal cancelationSignal)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
return messageSender.sendStory(targets, access, message, getSentTimestamp()); return messageSender.sendStory(targets, access, message, getSentTimestamp(), manifest);
} }
@Override @Override

View file

@ -39,14 +39,17 @@ import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PaymentDatabase; import org.thoughtcrime.securesms.database.PaymentDatabase;
import org.thoughtcrime.securesms.database.PaymentMetaDataUtil; import org.thoughtcrime.securesms.database.PaymentMetaDataUtil;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SentStorySyncManifest;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.DistributionListId;
import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageLogEntry; import org.thoughtcrime.securesms.database.model.MessageLogEntry;
@ -513,11 +516,11 @@ public final class MessageContentProcessor {
private static @Nullable SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) { private static @Nullable SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) {
if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) { if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) {
return content.getDataMessage().get().getGroupContext().get(); return content.getDataMessage().get().getGroupContext().get();
} else if (content.getSyncMessage().isPresent() && } else if (content.getSyncMessage().isPresent() &&
content.getSyncMessage().get().getSent().isPresent() && content.getSyncMessage().get().getSent().isPresent() &&
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent()) content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().isPresent())
{ {
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get(); return content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().get();
} else { } else {
return null; return null;
} }
@ -1203,9 +1206,15 @@ public final class MessageContentProcessor {
try { try {
GroupDatabase groupDatabase = SignalDatabase.groups(); GroupDatabase groupDatabase = SignalDatabase.groups();
if (message.getMessage().isGroupV2Message()) { if (message.getStoryMessage().isPresent() || !message.getStoryMessageRecipients().isEmpty()) {
GroupId.V2 groupId = GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey()); handleSynchronizeSentStoryMessage(message, content.getTimestamp());
if (handleGv2PreProcessing(groupId, content, message.getMessage().getGroupContext().get().getGroupV2().get(), senderRecipient)) { return;
}
SignalServiceDataMessage dataMessage = message.getDataMessage().get();
if (dataMessage.isGroupV2Message()) {
GroupId.V2 groupId = GroupId.v2(dataMessage.getGroupContext().get().getGroupV2().get().getMasterKey());
if (handleGv2PreProcessing(groupId, content, dataMessage.getGroupContext().get().getGroupV2().get(), senderRecipient)) {
return; return;
} }
} }
@ -1214,38 +1223,38 @@ public final class MessageContentProcessor {
if (message.isRecipientUpdate()) { if (message.isRecipientUpdate()) {
handleGroupRecipientUpdate(message, content.getTimestamp()); handleGroupRecipientUpdate(message, content.getTimestamp());
} else if (message.getMessage().isEndSession()) { } else if (dataMessage.isEndSession()) {
threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp()); threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp());
} else if (message.getMessage().isGroupV1Update()) { } else if (dataMessage.isGroupV1Update()) {
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true); Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, dataMessage, true);
threadId = gv1ThreadId == null ? -1 : gv1ThreadId; threadId = gv1ThreadId == null ? -1 : gv1ThreadId;
} else if (message.getMessage().isGroupV2Update()) { } else if (dataMessage.isGroupV2Update()) {
handleSynchronizeSentGv2Update(content, message); handleSynchronizeSentGv2Update(content, message);
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
} else if (Build.VERSION.SDK_INT > 19 && message.getMessage().getGroupCallUpdate().isPresent()) { } else if (Build.VERSION.SDK_INT > 19 && dataMessage.getGroupCallUpdate().isPresent()) {
handleGroupCallUpdateMessage(content, message.getMessage(), GroupUtil.idFromGroupContext(message.getMessage().getGroupContext()), senderRecipient); handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient);
} else if (message.getMessage().isEmptyGroupV2Message()) { } else if (dataMessage.isEmptyGroupV2Message()) {
warn(content.getTimestamp(), "Empty GV2 message! Doing nothing."); warn(content.getTimestamp(), "Empty GV2 message! Doing nothing.");
} else if (message.getMessage().isExpirationUpdate()) { } else if (dataMessage.isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(message); threadId = handleSynchronizeSentExpirationUpdate(message);
} else if (message.getMessage().getStoryContext().isPresent()) { } else if (dataMessage.getStoryContext().isPresent()) {
threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp()); threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp());
} else if (message.getMessage().getReaction().isPresent()) { } else if (dataMessage.getReaction().isPresent()) {
handleReaction(content, message.getMessage(), senderRecipient); handleReaction(content, dataMessage, senderRecipient);
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
} else if (message.getMessage().getRemoteDelete().isPresent()) { } else if (dataMessage.getRemoteDelete().isPresent()) {
handleRemoteDelete(content, message.getMessage(), senderRecipient); handleRemoteDelete(content, dataMessage, senderRecipient);
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce() || message.getMessage().getMentions().isPresent()) { } else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp()); threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp());
} else { } else {
threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp()); threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp());
} }
if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) { if (dataMessage.getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(dataMessage.getGroupContext().get()))) {
handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get(), senderRecipient); handleUnknownGroupMessage(content, dataMessage.getGroupContext().get(), senderRecipient);
} }
if (message.getMessage().getProfileKey().isPresent()) { if (dataMessage.getProfileKey().isPresent()) {
Recipient recipient = getSyncMessageDestination(message); Recipient recipient = getSyncMessageDestination(message);
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
@ -1275,8 +1284,9 @@ public final class MessageContentProcessor {
{ {
log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp()); log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp());
SignalServiceGroupV2 signalServiceGroupV2 = message.getMessage().getGroupContext().get().getGroupV2().get(); SignalServiceDataMessage dataMessage = message.getDataMessage().get();
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey()); SignalServiceGroupV2 signalServiceGroupV2 = dataMessage.getGroupContext().get().getGroupV2().get();
GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey());
if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) { if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) {
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2); log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
@ -1873,14 +1883,14 @@ public final class MessageContentProcessor {
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
message.getTimestamp(), message.getTimestamp(),
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds())); TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null); long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
database.markAsSent(messageId, true); database.markAsSent(messageId, true);
SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getMessage().getExpiresInSeconds()); SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getDataMessage().get().getExpiresInSeconds());
return threadId; return threadId;
} }
@ -1899,9 +1909,9 @@ public final class MessageContentProcessor {
log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp()); log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp());
try { try {
Optional<SignalServiceDataMessage.Reaction> reaction = message.getMessage().getReaction(); Optional<SignalServiceDataMessage.Reaction> reaction = message.getDataMessage().get().getReaction();
ParentStoryId parentStoryId; ParentStoryId parentStoryId;
SignalServiceDataMessage.StoryContext storyContext = message.getMessage().getStoryContext().get(); SignalServiceDataMessage.StoryContext storyContext = message.getDataMessage().get().getStoryContext().get();
MessageDatabase database = SignalDatabase.mms(); MessageDatabase database = SignalDatabase.mms();
Recipient recipient = getSyncMessageDestination(message); Recipient recipient = getSyncMessageDestination(message);
QuoteModel quoteModel = null; QuoteModel quoteModel = null;
@ -1916,10 +1926,10 @@ public final class MessageContentProcessor {
if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) { if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) {
body = reaction.get().getEmoji(); body = reaction.get().getEmoji();
} else { } else {
body = message.getMessage().getBody().orElse(null); body = message.getDataMessage().get().getBody().orElse(null);
} }
if (message.getMessage().getGroupContext().isPresent()) { if (message.getDataMessage().get().getGroupContext().isPresent()) {
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
} else if (groupStory || SignalDatabase.storySends().canReply(storyAuthorRecipient, storyContext.getSentTimestamp())) { } else if (groupStory || SignalDatabase.storySends().canReply(storyAuthorRecipient, storyContext.getSentTimestamp())) {
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
@ -1946,18 +1956,18 @@ public final class MessageContentProcessor {
ThreadDatabase.DistributionTypes.DEFAULT, ThreadDatabase.DistributionTypes.DEFAULT,
StoryType.NONE, StoryType.NONE,
parentStoryId, parentStoryId,
message.getMessage().getReaction().isPresent(), message.getDataMessage().get().getReaction().isPresent(),
quoteModel, quoteModel,
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(),
getMentions(message.getMessage().getMentions()).orElse(Collections.emptyList()), getMentions(message.getDataMessage().get().getMentions()).orElse(Collections.emptyList()),
Collections.emptySet(), Collections.emptySet(),
Collections.emptySet(), Collections.emptySet(),
null); null);
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) { if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(message); handleSynchronizeSentExpirationUpdate(message);
} }
@ -1976,13 +1986,13 @@ public final class MessageContentProcessor {
database.markAsSent(messageId, true); database.markAsSent(messageId, true);
if (message.getMessage().getExpiresInSeconds() > 0) { if (message.getDataMessage().get().getExpiresInSeconds() > 0) {
database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationDependencies.getExpiringMessageManager() ApplicationDependencies.getExpiringMessageManager()
.scheduleDeletion(messageId, .scheduleDeletion(messageId,
true, true,
message.getExpirationStartTimestamp(), message.getExpirationStartTimestamp(),
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds())); TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
} }
if (recipient.isSelf()) { if (recipient.isSelf()) {
@ -2003,6 +2013,129 @@ public final class MessageContentProcessor {
} }
} }
private void handleSynchronizeSentStoryMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException {
log(envelopeTimestamp, "Synchronize sent story message for " + message.getTimestamp());
SentStorySyncManifest manifest = SentStorySyncManifest.fromRecipientsSet(message.getStoryMessageRecipients());
if (message.isRecipientUpdate()) {
log(envelopeTimestamp, "Processing recipient update for story message and exiting...");
SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp());
return;
}
SignalServiceStoryMessage storyMessage = message.getStoryMessage().get();
Set<DistributionId> distributionIds = manifest.getDistributionIdSet();
Optional<GroupId> groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey()));
String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null);
StoryType storyType = getStoryType(storyMessage);
List<LinkPreview> linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)),
"",
true).orElse(Collections.emptyList());
List<Attachment> attachments = PointerAttachment.forPointers(storyMessage.getFileAttachment()
.map(SignalServiceAttachment::asPointer)
.map(Collections::singletonList));
for (final DistributionId distributionId : distributionIds) {
RecipientId distributionRecipientId = SignalDatabase.distributionLists().getOrCreateByDistributionId(distributionId, manifest);
Recipient distributionListRecipient = Recipient.resolved(distributionRecipientId);
insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
}
if (groupId.isPresent()) {
Optional<RecipientId> groupRecipient = SignalDatabase.recipients().getByGroupId(groupId.get());
if (groupRecipient.isPresent()) {
insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
}
}
SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp());
}
private void insertSentStoryMessage(@NonNull SentTranscriptMessage message,
@NonNull Recipient recipient,
@Nullable String textStoryBody,
@NonNull List<Attachment> pendingAttachments,
long sentAtTimestamp,
@NonNull StoryType storyType,
@NonNull List<LinkPreview> linkPreviews)
throws MmsException
{
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
textStoryBody,
pendingAttachments,
sentAtTimestamp,
-1,
0,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
storyType,
null,
false,
null,
Collections.emptyList(),
linkPreviews,
Collections.emptyList(),
Collections.emptySet(),
Collections.emptySet(),
null);
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
MmsDatabase database = SignalDatabase.mms();
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
long messageId;
List<DatabaseAttachment> attachments;
database.beginTransaction();
try {
messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
if (recipient.isGroup()) {
updateGroupReceiptStatus(message, messageId, recipient.requireGroupId());
} else {
database.markUnidentified(messageId, isUnidentified(message, recipient));
}
database.markAsSent(messageId, true);
List<DatabaseAttachment> allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId);
attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
if (recipient.isSelf()) {
SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp());
SignalDatabase.mmsSms().incrementDeliveryReceiptCount(id, System.currentTimeMillis());
SignalDatabase.mmsSms().incrementReadReceiptCount(id, System.currentTimeMillis());
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
for (DatabaseAttachment attachment : attachments) {
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false));
}
}
private @NonNull StoryType getStoryType(SignalServiceStoryMessage storyMessage) {
if (storyMessage.getAllowsReplies().orElse(false)) {
if (storyMessage.getTextAttachment().isPresent()) {
return StoryType.TEXT_STORY_WITH_REPLIES;
} else {
return StoryType.STORY_WITH_REPLIES;
}
} else {
if (storyMessage.getTextAttachment().isPresent()) {
return StoryType.TEXT_STORY_WITHOUT_REPLIES;
} else {
return StoryType.STORY_WITHOUT_REPLIES;
}
}
}
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp)
throws MmsException, BadGroupIdException throws MmsException, BadGroupIdException
{ {
@ -2010,26 +2143,26 @@ public final class MessageContentProcessor {
MessageDatabase database = SignalDatabase.mms(); MessageDatabase database = SignalDatabase.mms();
Recipient recipients = getSyncMessageDestination(message); Recipient recipients = getSyncMessageDestination(message);
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote()); Optional<QuoteModel> quote = getValidatedQuote(message.getDataMessage().get().getQuote());
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker()); Optional<Attachment> sticker = getStickerAttachment(message.getDataMessage().get().getSticker());
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts()); Optional<List<Contact>> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts());
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().orElse(""), false); Optional<List<LinkPreview>> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false);
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions()); Optional<List<Mention>> mentions = getMentions(message.getDataMessage().get().getMentions());
Optional<GiftBadge> giftBadge = getGiftBadge(message.getMessage().getGiftBadge()); Optional<GiftBadge> giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge());
boolean viewOnce = message.getMessage().isViewOnce(); boolean viewOnce = message.getDataMessage().get().isViewOnce();
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false)) List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
: PointerAttachment.forPointers(message.getMessage().getAttachments()); : PointerAttachment.forPointers(message.getDataMessage().get().getAttachments());
if (sticker.isPresent()) { if (sticker.isPresent()) {
syncAttachments.add(sticker.get()); syncAttachments.add(sticker.get());
} }
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients,
message.getMessage().getBody().orElse(null), message.getDataMessage().get().getBody().orElse(null),
syncAttachments, syncAttachments,
message.getTimestamp(), message.getTimestamp(),
-1, -1,
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()), TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()),
viewOnce, viewOnce,
ThreadDatabase.DistributionTypes.DEFAULT, ThreadDatabase.DistributionTypes.DEFAULT,
StoryType.NONE, StoryType.NONE,
@ -2045,7 +2178,7 @@ public final class MessageContentProcessor {
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
if (recipients.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) { if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(message); handleSynchronizeSentExpirationUpdate(message);
} }
@ -2072,13 +2205,13 @@ public final class MessageContentProcessor {
stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
if (message.getMessage().getExpiresInSeconds() > 0) { if (message.getDataMessage().get().getExpiresInSeconds() > 0) {
database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
ApplicationDependencies.getExpiringMessageManager() ApplicationDependencies.getExpiringMessageManager()
.scheduleDeletion(messageId, .scheduleDeletion(messageId,
true, true,
message.getExpirationStartTimestamp(), message.getExpirationStartTimestamp(),
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds())); TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()));
} }
if (recipients.isSelf()) { if (recipients.isSelf()) {
@ -2204,10 +2337,10 @@ public final class MessageContentProcessor {
log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp()); log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp());
Recipient recipient = getSyncMessageDestination(message); Recipient recipient = getSyncMessageDestination(message);
String body = message.getMessage().getBody().orElse(""); String body = message.getDataMessage().get().getBody().orElse("");
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()); long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
if (recipient.getExpiresInSeconds() != message.getMessage().getExpiresInSeconds()) { if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
handleSynchronizeSentExpirationUpdate(message); handleSynchronizeSentExpirationUpdate(message);
} }
@ -2862,7 +2995,7 @@ public final class MessageContentProcessor {
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
throws BadGroupIdException throws BadGroupIdException
{ {
return getGroupRecipient(message.getMessage().getGroupContext()).orElseGet(() -> Recipient.externalPush(message.getDestination().get())); return getGroupRecipient(message.getDataMessage().get().getGroupContext()).orElseGet(() -> Recipient.externalPush(message.getDestination().get()));
} }
private Recipient getMessageDestination(@NonNull SignalServiceContent content) throws BadGroupIdException { private Recipient getMessageDestination(@NonNull SignalServiceContent content) throws BadGroupIdException {

View file

@ -251,7 +251,7 @@ public final class LiveRecipient {
// TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members. // TODO [stories] We'll have to see what the perf is like for very large distribution lists. We may not be able to support fetching all the members.
if (groupRecord != null) { if (groupRecord != null) {
String title = groupRecord.getName(); String title = groupRecord.isUnknown() ? null : groupRecord.getName();
List<Recipient> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList(); List<Recipient> members = Stream.of(groupRecord.getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList();
return RecipientDetails.forDistributionList(title, members, record); return RecipientDetails.forDistributionList(title, members, record);

View file

@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.ParcelUtil; import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Preconditions;
import java.io.IOException; import java.io.IOException;
@ -86,6 +87,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -164,8 +166,9 @@ public class MessageSender {
Recipient recipient = message.getRecipient(); Recipient recipient = message.getRecipient();
if (recipient.isDistributionList()) { if (recipient.isDistributionList()) {
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId()); DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies()); List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
} }
} }
@ -381,8 +384,9 @@ public class MessageSender {
Recipient recipient = message.getRecipient(); Recipient recipient = message.getRecipient();
if (recipient.isDistributionList()) { if (recipient.isDistributionList()) {
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId()); List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies()); DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
} }
} }
@ -471,7 +475,7 @@ public class MessageSender {
db.markAsSending(messageId); db.markAsSending(messageId);
try { try {
ApplicationDependencies.getJobManager().add(RemoteDeleteSendJob.create(context, messageId, isMms)); RemoteDeleteSendJob.create(messageId, isMms).enqueue();
onMessageSent(); onMessageSent();
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring."); Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring.");

View file

@ -118,7 +118,7 @@ object PrivateStoryItem {
override fun bind(model: PartialModel) { override fun bind(model: PartialModel) {
itemView.setOnClickListener { model.onClick(model) } itemView.setOnClickListener { model.onClick(model) }
label.text = model.privateStoryItemData.name label.text = if (model.privateStoryItemData.isUnknown) context.getString(R.string.MessageRecord_unknown) else model.privateStoryItemData.name
} }
} }
} }

View file

@ -51,9 +51,10 @@ public final class GroupUtil {
return content.getDataMessage().get().getGroupContext().get(); return content.getDataMessage().get().getGroupContext().get();
} else if (content.getSyncMessage().isPresent() && } else if (content.getSyncMessage().isPresent() &&
content.getSyncMessage().get().getSent().isPresent() && content.getSyncMessage().get().getSent().isPresent() &&
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent()) content.getSyncMessage().get().getSent().get().getDataMessage().isPresent() &&
content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().isPresent())
{ {
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get(); return content.getSyncMessage().get().getSent().get().getDataMessage().get().getGroupContext().get();
} else if (content.getStoryMessage().isPresent() && content.getStoryMessage().get().getGroupContext().isPresent()) { } else if (content.getStoryMessage().isPresent() && content.getStoryMessage().get().getGroupContext().isPresent()) {
try { try {
return SignalServiceGroupContext.create(null, content.getStoryMessage().get().getGroupContext().get()); return SignalServiceGroupContext.create(null, content.getStoryMessage().get().getGroupContext().get());

View file

@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@ -252,32 +253,43 @@ public class SignalServiceMessageSender {
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY); sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId(), true, SenderKeyGroupEvents.EMPTY);
} }
public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients, public List<SendMessageResult> sendStory(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SignalServiceStoryMessage message, SignalServiceStoryMessage message,
long timestamp) long timestamp,
Set<SignalServiceStoryMessageRecipient> manifest)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
Content content = createStoryContent(message); Content content = createStoryContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty()); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.RESENDABLE, Optional.empty());
List<SendMessageResult> sendMessageResults = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null);
SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, manifest);
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null); sendSyncMessage(syncMessage, Optional.empty());
return sendMessageResults;
} }
/** /**
* Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort. * Send a typing indicator to a group using sender key. Doesn't bother with return results, since these are best-effort.
* @return * @return
*/ */
public List<SendMessageResult> sendGroupStory(DistributionId distributionId, public List<SendMessageResult> sendGroupStory(DistributionId distributionId,
Optional<byte[]> groupId, Optional<byte[]> groupId,
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess, List<UnidentifiedAccess> unidentifiedAccess,
SignalServiceStoryMessage message, SignalServiceStoryMessage message,
long timestamp) long timestamp,
Set<SignalServiceStoryMessageRecipient> manifest)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException, InvalidRegistrationIdException
{ {
Content content = createStoryContent(message); Content content = createStoryContent(message);
return sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY); List<SendMessageResult> sendMessageResults = sendGroupMessage(distributionId, recipients, unidentifiedAccess, timestamp, content, ContentHint.RESENDABLE, groupId, false, SenderKeyGroupEvents.EMPTY);
SignalServiceSyncMessage syncMessage = createSelfSendSyncMessageForStory(message, timestamp, manifest);
sendSyncMessage(syncMessage, Optional.empty());
return sendMessageResults;
} }
@ -362,7 +374,7 @@ public class SignalServiceMessageSender {
sendEvents.onMessageSent(); sendEvents.onMessageSent();
if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) { if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false); Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null); sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
@ -446,7 +458,7 @@ public class SignalServiceMessageSender {
sendEvents.onMessageSent(); sendEvents.onMessageSent();
if (store.isMultiDevice()) { if (store.isMultiDevice()) {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate); Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.empty(), message.getTimestamp(), results, isRecipientUpdate, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null); sendMessage(localAddress, Optional.empty(), message.getTimestamp(), syncMessageContent, false, null);
@ -496,7 +508,7 @@ public class SignalServiceMessageSender {
recipient = Optional.of(recipients.get(0)); recipient = Optional.of(recipients.get(0));
} }
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate); Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate, Collections.emptySet());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty()); EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.empty());
sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null); sendMessage(localAddress, Optional.empty(), timestamp, syncMessageContent, false, null);
@ -791,6 +803,16 @@ public class SignalServiceMessageSender {
return container.setReceiptMessage(builder).build(); return container.setReceiptMessage(builder).build();
} }
private Content createMessageContent(SentTranscriptMessage transcriptMessage) throws IOException {
if (transcriptMessage.getStoryMessage().isPresent()) {
return createStoryContent(transcriptMessage.getStoryMessage().get());
} else if (transcriptMessage.getDataMessage().isPresent()) {
return createMessageContent(transcriptMessage.getDataMessage().get());
} else {
return null;
}
}
private Content createMessageContent(SignalServiceDataMessage message) throws IOException { private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
Content.Builder container = Content.newBuilder(); Content.Builder container = Content.newBuilder();
DataMessage.Builder builder = DataMessage.newBuilder(); DataMessage.Builder builder = DataMessage.newBuilder();
@ -1108,27 +1130,30 @@ public class SignalServiceMessageSender {
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException { private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, boolean unidentifiedAccess) throws IOException {
SignalServiceAddress address = transcript.getDestination().get(); SignalServiceAddress address = transcript.getDestination().get();
Content content = createMessageContent(transcript.getMessage()); Content content = createMessageContent(transcript);
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.of(content)); SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess, true, -1, Optional.ofNullable(content));
return createMultiDeviceSentTranscriptContent(content, return createMultiDeviceSentTranscriptContent(content,
Optional.of(address), Optional.of(address),
transcript.getTimestamp(), transcript.getTimestamp(),
Collections.singletonList(result), Collections.singletonList(result),
false); transcript.isRecipientUpdate(),
transcript.getStoryMessageRecipients());
} }
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient, private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
long timestamp, List<SendMessageResult> sendMessageResults, long timestamp, List<SendMessageResult> sendMessageResults,
boolean isRecipientUpdate) boolean isRecipientUpdate,
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
{ {
Content.Builder container = Content.newBuilder(); Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder(); SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
DataMessage dataMessage = content.getDataMessage(); DataMessage dataMessage = content != null && content.hasDataMessage() ? content.getDataMessage() : null;
StoryMessage storyMessage = content != null && content.hasStoryMessage() ? content.getStoryMessage() : null;
sentMessage.setTimestamp(timestamp); sentMessage.setTimestamp(timestamp);
sentMessage.setMessage(dataMessage);
for (SendMessageResult result : sendMessageResults) { for (SendMessageResult result : sendMessageResults) {
if (result.getSuccess() != null) { if (result.getSuccess() != null) {
@ -1144,20 +1169,39 @@ public class SignalServiceMessageSender {
sentMessage.setDestinationUuid(recipient.get().getServiceId().toString()); sentMessage.setDestinationUuid(recipient.get().getServiceId().toString());
} }
if (dataMessage.getExpireTimer() > 0) { if (dataMessage != null) {
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis()); sentMessage.setMessage(dataMessage);
if (dataMessage.getExpireTimer() > 0) {
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
}
if (dataMessage.getIsViewOnce()) {
dataMessage = dataMessage.toBuilder().clearAttachments().build();
sentMessage.setMessage(dataMessage);
}
} }
if (dataMessage.getIsViewOnce()) { if (storyMessage != null) {
dataMessage = dataMessage.toBuilder().clearAttachments().build(); sentMessage.setStoryMessage(storyMessage);
sentMessage.setMessage(dataMessage);
} }
sentMessage.addAllStoryMessageRecipients(storyMessageRecipients.stream()
.map(this::createStoryMessageRecipient)
.collect(Collectors.toSet()));
sentMessage.setIsRecipientUpdate(isRecipientUpdate); sentMessage.setIsRecipientUpdate(isRecipientUpdate);
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build(); return container.setSyncMessage(syncMessage.setSent(sentMessage)).build();
} }
private SyncMessage.Sent.StoryMessageRecipient createStoryMessageRecipient(SignalServiceStoryMessageRecipient storyMessageRecipient) {
return SyncMessage.Sent.StoryMessageRecipient.newBuilder()
.addAllDistributionListIds(storyMessageRecipient.getDistributionListIds())
.setDestinationUuid(storyMessageRecipient.getSignalServiceAddress().getIdentifier())
.setIsAllowedToReply(storyMessageRecipient.isAllowedToReply())
.build();
}
private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) { private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) {
Content.Builder container = Content.newBuilder(); Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder();
@ -1580,13 +1624,28 @@ public class SignalServiceMessageSender {
return results; return results;
} }
private SignalServiceSyncMessage createSelfSendSyncMessageForStory(SignalServiceStoryMessage message, long sentTimestamp, Set<SignalServiceStoryMessageRecipient> manifest) {
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
sentTimestamp,
Optional.empty(),
0,
Collections.singletonMap(localAddress, false),
false,
Optional.of(message),
manifest);
return SignalServiceSyncMessage.forSentTranscript(transcript);
}
private SignalServiceSyncMessage createSelfSendSyncMessage(SignalServiceDataMessage message) { private SignalServiceSyncMessage createSelfSendSyncMessage(SignalServiceDataMessage message) {
SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress), SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(localAddress),
message.getTimestamp(), message.getTimestamp(),
message, Optional.of(message),
message.getExpiresInSeconds(), message.getExpiresInSeconds(),
Collections.singletonMap(localAddress, false), Collections.singletonMap(localAddress, false),
false); false,
Optional.empty(),
Collections.emptySet());
return SignalServiceSyncMessage.forSentTranscript(transcript); return SignalServiceSyncMessage.forSentTranscript(transcript);
} }

View file

@ -58,11 +58,14 @@ import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadata
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public final class SignalServiceContent { public final class SignalServiceContent {
@ -674,14 +677,21 @@ public final class SignalServiceContent {
throws ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException throws ProtocolInvalidKeyException, UnsupportedDataMessageException, InvalidMessageStructureException
{ {
if (content.hasSent()) { if (content.hasSent()) {
Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>(); Map<SignalServiceAddress, Boolean> unidentifiedStatuses = new HashMap<>();
SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent(); SignalServiceProtos.SyncMessage.Sent sentContent = content.getSent();
SignalServiceDataMessage dataMessage = createSignalServiceMessage(metadata, sentContent.getMessage()); Optional<SignalServiceDataMessage> dataMessage = sentContent.hasMessage() ? Optional.of(createSignalServiceMessage(metadata, sentContent.getMessage())) : Optional.empty();
Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid()) Optional<SignalServiceStoryMessage> storyMessage = sentContent.hasStoryMessage() ? Optional.of(createStoryMessage(sentContent.getStoryMessage())) : Optional.empty();
? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid()))) Optional<SignalServiceAddress> address = SignalServiceAddress.isValidAddress(sentContent.getDestinationUuid())
: Optional.empty(); ? Optional.of(new SignalServiceAddress(ServiceId.parseOrThrow(sentContent.getDestinationUuid())))
: Optional.empty();
Set<SignalServiceStoryMessageRecipient> recipientManifest = sentContent.getStoryMessageRecipientsList()
.stream()
.map(SignalServiceContent::createSignalServiceStoryMessageRecipient)
.collect(Collectors.toSet());
if (!address.isPresent() && !dataMessage.getGroupContext().isPresent()) { if (!address.isPresent() &&
!dataMessage.flatMap(SignalServiceDataMessage::getGroupContext).isPresent() &&
!storyMessage.flatMap(SignalServiceStoryMessage::getGroupContext).isPresent()) {
throw new InvalidMessageStructureException("SyncMessage missing both destination and group ID!"); throw new InvalidMessageStructureException("SyncMessage missing both destination and group ID!");
} }
@ -699,7 +709,9 @@ public final class SignalServiceContent {
dataMessage, dataMessage,
sentContent.getExpirationStartTimestamp(), sentContent.getExpirationStartTimestamp(),
unidentifiedStatuses, unidentifiedStatuses,
sentContent.getIsRecipientUpdate())); sentContent.getIsRecipientUpdate(),
storyMessage,
recipientManifest));
} }
if (content.hasRequest()) { if (content.hasRequest()) {
@ -913,6 +925,14 @@ public final class SignalServiceContent {
return SignalServiceSyncMessage.empty(); return SignalServiceSyncMessage.empty();
} }
private static SignalServiceStoryMessageRecipient createSignalServiceStoryMessageRecipient(SignalServiceProtos.SyncMessage.Sent.StoryMessageRecipient storyMessageRecipient) {
return new SignalServiceStoryMessageRecipient(
new SignalServiceAddress(ServiceId.parseOrThrow(storyMessageRecipient.getDestinationUuid())),
storyMessageRecipient.getDistributionListIdsList(),
storyMessageRecipient.getIsAllowedToReply()
);
}
private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) { private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) {
boolean isMultiRing = content.getMultiRing(); boolean isMultiRing = content.getMultiRing();
Integer destinationDeviceId = content.hasDestinationDeviceId() ? content.getDestinationDeviceId() : null; Integer destinationDeviceId = content.hasDestinationDeviceId() ? content.getDestinationDeviceId() : null;

View file

@ -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;
}
}

View file

@ -8,6 +8,8 @@ package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -18,21 +20,25 @@ import java.util.Set;
public class SentTranscriptMessage { public class SentTranscriptMessage {
private final Optional<SignalServiceAddress> destination; private final Optional<SignalServiceAddress> destination;
private final long timestamp; private final long timestamp;
private final long expirationStartTimestamp; private final long expirationStartTimestamp;
private final SignalServiceDataMessage message; private final Optional<SignalServiceDataMessage> message;
private final Map<String, Boolean> unidentifiedStatusBySid; private final Map<String, Boolean> unidentifiedStatusBySid;
private final Map<String, Boolean> unidentifiedStatusByE164; private final Map<String, Boolean> unidentifiedStatusByE164;
private final Set<SignalServiceAddress> recipients; private final Set<SignalServiceAddress> recipients;
private final boolean isRecipientUpdate; private final boolean isRecipientUpdate;
private final Optional<SignalServiceStoryMessage> storyMessage;
private final Set<SignalServiceStoryMessageRecipient> storyMessageRecipients;
public SentTranscriptMessage(Optional<SignalServiceAddress> destination, public SentTranscriptMessage(Optional<SignalServiceAddress> destination,
long timestamp, long timestamp,
SignalServiceDataMessage message, Optional<SignalServiceDataMessage> message,
long expirationStartTimestamp, long expirationStartTimestamp,
Map<SignalServiceAddress, Boolean> unidentifiedStatus, Map<SignalServiceAddress, Boolean> unidentifiedStatus,
boolean isRecipientUpdate) boolean isRecipientUpdate,
Optional<SignalServiceStoryMessage> storyMessage,
Set<SignalServiceStoryMessageRecipient> storyMessageRecipients)
{ {
this.destination = destination; this.destination = destination;
this.timestamp = timestamp; this.timestamp = timestamp;
@ -42,6 +48,8 @@ public class SentTranscriptMessage {
this.unidentifiedStatusByE164 = new HashMap<>(); this.unidentifiedStatusByE164 = new HashMap<>();
this.recipients = unidentifiedStatus.keySet(); this.recipients = unidentifiedStatus.keySet();
this.isRecipientUpdate = isRecipientUpdate; this.isRecipientUpdate = isRecipientUpdate;
this.storyMessage = storyMessage;
this.storyMessageRecipients = storyMessageRecipients;
for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) { for (Map.Entry<SignalServiceAddress, Boolean> entry : unidentifiedStatus.entrySet()) {
unidentifiedStatusBySid.put(entry.getKey().getServiceId().toString(), entry.getValue()); unidentifiedStatusBySid.put(entry.getKey().getServiceId().toString(), entry.getValue());
@ -64,10 +72,18 @@ public class SentTranscriptMessage {
return expirationStartTimestamp; return expirationStartTimestamp;
} }
public SignalServiceDataMessage getMessage() { public Optional<SignalServiceDataMessage> getDataMessage() {
return message; return message;
} }
public Optional<SignalServiceStoryMessage> getStoryMessage() {
return storyMessage;
}
public Set<SignalServiceStoryMessageRecipient> getStoryMessageRecipients() {
return storyMessageRecipients;
}
public boolean isUnidentified(ServiceId serviceId) { public boolean isUnidentified(ServiceId serviceId) {
return isUnidentified(serviceId.toString()); return isUnidentified(serviceId.toString());
} }

View file

@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.push;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
/** /**
@ -40,4 +41,17 @@ public final class DistributionId {
public String toString() { public String toString() {
return uuid.toString(); return uuid.toString();
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final DistributionId that = (DistributionId) o;
return Objects.equals(uuid, that.uuid);
}
@Override
public int hashCode() {
return Objects.hash(uuid);
}
} }

View file

@ -427,6 +427,12 @@ message SyncMessage {
optional bool unidentified = 2; optional bool unidentified = 2;
} }
message StoryMessageRecipient {
optional string destinationUuid = 1;
repeated string distributionListIds = 2;
optional bool isAllowedToReply = 3;
}
reserved /*destinationE164*/ 1; reserved /*destinationE164*/ 1;
optional string destinationUuid = 7; optional string destinationUuid = 7;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
@ -434,6 +440,8 @@ message SyncMessage {
optional uint64 expirationStartTimestamp = 4; optional uint64 expirationStartTimestamp = 4;
repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5; repeated UnidentifiedDeliveryStatus unidentifiedStatus = 5;
optional bool isRecipientUpdate = 6 [default = false]; optional bool isRecipientUpdate = 6 [default = false];
optional StoryMessage storyMessage = 8;
repeated StoryMessageRecipient storyMessageRecipients = 9;
} }
message Contacts { message Contacts {