Add story distribution list deduplication handling.

This commit is contained in:
Cody Henthorne
2022-03-28 19:43:42 -04:00
committed by GitHub
parent ba394e1021
commit 2f5cb5f090
18 changed files with 565 additions and 57 deletions

View File

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

View File

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

View File

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

View File

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

View File

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