Add story distribution list deduplication handling.

fork-5.53.8
Cody Henthorne 2022-03-28 19:43:42 -04:00 zatwierdzone przez GitHub
rodzic ba394e1021
commit 2f5cb5f090
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
18 zmienionych plików z 565 dodań i 57 usunięć

Wyświetl plik

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.database
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Helper methods for inserting an MMS message into the MMS table.
*/
object MmsHelper {
fun insert(
recipient: Recipient = Recipient.UNKNOWN,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
threadId: Long = 1,
storyType: StoryType = StoryType.NONE
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
null,
false,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet()
)
return insert(
message = message,
threadId = threadId,
)
}
fun insert(
message: OutgoingMediaMessage,
threadId: Long
): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
}
}

Wyświetl plik

@ -0,0 +1,192 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest {
private lateinit var recipients1to10: List<RecipientId>
private lateinit var recipients11to20: List<RecipientId>
private lateinit var recipients6to15: List<RecipientId>
private lateinit var recipients6to10: List<RecipientId>
private var messageId1: Long = 0
private var messageId2: Long = 0
private var messageId3: Long = 0
private lateinit var storySends: StorySendsDatabase
@Before
fun setup() {
storySends = SignalDatabase.storySends
messageId1 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
messageId2 = MmsHelper.insert(storyType = StoryType.STORY_WITH_REPLIES)
messageId3 = MmsHelper.insert(storyType = StoryType.STORY_WITHOUT_REPLIES)
recipients1to10 = makeRecipients(10)
recipients11to20 = makeRecipients(10)
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
recipients6to10 = recipients1to10.takeLast(5)
}
@Test
fun getRecipientsToSendTo_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false)
storySends.insert(messageId2, recipients11to20, 200, true)
storySends.insert(messageId3, recipients1to10, 300, false)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlap() {
storySends.insert(messageId1, recipients1to10, 100, false)
storySends.insert(messageId2, recipients6to15, 100, true)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlapAll() {
val recipient1 = recipients1to10.first()
val recipient2 = recipients11to20.first()
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false)
storySends.insert(messageId2, listOf(recipient1), 100, true)
storySends.insert(messageId3, listOf(recipient2), 100, true)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
assertThat(recipientIdsForMessage1, hasSize(0))
assertThat(recipientIdsForMessage2, hasSize(1))
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
assertThat(recipientIdsForMessage3, hasSize(1))
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
}
@Test
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
storySends.insert(messageId1, recipients6to15, 100, true)
storySends.insert(messageId2, recipients1to10, 100, false)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false)
storySends.insert(messageId2, recipients11to20, 200, true)
storySends.insert(messageId3, recipients1to10, 300, false)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false)
storySends.insert(messageId2, recipients6to15, 200, true)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun canReply_storyWithReplies() {
storySends.insert(messageId2, recipients1to10, 200, true)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(true))
}
@Test
fun canReply_storyWithoutReplies() {
storySends.insert(messageId1, recipients1to10, 200, false)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(false))
}
@Test
fun canReply_storyWithAndWithoutRepliesOverlap() {
storySends.insert(messageId1, recipients1to10, 200, false)
storySends.insert(messageId2, recipients6to10, 200, true)
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
assertThat(message1OnlyRecipientCanReply, `is`(false))
assertThat(message2RecipientCanReply, `is`(true))
}
private fun makeRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
}
}
}

Wyświetl plik

