Allow for MSL entries to be associated with multiple messages.

This commit is contained in:
Greyson Parrelli 2021-06-29 16:38:24 -04:00 committed by Alex Hart
parent 92e8f9de0e
commit 5372f79c40
6 changed files with 176 additions and 89 deletions

View file

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageLogEntry import org.thoughtcrime.securesms.database.model.MessageLogEntry
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@ -17,41 +18,51 @@ import java.util.UUID
/** /**
* Stores a 24-hr buffer of all outgoing messages. Used for the retry logic required for sender key. * Stores a 24-hr buffer of all outgoing messages. Used for the retry logic required for sender key.
* *
* General note: This class is actually two tables -- one to store the entry, and another to store all the devices that were sent it. * General note: This class is actually three tables:
* - one to store the entry
* - one to store all the devices that were sent it, and
* - one to store the set of related messages.
* *
* The general lifecycle of entries in the store goes something like this: * The general lifecycle of entries in the store goes something like this:
* - Upon sending a message, throw an entry in the 'message table' and throw an entry for each recipient you sent it to in the 'recipient table' * - Upon sending a message, put an entry in the 'payload table', an entry for each recipient you sent it to in the 'recipient table', and an entry for each
* related message in the 'message table'
* - Whenever you get a delivery receipt, delete the entries in the 'recipient table' * - Whenever you get a delivery receipt, delete the entries in the 'recipient table'
* - Whenever there's no more records in the 'recipient table' for a given message, delete the entry in the 'message table' * - Whenever there's no more records in the 'recipient table' for a given message, delete the entry in the 'message table'
* - Whenever you delete a message, delete the entry in the 'message table' * - Whenever you delete a message, delete the relevant entries from the 'payload table'
* - Whenever you read an entry from the table, first trim off all the entries that are too old * - Whenever you read an entry from the table, first trim off all the entries that are too old
* *
* Because of all of this, you can be sure that if an entry is in this store, it's safe to resend to someone upon request * Because of all of this, you can be sure that if an entry is in this store, it's safe to resend to someone upon request
* *
* Worth noting that we use triggers + foreign keys to make sure entries in this table are properly cleaned up. Triggers for when you delete a message, and * Worth noting that we use triggers + foreign keys to make sure entries in this table are properly cleaned up. Triggers for when you delete a message, and
* a cascading delete foreign key between these two tables. * cascading delete foreign keys between these three tables.
*
* Performance considerations:
* - The most common operations by far are:
* - Inserting into the table
* - Deleting a recipient (in response to a delivery receipt)
* - We should also optimize for when we delete messages from the sms/mms tables, since you can delete a bunch at once
* - We *don't* really need to optimize for retrieval, since that happens very infrequently. In particular, we don't want to slow down inserts in order to
* improve retrieval time. That means we shouldn't be adding indexes that optimize for retrieval.
*/ */
class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLCipherOpenHelper?) : Database(context, databaseHelper) { class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLCipherOpenHelper?) : Database(context, databaseHelper) {
companion object { companion object {
@JvmField @JvmField
val CREATE_TABLE: Array<String> = arrayOf(MessageTable.CREATE_TABLE, RecipientTable.CREATE_TABLE) val CREATE_TABLE: Array<String> = arrayOf(PayloadTable.CREATE_TABLE, RecipientTable.CREATE_TABLE, MessageTable.CREATE_TABLE)
@JvmField @JvmField
val CREATE_INDEXES: Array<String> = MessageTable.CREATE_INDEXES + RecipientTable.CREATE_INDEXES val CREATE_INDEXES: Array<String> = PayloadTable.CREATE_INDEXES + RecipientTable.CREATE_INDEXES + MessageTable.CREATE_INDEXES
@JvmField @JvmField
val CREATE_TRIGGERS: Array<String> = MessageTable.CREATE_TRIGGERS val CREATE_TRIGGERS: Array<String> = PayloadTable.CREATE_TRIGGERS
} }
private object MessageTable { private object PayloadTable {
const val TABLE_NAME = "message_send_log" const val TABLE_NAME = "msl_payload"
const val ID = "_id" const val ID = "_id"
const val DATE_SENT = "date_sent" const val DATE_SENT = "date_sent"
const val CONTENT = "content" const val CONTENT = "content"
const val RELATED_MESSAGE_ID = "related_message_id"
const val IS_RELATED_MESSAGE_MMS = "is_related_message_mms"
const val CONTENT_HINT = "content_hint" const val CONTENT_HINT = "content_hint"
const val CREATE_TABLE = """ const val CREATE_TABLE = """
@ -59,54 +70,75 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
$ID INTEGER PRIMARY KEY, $ID INTEGER PRIMARY KEY,
$DATE_SENT INTEGER NOT NULL, $DATE_SENT INTEGER NOT NULL,
$CONTENT BLOB NOT NULL, $CONTENT BLOB NOT NULL,
$RELATED_MESSAGE_ID INTEGER DEFAULT -1,
$IS_RELATED_MESSAGE_MMS INTEGER DEFAULT 0,
$CONTENT_HINT INTEGER NOT NULL $CONTENT_HINT INTEGER NOT NULL
) )
""" """
@JvmField /** Created for [deleteEntriesForRecipient] */
val CREATE_INDEXES = arrayOf( val CREATE_INDEXES = arrayOf(
"CREATE INDEX message_log_date_sent_index ON $TABLE_NAME ($DATE_SENT)", "CREATE INDEX msl_payload_date_sent_index ON $TABLE_NAME ($DATE_SENT)",
"CREATE INDEX message_log_related_message_index ON $TABLE_NAME ($RELATED_MESSAGE_ID, $IS_RELATED_MESSAGE_MMS)"
) )
@JvmField
val CREATE_TRIGGERS = arrayOf( val CREATE_TRIGGERS = arrayOf(
""" """
CREATE TRIGGER msl_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME} CREATE TRIGGER msl_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME}
BEGIN BEGIN
DELETE FROM $TABLE_NAME WHERE $RELATED_MESSAGE_ID = old.${SmsDatabase.ID} AND $IS_RELATED_MESSAGE_MMS = 0; DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = old.${SmsDatabase.ID} AND ${MessageTable.IS_MMS} = 0);
END END
""", """,
""" """
CREATE TRIGGER msl_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME} CREATE TRIGGER msl_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME}
BEGIN BEGIN
DELETE FROM $TABLE_NAME WHERE $RELATED_MESSAGE_ID = old.${MmsDatabase.ID} AND $IS_RELATED_MESSAGE_MMS = 1; DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = old.${MmsDatabase.ID} AND ${MessageTable.IS_MMS} = 1);
END END
""" """
) )
} }
private object RecipientTable { private object RecipientTable {
const val TABLE_NAME = "message_send_log_recipients" const val TABLE_NAME = "msl_recipient"
const val ID = "_id" const val ID = "_id"
const val MESSAGE_LOG_ID = "message_send_log_id" const val PAYLOAD_ID = "payload_id"
const val RECIPIENT_ID = "recipient_id" const val RECIPIENT_ID = "recipient_id"
const val DEVICE = "device" const val DEVICE = "device"
const val CREATE_TABLE = """ const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY, $ID INTEGER PRIMARY KEY,
$MESSAGE_LOG_ID INTEGER NOT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE CASCADE, $PAYLOAD_ID INTEGER NOT NULL REFERENCES ${PayloadTable.TABLE_NAME} (${PayloadTable.ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL, $RECIPIENT_ID INTEGER NOT NULL,
$DEVICE INTEGER NOT NULL $DEVICE INTEGER NOT NULL
) )
""" """
/** Created for [deleteEntriesForRecipient] */
val CREATE_INDEXES = arrayOf( val CREATE_INDEXES = arrayOf(
"CREATE INDEX message_send_log_recipients_recipient_index ON $TABLE_NAME ($RECIPIENT_ID, $DEVICE)" "CREATE INDEX msl_recipient_recipient_index ON $TABLE_NAME ($RECIPIENT_ID, $DEVICE, $PAYLOAD_ID)",
"CREATE INDEX msl_recipient_payload_index ON $TABLE_NAME ($PAYLOAD_ID)"
)
}
private object MessageTable {
const val TABLE_NAME = "msl_message"
const val ID = "_id"
const val PAYLOAD_ID = "payload_id"
const val MESSAGE_ID = "message_id"
const val IS_MMS = "is_mms"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$PAYLOAD_ID INTEGER NOT NULL REFERENCES ${PayloadTable.TABLE_NAME} (${PayloadTable.ID}) ON DELETE CASCADE,
$MESSAGE_ID INTEGER NOT NULL,
$IS_MMS INTEGER NOT NULL
)
"""
/** Created for [PayloadTable.CREATE_TRIGGERS] and [deleteAllRelatedToMessage] */
val CREATE_INDEXES = arrayOf(
"CREATE INDEX msl_message_message_index ON $TABLE_NAME ($MESSAGE_ID, $IS_MMS, $PAYLOAD_ID)"
) )
} }
@ -115,7 +147,7 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) {
val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices))
insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, relatedMessageId, isRelatedMessageMms) insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, listOf(MessageId(relatedMessageId, isRelatedMessageMms)))
} }
} }
@ -140,35 +172,44 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
val content: SignalServiceProtos.Content = results.first { it.isSuccess && it.success.content.isPresent }.success.content.get() val content: SignalServiceProtos.Content = results.first { it.isSuccess && it.success.content.isPresent }.success.content.get()
insert(recipientDevices, sentTimestamp, content, contentHint, relatedMessageId, isRelatedMessageMms) insert(recipientDevices, sentTimestamp, content, contentHint, listOf(MessageId(relatedMessageId, isRelatedMessageMms)))
} }
private fun insert(recipients: List<RecipientDevice>, dateSent: Long, content: SignalServiceProtos.Content, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) { private fun insert(recipients: List<RecipientDevice>, dateSent: Long, content: SignalServiceProtos.Content, contentHint: ContentHint, messageIds: List<MessageId>) {
val db = databaseHelper.writableDatabase val db = databaseHelper.writableDatabase
db.beginTransaction() db.beginTransaction()
try { try {
val logValues = ContentValues().apply { val payloadValues = ContentValues().apply {
put(MessageTable.DATE_SENT, dateSent) put(PayloadTable.DATE_SENT, dateSent)
put(MessageTable.CONTENT, content.toByteArray()) put(PayloadTable.CONTENT, content.toByteArray())
put(MessageTable.CONTENT_HINT, contentHint.type) put(PayloadTable.CONTENT_HINT, contentHint.type)
put(MessageTable.RELATED_MESSAGE_ID, relatedMessageId)
put(MessageTable.IS_RELATED_MESSAGE_MMS, if (isRelatedMessageMms) 1 else 0)
} }
val messageLogId: Long = db.insert(MessageTable.TABLE_NAME, null, logValues) val payloadId: Long = db.insert(PayloadTable.TABLE_NAME, null, payloadValues)
recipients.forEach { recipientDevice -> recipients.forEach { recipientDevice ->
recipientDevice.devices.forEach { device -> recipientDevice.devices.forEach { device ->
val recipientValues = ContentValues() val recipientValues = ContentValues().apply {
recipientValues.put(RecipientTable.MESSAGE_LOG_ID, messageLogId) put(RecipientTable.PAYLOAD_ID, payloadId)
recipientValues.put(RecipientTable.RECIPIENT_ID, recipientDevice.recipientId.serialize()) put(RecipientTable.RECIPIENT_ID, recipientDevice.recipientId.serialize())
recipientValues.put(RecipientTable.DEVICE, device) put(RecipientTable.DEVICE, device)
}
db.insert(RecipientTable.TABLE_NAME, null, recipientValues) db.insert(RecipientTable.TABLE_NAME, null, recipientValues)
} }
} }
messageIds.forEach { messageId ->
val messageValues = ContentValues().apply {
put(MessageTable.PAYLOAD_ID, payloadId)
put(MessageTable.MESSAGE_ID, messageId.id)
put(MessageTable.IS_MMS, if (messageId.mms) 1 else 0)
}
db.insert(MessageTable.TABLE_NAME, null, messageValues)
}
db.setTransactionSuccessful() db.setTransactionSuccessful()
} finally { } finally {
db.endTransaction() db.endTransaction()
@ -181,20 +222,34 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()) trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())
val db = databaseHelper.readableDatabase val db = databaseHelper.readableDatabase
val table = "${MessageTable.TABLE_NAME} LEFT JOIN ${RecipientTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_LOG_ID}" val table = "${PayloadTable.TABLE_NAME} LEFT JOIN ${RecipientTable.TABLE_NAME} ON ${PayloadTable.TABLE_NAME}.${PayloadTable.ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.PAYLOAD_ID}"
val query = "${MessageTable.DATE_SENT} = ? AND ${RecipientTable.RECIPIENT_ID} = ? AND ${RecipientTable.DEVICE} = ?" val query = "${PayloadTable.DATE_SENT} = ? AND ${RecipientTable.RECIPIENT_ID} = ? AND ${RecipientTable.DEVICE} = ?"
val args = SqlUtil.buildArgs(dateSent, recipientId, device) val args = SqlUtil.buildArgs(dateSent, recipientId, device)
db.query(table, null, query, args, null, null, null).use { cursor -> db.query(table, null, query, args, null, null, null).use { entryCursor ->
if (cursor.moveToFirst()) { if (entryCursor.moveToFirst()) {
return MessageLogEntry( val payloadId = CursorUtil.requireLong(entryCursor, RecipientTable.PAYLOAD_ID)
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RecipientTable.RECIPIENT_ID)),
dateSent = CursorUtil.requireLong(cursor, MessageTable.DATE_SENT), db.query(MessageTable.TABLE_NAME, null, "${MessageTable.PAYLOAD_ID} = ?", SqlUtil.buildArgs(payloadId), null, null, null).use { messageCursor ->
content = SignalServiceProtos.Content.parseFrom(CursorUtil.requireBlob(cursor, MessageTable.CONTENT)), val messageIds: MutableList<MessageId> = mutableListOf()
contentHint = ContentHint.fromType(CursorUtil.requireInt(cursor, MessageTable.CONTENT_HINT)),
relatedMessageId = CursorUtil.requireLong(cursor, MessageTable.RELATED_MESSAGE_ID), while (messageCursor.moveToNext()) {
isRelatedMessageMms = CursorUtil.requireBoolean(cursor, MessageTable.IS_RELATED_MESSAGE_MMS) messageIds.add(
) MessageId(
id = CursorUtil.requireLong(messageCursor, MessageTable.MESSAGE_ID),
mms = CursorUtil.requireBoolean(messageCursor, MessageTable.IS_MMS)
)
)
}
return MessageLogEntry(
recipientId = RecipientId.from(CursorUtil.requireLong(entryCursor, RecipientTable.RECIPIENT_ID)),
dateSent = CursorUtil.requireLong(entryCursor, PayloadTable.DATE_SENT),
content = SignalServiceProtos.Content.parseFrom(CursorUtil.requireBlob(entryCursor, PayloadTable.CONTENT)),
contentHint = ContentHint.fromType(CursorUtil.requireInt(entryCursor, PayloadTable.CONTENT_HINT)),
relatedMessages = messageIds
)
}
} }
} }
@ -205,10 +260,10 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
if (!FeatureFlags.senderKey()) return if (!FeatureFlags.senderKey()) return
val db = databaseHelper.writableDatabase val db = databaseHelper.writableDatabase
val query = "${MessageTable.RELATED_MESSAGE_ID} = ? AND ${MessageTable.IS_RELATED_MESSAGE_MMS} = ?" val query = "${PayloadTable.ID} IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = ? AND ${MessageTable.IS_MMS} = ?)"
val args = SqlUtil.buildArgs(messageId, if (mms) 1 else 0) val args = SqlUtil.buildArgs(messageId, if (mms) 1 else 0)
db.delete(MessageTable.TABLE_NAME, query, args) db.delete(PayloadTable.TABLE_NAME, query, args)
} }
fun deleteEntryForRecipient(dateSent: Long, recipientId: RecipientId, device: Int) { fun deleteEntryForRecipient(dateSent: Long, recipientId: RecipientId, device: Int) {
@ -227,17 +282,17 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
val query = """ val query = """
${RecipientTable.RECIPIENT_ID} = ? AND ${RecipientTable.RECIPIENT_ID} = ? AND
${RecipientTable.DEVICE} = ? AND ${RecipientTable.DEVICE} = ? AND
${RecipientTable.MESSAGE_LOG_ID} IN ( ${RecipientTable.PAYLOAD_ID} IN (
SELECT ${MessageTable.ID} SELECT ${PayloadTable.ID}
FROM ${MessageTable.TABLE_NAME} FROM ${PayloadTable.TABLE_NAME}
WHERE ${MessageTable.DATE_SENT} IN (${dateSent.joinToString(",")}) WHERE ${PayloadTable.DATE_SENT} IN (${dateSent.joinToString(",")})
)""" )"""
val args = SqlUtil.buildArgs(recipientId, device) val args = SqlUtil.buildArgs(recipientId, device)
db.delete(RecipientTable.TABLE_NAME, query, args) db.delete(RecipientTable.TABLE_NAME, query, args)
val cleanQuery = "${MessageTable.ID} NOT IN (SELECT ${RecipientTable.MESSAGE_LOG_ID} FROM ${RecipientTable.TABLE_NAME})" val cleanQuery = "${PayloadTable.ID} NOT IN (SELECT ${RecipientTable.PAYLOAD_ID} FROM ${RecipientTable.TABLE_NAME})"
db.delete(MessageTable.TABLE_NAME, cleanQuery, null) db.delete(PayloadTable.TABLE_NAME, cleanQuery, null)
db.setTransactionSuccessful() db.setTransactionSuccessful()
} finally { } finally {
@ -248,17 +303,17 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC
fun deleteAll() { fun deleteAll() {
if (!FeatureFlags.senderKey()) return if (!FeatureFlags.senderKey()) return
databaseHelper.writableDatabase.delete(MessageTable.TABLE_NAME, null, null) databaseHelper.writableDatabase.delete(PayloadTable.TABLE_NAME, null, null)
} }
fun trimOldMessages(currentTime: Long, maxAge: Long) { fun trimOldMessages(currentTime: Long, maxAge: Long) {
if (!FeatureFlags.senderKey()) return if (!FeatureFlags.senderKey()) return
val db = databaseHelper.writableDatabase val db = databaseHelper.writableDatabase
val query = "${MessageTable.DATE_SENT} < ?" val query = "${PayloadTable.DATE_SENT} < ?"
val args = SqlUtil.buildArgs(currentTime - maxAge) val args = SqlUtil.buildArgs(currentTime - maxAge)
db.delete(MessageTable.TABLE_NAME, query, args) db.delete(PayloadTable.TABLE_NAME, query, args)
} }
private data class RecipientDevice(val recipientId: RecipientId, val devices: List<Int>) private data class RecipientDevice(val recipientId: RecipientId, val devices: List<Int>)

View file

@ -201,8 +201,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int SENDER_KEY = 103; private static final int SENDER_KEY = 103;
private static final int MESSAGE_DUPE_INDEX = 104; private static final int MESSAGE_DUPE_INDEX = 104;
private static final int MESSAGE_LOG = 105; private static final int MESSAGE_LOG = 105;
private static final int MESSAGE_LOG_2 = 106;
private static final int DATABASE_VERSION = 105; private static final int DATABASE_VERSION = 106;
private static final String DATABASE_NAME = "signal.db"; private static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -1598,6 +1599,41 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL("CREATE INDEX message_send_log_recipients_recipient_index ON message_send_log_recipients (recipient_id, device)"); db.execSQL("CREATE INDEX message_send_log_recipients_recipient_index ON message_send_log_recipients (recipient_id, device)");
} }
if (oldVersion < MESSAGE_LOG_2) {
db.execSQL("DROP TABLE message_send_log");
db.execSQL("DROP INDEX IF EXISTS message_log_date_sent_index");
db.execSQL("DROP INDEX IF EXISTS message_log_related_message_index");
db.execSQL("DROP TRIGGER msl_sms_delete");
db.execSQL("DROP TRIGGER msl_mms_delete");
db.execSQL("DROP TABLE message_send_log_recipients");
db.execSQL("DROP INDEX IF EXISTS message_send_log_recipients_recipient_index");
db.execSQL("CREATE TABLE msl_payload (_id INTEGER PRIMARY KEY, " +
"date_sent INTEGER NOT NULL, " +
"content BLOB NOT NULL, " +
"content_hint INTEGER NOT NULL)");
db.execSQL("CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)");
db.execSQL("CREATE TABLE msl_recipient (_id INTEGER PRIMARY KEY, " +
"payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE, " +
"recipient_id INTEGER NOT NULL, " +
"device INTEGER NOT NULL)");
db.execSQL("CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)");
db.execSQL("CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)");
db.execSQL("CREATE TABLE msl_message (_id INTEGER PRIMARY KEY, " +
"payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE, " +
"message_id INTEGER NOT NULL, " +
"is_mms INTEGER NOT NULL)");
db.execSQL("CREATE INDEX msl_message_message_index ON msl_message (message_id, is_mms, payload_id)");
db.execSQL("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END");
db.execSQL("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View file

@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.database.model
/**
* Represents a pair of values that can be used to find a message. Because we have two tables,
* that means this has both the primary key and a boolean indicating which table it's in.
*/
data class MessageId(
val id: Long,
@get:JvmName("isMms") val mms: Boolean
)

View file

@ -12,10 +12,9 @@ data class MessageLogEntry(
val dateSent: Long, val dateSent: Long,
val content: SignalServiceProtos.Content, val content: SignalServiceProtos.Content,
val contentHint: ContentHint, val contentHint: ContentHint,
val relatedMessageId: Long, val relatedMessages: List<MessageId>
val isRelatedMessageMms: Boolean,
) { ) {
val hasRelatedMessage: Boolean val hasRelatedMessage: Boolean
@JvmName("hasRelatedMessage") @JvmName("hasRelatedMessage")
get() = relatedMessageId > 0 get() = relatedMessages.isNotEmpty()
} }

View file

@ -52,6 +52,7 @@ public final class SenderKeyDistributionSendJob extends BaseJob {
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED) .setMaxAttempts(Parameters.UNLIMITED)
.setMaxInstancesForQueue(1)
.build()); .build());
} }

