mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Add sent story syncing.
This commit is contained in:
@@ -10,6 +10,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
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.DistributionListPartialRecord
|
||||
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.storage.SignalStoryDistributionListRecord
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.lang.AssertionError
|
||||
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)
|
||||
|
||||
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) {
|
||||
val recipientId = db.insert(
|
||||
@@ -68,6 +70,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||
const val IS_UNKNOWN = "is_unknown"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
@@ -76,7 +79,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
|
||||
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
|
||||
$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)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
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? {
|
||||
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 {
|
||||
query.isNullOrEmpty() && includeMyStory -> 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}"
|
||||
else -> "${ListTable.NAME} GLOB ? AND ${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} AND NOT ${ListTable.IS_UNKNOWN}"
|
||||
}
|
||||
|
||||
val whereArgs = when {
|
||||
@@ -155,7 +160,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
|
||||
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
|
||||
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}"
|
||||
|
||||
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)),
|
||||
name = CursorUtil.requireString(it, ListTable.NAME),
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -184,7 +234,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
distributionId: DistributionId = DistributionId.from(UUID.randomUUID()),
|
||||
allowsReplies: Boolean = true,
|
||||
deletionTimestamp: Long = 0L,
|
||||
storageId: ByteArray? = null
|
||||
storageId: ByteArray? = null,
|
||||
isUnknown: Boolean = false
|
||||
): DistributionListId? {
|
||||
val db = writableDatabase
|
||||
|
||||
@@ -196,6 +247,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
put(ListTable.ALLOWS_REPLIES, if (deletionTimestamp == 0L) allowsReplies else false)
|
||||
putNull(ListTable.RECIPIENT_ID)
|
||||
put(ListTable.DELETION_TIMESTAMP, deletionTimestamp)
|
||||
put(ListTable.IS_UNKNOWN, isUnknown)
|
||||
}
|
||||
|
||||
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 {
|
||||
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()) {
|
||||
@@ -251,7 +318,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getMembers(id),
|
||||
deletedAtTimestamp = 0L
|
||||
deletedAtTimestamp = 0L,
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -270,7 +338,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
|
||||
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
|
||||
members = getRawMembers(id),
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP)
|
||||
deletedAtTimestamp = cursor.requireLong(ListTable.DELETION_TIMESTAMP),
|
||||
isUnknown = CursorUtil.requireBoolean(cursor, ListTable.IS_UNKNOWN)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -461,7 +530,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
try {
|
||||
val listTableValues = contentValuesOf(
|
||||
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(
|
||||
@@ -493,4 +563,8 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
|
||||
private fun createUniqueNameForDeletedStory(): String {
|
||||
return "DELETED-${UUID.randomUUID()}"
|
||||
}
|
||||
|
||||
private fun createUniqueNameForUnknownDistributionId(): String {
|
||||
return "DELETED-${UUID.randomUUID()}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract boolean isStory(long messageId);
|
||||
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
|
||||
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
|
||||
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
|
||||
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
|
||||
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
|
||||
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
|
||||
|
||||
@@ -584,6 +584,15 @@ public class MmsDatabase extends MessageDatabase {
|
||||
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
|
||||
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
|
||||
@@ -932,7 +941,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
public boolean hasMeaningfulMessage(long threadId) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1401,6 +1401,11 @@ public class SmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getAllOutgoingStoriesAt(long sentTimestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
@@ -3,11 +3,15 @@ package org.thoughtcrime.securesms.database
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import androidx.core.content.contentValuesOf
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
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
|
||||
@@ -26,6 +30,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
const val SENT_TIMESTAMP = "sent_timestamp"
|
||||
const val ALLOWS_REPLIES = "allows_replies"
|
||||
const val DISTRIBUTION_ID = "distribution_id"
|
||||
|
||||
val CREATE_TABLE = """
|
||||
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,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE,
|
||||
$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()
|
||||
|
||||
@@ -42,7 +48,7 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||
""".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
|
||||
|
||||
db.beginTransaction()
|
||||
@@ -52,11 +58,12 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||
MESSAGE_ID to messageId,
|
||||
RECIPIENT_ID to id.serialize(),
|
||||
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) }
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
@@ -177,4 +184,208 @@ class StorySendsDatabase(context: Context, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1300,19 +1300,23 @@ public class ThreadDatabase extends Database {
|
||||
private boolean update(long threadId, boolean unarchive, boolean allowDeletion, boolean notifyListeners) {
|
||||
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
||||
boolean meaningfulMessages = mmsSmsDatabase.hasMeaningfulMessage(threadId);
|
||||
boolean isPinned = getPinnedThreadIds().contains(threadId);
|
||||
boolean shouldDelete = allowDeletion && !isPinned && !SignalDatabase.mms().containsStories(threadId);
|
||||
|
||||
if (!meaningfulMessages) {
|
||||
if (allowDeletion) {
|
||||
if (shouldDelete) {
|
||||
deleteConversation(threadId);
|
||||
return true;
|
||||
} else if (!isPinned) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
MessageRecord record;
|
||||
try {
|
||||
record = mmsSmsDatabase.getConversationSnippet(threadId);
|
||||
} catch (NoSuchMessageException e) {
|
||||
if (allowDeletion && !SignalDatabase.mms().containsStories(threadId)) {
|
||||
if (shouldDelete) {
|
||||
deleteConversation(threadId);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -196,8 +196,9 @@ object SignalDatabaseMigrations {
|
||||
private const val CDS_V2 = 140
|
||||
private const val GROUP_SERVICE_ID = 141
|
||||
private const val QUOTE_TYPE = 142
|
||||
private const val STORY_SYNCS = 143
|
||||
|
||||
const val DATABASE_VERSION = 142
|
||||
const val DATABASE_VERSION = 143
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2533,6 +2534,39 @@ object SignalDatabaseMigrations {
|
||||
if (oldVersion < QUOTE_TYPE) {
|
||||
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
|
||||
|
||||
@@ -6,5 +6,6 @@ data class DistributionListPartialRecord(
|
||||
val id: DistributionListId,
|
||||
val name: CharSequence,
|
||||
val recipientId: RecipientId,
|
||||
val allowsReplies: Boolean
|
||||
val allowsReplies: Boolean,
|
||||
val isUnknown: Boolean
|
||||
)
|
||||
|
||||
@@ -12,5 +12,6 @@ data class DistributionListRecord(
|
||||
val distributionId: DistributionId,
|
||||
val allowsReplies: Boolean,
|
||||
val members: List<RecipientId>,
|
||||
val deletedAtTimestamp: Long
|
||||
val deletedAtTimestamp: Long,
|
||||
val isUnknown: Boolean
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user