@ -133,7 +133,7 @@ public class MmsDatabase extends MessageDatabase {
static final String MESSAGE_RANGES = "ranges";
public static final String VIEW_ONCE = "reveal_duration";
static final String STORY_TYPE = "is_story";
public static final String STORY_TYPE = "is_story";
static final String PARENT_STORY_ID = "parent_story_id";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
@ -930,9 +930,25 @@ public class MmsDatabase extends MessageDatabase {
if (messageUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) {
earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp);
}
return messageUpdates;
}
String columnName = receiptType.getColumnName();
for (MessageId storyMessageId : SignalDatabase.storySends().getStoryMessagesFor(messageId)) {
database.execSQL("UPDATE " + TABLE_NAME + " SET " +
columnName + " = " + columnName + " + 1, " +
RECEIPT_TIMESTAMP + " = CASE " +
"WHEN " + columnName + " = 0 THEN MAX(" + RECEIPT_TIMESTAMP + ", ?) " +
"ELSE " + RECEIPT_TIMESTAMP + " " +
"END " +
"WHERE " + ID + " = ?",
SqlUtil.buildArgs(timestamp, storyMessageId.getId()));
SignalDatabase.groupReceipts().update(messageId.getRecipientId(), storyMessageId.getId(), receiptType.getGroupStatus(), timestamp);
messageUpdates.add(new MessageUpdate(-1, storyMessageId));
}
return messageUpdates;
}
@Override
@ -1879,6 +1895,15 @@ public class MmsDatabase extends MessageDatabase {
receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis());
for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) {
receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
}
} else if (message.getRecipient().isDistributionList()) {
GroupReceiptDatabase receiptDatabase = SignalDatabase.groupReceipts();
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(message.getRecipient().requireDistributionListId());
receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis());
for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) {
receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
}

Wyświetl plik

@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notification
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.storySends
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.RecipientRecord
@ -2707,6 +2708,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
// DistributionLists
distributionLists.remapRecipient(byE164, byAci)
// Story Sends
storySends.remapRecipient(byE164, byAci)
// Recipient
Log.w(TAG, "Deleting recipient $byE164", true)
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))

Wyświetl plik

@ -70,6 +70,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.enableWriteAheadLogging()
@ -103,6 +104,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
db.execSQL(GroupCallRingDatabase.CREATE_TABLE)
db.execSQL(ReactionDatabase.CREATE_TABLE)
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
db.execSQL(StorySendsDatabase.CREATE_TABLE)
executeStatements(db, SearchDatabase.CREATE_TABLE)
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
@ -125,6 +127,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES)
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
executeStatements(db, DonationReceiptDatabase.CREATE_INDEXS)
db.execSQL(StorySendsDatabase.CREATE_INDEX)
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS)
executeStatements(db, ReactionDatabase.CREATE_TRIGGERS)
@ -480,5 +483,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("donationReceipts")
val donationReceipts: DonationReceiptDatabase
get() = instance!!.donationReceiptDatabase
@get:JvmStatic
@get:JvmName("storySends")
val storySends: StorySendsDatabase
get() = instance!!.storySendsDatabase
}
}

Wyświetl plik

@ -0,0 +1,180 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Sending to a distribution list is a bit trickier. When we send to multiple distribution lists with overlapping membership, we want to
* show them as distinct items on the sending side, but as a single item on the receiving side. Basically, if Alice has two lists and Bob
* is on both, Bob should always see a story for Alice and not know that Alice has him in multiple lists. And when Bob views the story,
* Alice should update the UI to show a view in each list. To do this, we need to:
* 1. Only send a single copy of each story to a given recipient, while
* 2. Knowing which people would have gotten duplicate copies.
*/
class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
companion object {
const val TABLE_NAME = "story_sends"
const val ID = "_id"
const val MESSAGE_ID = "message_id"
const val RECIPIENT_ID = "recipient_id"
const val SENT_TIMESTAMP = "sent_timestamp"
const val ALLOWS_REPLIES = "allows_replies"
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsDatabase.TABLE_NAME} (${MmsDatabase.ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
$SENT_TIMESTAMP INTEGER NOT NULL,
$ALLOWS_REPLIES INTEGER NOT NULL
)
""".trimIndent()
val CREATE_INDEX = """
CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON $TABLE_NAME ($RECIPIENT_ID, $SENT_TIMESTAMP, $ALLOWS_REPLIES)
""".trimIndent()
}
fun insert(messageId: Long, recipientIds: Collection<RecipientId>, sentTimestamp: Long, allowsReplies: Boolean) {
val db = writableDatabase
db.beginTransaction()
try {
val insertValues: List<ContentValues> = recipientIds.map { id ->
contentValuesOf(
MESSAGE_ID to messageId,
RECIPIENT_ID to id.serialize(),
SENT_TIMESTAMP to sentTimestamp,
ALLOWS_REPLIES to allowsReplies.toInt()
)
}
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(MESSAGE_ID, RECIPIENT_ID, SENT_TIMESTAMP, ALLOWS_REPLIES), insertValues)
.forEach { query -> db.execSQL(query.where, query.whereArgs) }
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun getRecipientsToSendTo(messageId: Long, sentTimestamp: Long, allowsReplies: Boolean): List<RecipientId> {
val recipientIds = mutableListOf<RecipientId>()
val query = """
SELECT DISTINCT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE
$MESSAGE_ID = $messageId
AND $RECIPIENT_ID NOT IN (
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE
$SENT_TIMESTAMP = $sentTimestamp
AND $MESSAGE_ID < $messageId
AND $ALLOWS_REPLIES >= ${allowsReplies.toInt()}
)
AND $RECIPIENT_ID NOT IN (
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE
$SENT_TIMESTAMP = $sentTimestamp
AND $MESSAGE_ID > $messageId
AND $ALLOWS_REPLIES > ${allowsReplies.toInt()}
)
""".trimIndent()
readableDatabase.rawQuery(query, null).use { cursor ->
while (cursor.moveToNext()) {
recipientIds += RecipientId.from(cursor.requireLong(RECIPIENT_ID))
}
}
return recipientIds
}
/**
* The weirdness with remote deletes and stories is that just because you remote-delete a story to List A doesnt mean you
* send the delete to everyone on the list some people have it through multiple lists.
*
* The general idea is to find all recipients for a story that still have a non-deleted copy of it.
*/
fun getRemoteDeleteRecipients(messageId: Long, sentTimestamp: Long): List<RecipientId> {
val recipientIds = mutableListOf<RecipientId>()
val query = """
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE
$MESSAGE_ID = $messageId
AND $RECIPIENT_ID NOT IN (
SELECT $RECIPIENT_ID
FROM $TABLE_NAME
WHERE $MESSAGE_ID != $messageId
AND $SENT_TIMESTAMP = $sentTimestamp
AND $MESSAGE_ID IN (
SELECT ${MmsDatabase.ID}
FROM ${MmsDatabase.TABLE_NAME}
WHERE ${MmsDatabase.REMOTE_DELETED} = 0
)
)
""".trimIndent()
readableDatabase.rawQuery(query, null).use { cursor ->
while (cursor.moveToNext()) {
recipientIds += RecipientId.from(cursor.requireLong(RECIPIENT_ID))
}
}
return recipientIds
}
fun canReply(recipientId: RecipientId, sentTimestamp: Long): Boolean {
readableDatabase.query(
TABLE_NAME,
arrayOf("1"),
"$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ? AND $ALLOWS_REPLIES = ?",
SqlUtil.buildArgs(recipientId, sentTimestamp, 1),
null,
null,
null
).use {
return it.moveToFirst()
}
}
fun getStoryMessagesFor(syncMessageId: MessageDatabase.SyncMessageId): Set<MessageId> {
val messageIds = mutableSetOf<MessageId>()
readableDatabase.query(
TABLE_NAME,
arrayOf(MESSAGE_ID),
"$RECIPIENT_ID = ? AND $SENT_TIMESTAMP = ?",
SqlUtil.buildArgs(syncMessageId.recipientId, syncMessageId.timetamp),
null,
null,
null
).use { cursor ->
while (cursor.moveToNext()) {
messageIds += MessageId(cursor.requireLong(MESSAGE_ID), true)
}
}
return messageIds
}
fun remapRecipient(oldId: RecipientId, newId: RecipientId) {
val query = "$RECIPIENT_ID = ?"
val args = SqlUtil.buildArgs(oldId)
val values = contentValuesOf(RECIPIENT_ID to newId.serialize())
writableDatabase.update(TABLE_NAME, values, query, args)
}
}

Wyświetl plik

@ -195,8 +195,9 @@ object SignalDatabaseMigrations {
private const val ALLOW_STORY_REPLIES = 133
private const val GROUP_STORIES = 134
private const val MMS_COUNT_INDEX = 135
private const val STORY_SENDS = 136
const val DATABASE_VERSION = 135
const val DATABASE_VERSION = 136
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -2486,6 +2487,22 @@ object SignalDatabaseMigrations {
if (oldVersion < MMS_COUNT_INDEX) {
db.execSQL("CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON mms (thread_id, date_received, is_story, parent_story_id)")
}
if (oldVersion < STORY_SENDS) {
db.execSQL(
"""
CREATE TABLE story_sends (
_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
)
""".trimIndent()
)
db.execSQL("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")
}
}
@JvmStatic

Wyświetl plik

@ -145,7 +145,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
List<Recipient> target;
if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).distinct().map(Recipient::resolved).toList();
else target = Stream.of(Stories.getRecipientsToSendTo(listRecipient.requireDistributionListId(), messageId)).distinctBy(Recipient::getId).toList();
else target = Stream.of(Stories.getRecipientsToSendTo(messageId, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies())).distinctBy(Recipient::getId).toList();
List<SendMessageResult> results = deliver(message, target);
Log.i(TAG, JobLogger.format(this, "Finished send."));

Wyświetl plik

@ -34,6 +34,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -72,12 +73,7 @@ public class RemoteDeleteSendJob extends BaseJob {
List<RecipientId> recipients;
if (conversationRecipient.isDistributionList()) {
DistributionListId distributionListId = conversationRecipient.requireDistributionListId();
recipients = Stories.getRecipientsToSendTo(distributionListId, messageId)
.stream()
.map(Recipient::getId)
.collect(Collectors.toList());
recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp());
} else {
recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();

Wyświetl plik

@ -201,6 +201,7 @@ class MediaSelectionRepository(context: Context) {
private fun sendMessages(contacts: List<RecipientSearchKey>, body: String, preUploadResults: Collection<PreUploadResult>, mentions: List<Mention>, isViewOnce: Boolean) {
val broadcastMessages: MutableList<OutgoingSecureMediaMessage> = ArrayList(contacts.size)
val storyMessages: MutableMap<PreUploadResult, MutableList<OutgoingSecureMediaMessage>> = mutableMapOf()
val distributionListSentTimestamps: MutableMap<PreUploadResult, Long> = mutableMapOf()
for (contact in contacts) {
val recipient = Recipient.resolved(contact.recipientId)
@ -220,7 +221,7 @@ class MediaSelectionRepository(context: Context) {
recipient,
body,
emptyList(),
System.currentTimeMillis(),
if (recipient.isDistributionList) distributionListSentTimestamps.getOrPut(preUploadResults.first()) { System.currentTimeMillis() } else System.currentTimeMillis(),
-1,
TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
isViewOnce,
@ -239,7 +240,7 @@ class MediaSelectionRepository(context: Context) {
if (isStory && preUploadResults.size > 1) {
preUploadResults.forEach {
val list = storyMessages[it] ?: mutableListOf()
list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(System.currentTimeMillis()))
list.add(OutgoingSecureMediaMessage(message).withSentTimestamp(if (recipient.isDistributionList) distributionListSentTimestamps.getOrPut(it) { System.currentTimeMillis() } else System.currentTimeMillis()))
storyMessages[it] = list
// XXX We must do this to avoid sending out messages to the same recipient with the same

Wyświetl plik

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.mediasend.v2.text.send
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
@ -44,6 +43,7 @@ class TextStoryPostSendRepository {
private fun performSend(contactSearchKey: Set<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
return Single.fromCallable {
val messages: MutableList<OutgoingSecureMediaMessage> = mutableListOf()
val distributionListSentTimestamp = System.currentTimeMillis()
for (contact in contactSearchKey) {
val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get())
@ -63,7 +63,7 @@ class TextStoryPostSendRepository {
recipient,
serializeTextStoryState(textStoryPostCreationState),
emptyList(),
System.currentTimeMillis(),
if (recipient.isDistributionList) distributionListSentTimestamp else System.currentTimeMillis(),
-1,
0,
false,
@ -83,9 +83,9 @@ class TextStoryPostSendRepository {
ThreadUtil.sleep(5)
}
messages.map { Stories.sendIndividualStory(it) }
Stories.sendTextStories(messages)
}.flatMap { messages ->
Completable.concat(messages).toSingleDefault<TextStoryPostSendResult>(TextStoryPostSendResult.Success)
messages.toSingleDefault<TextStoryPostSendResult>(TextStoryPostSendResult.Success)
}
}

Wyświetl plik

@ -1564,16 +1564,14 @@ public final class MessageContentProcessor {
if (message.getGroupContext().isPresent()) {
parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId());
} else {
} else if (SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) {
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
if (!story.getStoryType().isStoryWithReplies()) {
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
return;
}
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, "", false, story.getSlideDeck().asAttachments(), Collections.emptyList());
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, message.getBody().orElse(""), false, story.getSlideDeck().asAttachments(), Collections.emptyList());
} else {
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
return;
}
} catch (NoSuchMessageException e) {
warn(content.getTimestamp(), "Couldn't find story for reply.", e);

Wyświetl plik

@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -304,14 +305,9 @@ public class MessageSender {
OutgoingSecureMediaMessage message = messages.get(i);
Recipient recipient = message.getRecipient();
if (isLocalSelfSend(context, recipient, false)) {
sendLocalMediaSelf(context, messageId);
} else if (recipient.isPushGroup()) {
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), null, true), messageDependsOnIds, recipient.getId().toQueueKey());
} else if (recipient.isDistributionList()) {
jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true), messageDependsOnIds, recipient.getId().toQueueKey());
} else {
jobManager.add(new PushMediaSendJob(messageId, recipient, true), messageDependsOnIds, recipient.getId().toQueueKey());
if (recipient.isDistributionList()) {
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies());
}
}
@ -319,9 +315,25 @@ public class MessageSender {
mmsDatabase.setTransactionSuccessful();
} catch (MmsException e) {
Log.w(TAG, "Failed to send messages.", e);
return;
} finally {
mmsDatabase.endTransaction();
}
for (int i = 0; i < messageIds.size(); i++) {
long messageId = messageIds.get(i);
Recipient recipient = messages.get(i).getRecipient();
if (isLocalSelfSend(context, recipient, false)) {
sendLocalMediaSelf(context, messageId);
} else if (recipient.isPushGroup()) {
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), null, true), messageDependsOnIds, recipient.getId().toQueueKey());
} else if (recipient.isDistributionList()) {
jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true), messageDependsOnIds, recipient.getId().toQueueKey());
} else {
jobManager.add(new PushMediaSendJob(messageId, recipient, true), messageDependsOnIds, recipient.getId().toQueueKey());
}
}
}
/**

Wyświetl plik

@ -5,13 +5,11 @@ import androidx.fragment.app.FragmentManager
import io.reactivex.rxjava3.core.Completable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.HeaderAction
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
@ -40,29 +38,16 @@ object Stories {
}
@WorkerThread
fun sendIndividualStory(message: OutgoingMediaMessage): Completable {
fun sendTextStories(messages: List<OutgoingSecureMediaMessage>): Completable {
return Completable.create { emitter ->
MessageSender.send(
ApplicationDependencies.getApplication(),
message,
-1L,
false,
null
) {
emitter.onComplete()
}
MessageSender.sendMediaBroadcast(ApplicationDependencies.getApplication(), messages, listOf(), listOf())
emitter.onComplete()
}
}
@JvmStatic
fun getRecipientsToSendTo(distributionListId: DistributionListId, messageId: Long): List<Recipient> {
val destinations: List<GroupReceiptInfo> = SignalDatabase.groupReceipts.getGroupReceiptInfo(messageId)
val recipientIds: List<RecipientId> = if (destinations.isNotEmpty()) {
destinations.map(GroupReceiptInfo::getRecipientId)
} else {
SignalDatabase.distributionLists.getMembers(distributionListId)
}
fun getRecipientsToSendTo(messageId: Long, sentTimestamp: Long, allowsReplies: Boolean): List<Recipient> {
val recipientIds: List<RecipientId> = SignalDatabase.storySends.getRecipientsToSendTo(messageId, sentTimestamp, allowsReplies)
return RecipientUtil.getEligibleForSending(recipientIds.map(Recipient::resolved))
}

Wyświetl plik

@ -88,8 +88,8 @@ object MyStoriesItem {
viewCount.text = context.resources.getQuantityString(
R.plurals.MyStories__d_views,
model.distributionStory.messageRecord.readReceiptCount,
model.distributionStory.messageRecord.readReceiptCount
model.distributionStory.messageRecord.viewedReceiptCount,
model.distributionStory.messageRecord.viewedReceiptCount
)
if (STATUS_CHANGE in payload) {

Wyświetl plik

@ -8,6 +8,7 @@ import org.signal.spinner.Spinner.DatabaseConfig
import org.thoughtcrime.securesms.database.DatabaseMonitor
import org.thoughtcrime.securesms.database.GV2Transformer
import org.thoughtcrime.securesms.database.GV2UpdateTransformer
import org.thoughtcrime.securesms.database.IsStoryTransformer
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
@ -39,7 +40,7 @@ class SpinnerApplicationContext : ApplicationContext() {
linkedMapOf(
"signal" to DatabaseConfig(
db = SignalDatabase.rawDatabase,
columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer)
columnTransformers = listOf(MessageBitmaskColumnTransformer, GV2Transformer, GV2UpdateTransformer, IsStoryTransformer)
),
"jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase),
"keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase),

Wyświetl plik

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.database
import android.database.Cursor
import org.signal.core.util.requireInt
import org.signal.spinner.ColumnTransformer
import org.thoughtcrime.securesms.database.model.StoryType.Companion.fromCode
object IsStoryTransformer : ColumnTransformer {
override fun matches(tableName: String?, columnName: String): Boolean {
return columnName == MmsDatabase.STORY_TYPE && (tableName == null || tableName == MmsDatabase.TABLE_NAME)
}
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
val storyType = fromCode(cursor.requireInt(MmsDatabase.STORY_TYPE))
return "${cursor.requireInt(MmsDatabase.STORY_TYPE)}<br><br>$storyType"
}
}

Wyświetl plik

@ -6,6 +6,7 @@ import com.google.android.mms.pdu_alt.PduHeaders
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Helper methods for inserting an MMS message into the MMS table.
@ -15,6 +16,7 @@ object TestMms {
fun insert(
db: SQLiteDatabase,
recipient: Recipient = Recipient.UNKNOWN,
recipientId: RecipientId = Recipient.UNKNOWN.id,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
receivedTimestampMillis: Long = System.currentTimeMillis(),
@ -51,6 +53,7 @@ object TestMms {
return insert(
db = db,
message = message,
recipientId = recipientId,
body = body,
type = type,
unread = unread,
@ -63,6 +66,7 @@ object TestMms {
fun insert(
db: SQLiteDatabase,
message: OutgoingMediaMessage,
recipientId: RecipientId = message.recipient.id,
body: String = message.body,
type: Long = MmsSmsColumns.Types.BASE_INBOX_TYPE,
unread: Boolean = false,
@ -81,7 +85,7 @@ object TestMms {
put(MmsSmsColumns.SUBSCRIPTION_ID, message.subscriptionId)
put(MmsSmsColumns.EXPIRES_IN, message.expiresIn)
put(MmsDatabase.VIEW_ONCE, message.isViewOnce)
put(MmsSmsColumns.RECIPIENT_ID, message.recipient.id.serialize())
put(MmsSmsColumns.RECIPIENT_ID, recipientId.serialize())
put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
put(MmsSmsColumns.VIEWED_RECEIPT_COUNT, if (viewed) 1 else 0)
@ -94,4 +98,17 @@ object TestMms {
return db.insert(MmsDatabase.TABLE_NAME, null, contentValues)
}
fun markAsRemoteDelete(db: SQLiteDatabase, messageId: Long) {
val values = ContentValues()
values.put(MmsSmsColumns.REMOTE_DELETED, 1)
values.putNull(MmsSmsColumns.BODY)
values.putNull(MmsDatabase.QUOTE_BODY)
values.putNull(MmsDatabase.QUOTE_AUTHOR)
values.putNull(MmsDatabase.QUOTE_ATTACHMENT)
values.putNull(MmsDatabase.QUOTE_ID)
values.putNull(MmsDatabase.LINK_PREVIEWS)
values.putNull(MmsDatabase.SHARED_CONTACTS)
db.update(MmsDatabase.TABLE_NAME, values, Database.ID_WHERE, arrayOf(messageId.toString()))
}
}