View file

@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
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.Mention; import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageLogEntry; import org.thoughtcrime.securesms.database.model.MessageLogEntry;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
@ -1795,9 +1796,10 @@ public final class MessageContentProcessor {
return; return;
} }
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(threadRecipient.requireGroupId().requireV2()); GroupId.V2 groupId = threadRecipient.requireGroupId().requireV2();
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId);
SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId()); SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId());
DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress)); DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress));
if (messageLogEntry != null) { if (messageLogEntry != null) {
@ -1807,29 +1809,11 @@ public final class MessageContentProcessor {
messageLogEntry.getDateSent(), messageLogEntry.getDateSent(),
messageLogEntry.getContent(), messageLogEntry.getContent(),
messageLogEntry.getContentHint(), messageLogEntry.getContentHint(),
threadRecipient.requireGroupId().requireV2(), groupId,
distributionId)); distributionId));
} else { } else {
warn(content.getTimestamp(), "[RetryReceipt-SK] Unable to find MSL entry for " + requester.getId() + " with timestamp " + sentTimestamp + "."); warn(content.getTimestamp(), "[RetryReceipt-SK] Unable to find MSL entry for " + requester.getId() + " with timestamp " + sentTimestamp + ".");
if (!content.getGroupId().isPresent()) {
warn(content.getTimestamp(), "[RetryReceipt-SK] No groupId on the Content, so we cannot send them a SenderKeyDistributionMessage.");
return;
}
GroupId groupId;
try {
groupId = GroupId.push(content.getGroupId().get());
} catch (BadGroupIdException e) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt-SK] Bad groupId!");
return;
}
if (!groupId.isV2()) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt-SK] Not a valid GV2 ID!");
return;
}
Optional<GroupRecord> groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId); Optional<GroupRecord> groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
if (!groupRecord.isPresent()) { if (!groupRecord.isPresent()) {
@ -1875,12 +1859,14 @@ public final class MessageContentProcessor {
} }
} }
private static @Nullable MessageRecord findRetryReceiptRelatedMessage(@NonNull Context context, @Nullable MessageLogEntry messageLogEntry, long sentTimestamp) { private @Nullable MessageRecord findRetryReceiptRelatedMessage(@NonNull Context context, @Nullable MessageLogEntry messageLogEntry, long sentTimestamp) {
if (messageLogEntry != null && messageLogEntry.hasRelatedMessage()) { if (messageLogEntry != null && messageLogEntry.hasRelatedMessage()) {
if (messageLogEntry.isRelatedMessageMms()) { MessageId relatedMessage = messageLogEntry.getRelatedMessages().get(0);
return DatabaseFactory.getMmsDatabase(context).getMessageRecordOrNull(messageLogEntry.getRelatedMessageId());
if (relatedMessage.isMms()) {
return DatabaseFactory.getMmsDatabase(context).getMessageRecordOrNull(relatedMessage.getId());
} else { } else {
return DatabaseFactory.getSmsDatabase(context).getMessageRecordOrNull(messageLogEntry.getRelatedMessageId()); return DatabaseFactory.getSmsDatabase(context).getMessageRecordOrNull(relatedMessage.getId());
} }
} else { } else {
return DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId()); return DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId());