mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 10:20:25 +01:00
Add story distribution list deduplication handling.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 doesn’t 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user