mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Add new name collision state management.
This commit is contained in:
committed by
Greyson Parrelli
parent
62cf3feeaa
commit
15d8a698c5
@@ -813,6 +813,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
Recipient.live(groupRecipientId).refresh()
|
||||
notifyConversationListListeners()
|
||||
|
||||
if (groupId.isV2) {
|
||||
SignalDatabase.nameCollisions.handleGroupNameCollisions(groupId.requireV2(), members.toSet())
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -881,7 +885,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
|
||||
val groupMembers = getV2GroupMembers(decryptedGroup, true)
|
||||
|
||||
if (existingGroup.isPresent && existingGroup.get().isV2Group) {
|
||||
val addedMembers: List<RecipientId> = if (existingGroup.isPresent && existingGroup.get().isV2Group) {
|
||||
val change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().decryptedGroup, decryptedGroup)
|
||||
val removed: List<ServiceId> = DecryptedGroupUtil.removedMembersServiceIdList(change)
|
||||
|
||||
@@ -898,6 +902,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
e164 = null
|
||||
)
|
||||
}
|
||||
|
||||
change.newMembers.toAciList().toRecipientIds()
|
||||
} else {
|
||||
groupMembers
|
||||
}
|
||||
|
||||
writableDatabase.withinTransaction { database ->
|
||||
@@ -920,6 +928,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
|
||||
Recipient.live(groupRecipientId).refresh()
|
||||
notifyConversationListListeners()
|
||||
|
||||
if (groupId.isV2 && addedMembers.isNotEmpty()) {
|
||||
SignalDatabase.nameCollisions.handleGroupNameCollisions(groupId.requireV2(), addedMembers.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(groupId: GroupId.V1, title: String?) {
|
||||
|
||||
@@ -1073,6 +1073,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
notifyConversationListeners(threadId)
|
||||
TrimThreadJob.enqueueAsync(threadId)
|
||||
}
|
||||
|
||||
groupRecords.filter { it.isV2Group }.forEach {
|
||||
SignalDatabase.nameCollisions.handleGroupNameCollisions(it.id.requireV2(), setOf(recipient.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.exists
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSet
|
||||
import org.signal.core.util.readToSingleLong
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupId.V2
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Tables to help manage the state of name collisions.
|
||||
*/
|
||||
class NameCollisionTables(
|
||||
context: Context,
|
||||
database: SignalDatabase
|
||||
) : DatabaseTable(context, database) {
|
||||
|
||||
companion object {
|
||||
private const val ID = "_id"
|
||||
|
||||
private val PROFILE_CHANGE_TIMEOUT = 1.days
|
||||
|
||||
fun createTables(db: SQLiteDatabase) {
|
||||
db.execSQL(NameCollisionTable.CREATE_TABLE)
|
||||
db.execSQL(NameCollisionMembershipTable.CREATE_TABLE)
|
||||
}
|
||||
|
||||
fun createIndexes(db: SQLiteDatabase) {
|
||||
NameCollisionMembershipTable.CREATE_INDEXES.forEach {
|
||||
db.execSQL(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a detected name collision which can involve one or more recipients.
|
||||
*/
|
||||
private object NameCollisionTable {
|
||||
const val TABLE_NAME = "name_collision"
|
||||
|
||||
/**
|
||||
* The thread id of the conversation to display this collision for.
|
||||
*/
|
||||
const val THREAD_ID = "thread_id"
|
||||
|
||||
/**
|
||||
* Whether the user has manually dismissed the collision.
|
||||
*/
|
||||
const val DISMISSED = "dismissed"
|
||||
|
||||
/**
|
||||
* The hash representing the latest known display name state.
|
||||
*/
|
||||
const val HASH = "hash"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$THREAD_ID INTEGER UNIQUE NOT NULL,
|
||||
$DISMISSED INTEGER DEFAULT 0,
|
||||
$HASH STRING DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a recipient who is involved in a name collision.
|
||||
*/
|
||||
private object NameCollisionMembershipTable {
|
||||
const val TABLE_NAME = "name_collision_membership"
|
||||
|
||||
/**
|
||||
* FK Reference to a name_collision
|
||||
*/
|
||||
const val COLLISION_ID = "collision_id"
|
||||
|
||||
/**
|
||||
* FK Reference to the recipient involved
|
||||
*/
|
||||
const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
/**
|
||||
* Proto containing group profile change details. Only present for entries tied to group collisions.
|
||||
*/
|
||||
const val PROFILE_CHANGE_DETAILS = "profile_change_details"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$COLLISION_ID INTEGER NOT NULL REFERENCES ${NameCollisionTable.TABLE_NAME} ($ID) ON DELETE CASCADE,
|
||||
$RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} ($ID) ON DELETE CASCADE,
|
||||
$PROFILE_CHANGE_DETAILS BLOB DEFAULT NULL,
|
||||
UNIQUE ($COLLISION_ID, $RECIPIENT_ID)
|
||||
)
|
||||
"""
|
||||
|
||||
val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX name_collision_membership_collision_id_index ON $TABLE_NAME ($COLLISION_ID)",
|
||||
"CREATE INDEX name_collision_membership_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID)"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the relevant collisions dismissed according to the given thread recipient.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun markCollisionsForThreadRecipientDismissed(threadRecipientId: RecipientId) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(threadRecipientId) ?: return@withinTransaction
|
||||
|
||||
db.update(NameCollisionTable.TABLE_NAME)
|
||||
.values(NameCollisionTable.DISMISSED to 1)
|
||||
.where("${NameCollisionTable.THREAD_ID} = ?", threadId)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A flattened list of similar recipients.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getCollisionsForThreadRecipientId(recipientId: RecipientId): List<ReviewRecipient> {
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(recipientId) ?: return emptyList()
|
||||
val collisionId = readableDatabase
|
||||
.select(ID)
|
||||
.from(NameCollisionTable.TABLE_NAME)
|
||||
.where("${NameCollisionTable.THREAD_ID} = ? AND ${NameCollisionTable.DISMISSED} = 0", threadId)
|
||||
.run()
|
||||
.readToSingleLong()
|
||||
|
||||
if (collisionId <= 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val collisions = readableDatabase
|
||||
.select()
|
||||
.from(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.where("${NameCollisionMembershipTable.COLLISION_ID} = ?", collisionId)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
ReviewRecipient(
|
||||
Recipient.resolved(RecipientId.from(cursor.requireLong(NameCollisionMembershipTable.RECIPIENT_ID))),
|
||||
cursor.requireBlob(NameCollisionMembershipTable.PROFILE_CHANGE_DETAILS)?.let { ProfileChangeDetails.ADAPTER.decode(it) }
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
val groups = collisions.groupBy { SqlUtil.buildCaseInsensitiveGlobPattern(it.recipient.getDisplayName(context)) }
|
||||
val toDelete: List<ReviewRecipient> = groups.values.filter { it.size < 2 }.flatten()
|
||||
val toReturn: List<ReviewRecipient> = groups.values.filter { it.size >= 2 }.flatten()
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val queries = SqlUtil.buildCollectionQuery(
|
||||
column = NameCollisionMembershipTable.RECIPIENT_ID,
|
||||
values = toDelete.map { it.recipient.id }
|
||||
)
|
||||
|
||||
for (query in queries) {
|
||||
db.delete(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.where("${NameCollisionMembershipTable.COLLISION_ID} = ? AND ${query.where}", SqlUtil.appendArgs(arrayOf(collisionId.toString()), query.whereArgs))
|
||||
.run()
|
||||
}
|
||||
|
||||
pruneCollisions()
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the collision *only* for the given individual.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun handleIndividualNameCollision(recipientId: RecipientId) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val similarRecipients = SignalDatabase.recipients.getSimilarRecipientIds(Recipient.resolved(recipientId))
|
||||
|
||||
db.delete(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.where("${NameCollisionMembershipTable.RECIPIENT_ID} = ?", recipientId)
|
||||
.run()
|
||||
|
||||
if (similarRecipients.size == 1) {
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(recipientId) ?: -1
|
||||
if (threadId > 0L) {
|
||||
db.delete(NameCollisionTable.TABLE_NAME)
|
||||
.where("${NameCollisionTable.THREAD_ID} = ?", threadId)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
similarRecipients.forEach { threadRecipientId ->
|
||||
handleNameCollisions(
|
||||
threadRecipientId = threadRecipientId,
|
||||
getCollisionRecipients = {
|
||||
val recipients = Recipient.resolvedList(similarRecipients)
|
||||
|
||||
recipients.map { ReviewRecipient(it) }.toSet()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pruneCollisions()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the collisions for the given group
|
||||
*/
|
||||
@WorkerThread
|
||||
fun handleGroupNameCollisions(groupId: GroupId.V2, changed: Set<RecipientId>) {
|
||||
writableDatabase.withinTransaction {
|
||||
val threadRecipientId = SignalDatabase.recipients.getByGroupId(groupId).orNull() ?: return@withinTransaction
|
||||
handleNameCollisions(
|
||||
threadRecipientId = threadRecipientId,
|
||||
getCollisionRecipients = { getDuplicatedGroupRecipients(groupId, changed).toSet() }
|
||||
)
|
||||
|
||||
pruneCollisions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNameCollisions(
|
||||
threadRecipientId: RecipientId,
|
||||
getCollisionRecipients: () -> Set<ReviewRecipient>
|
||||
) {
|
||||
check(writableDatabase.inTransaction())
|
||||
|
||||
val resolved = Recipient.resolved(threadRecipientId)
|
||||
val collisionRecipients: Set<ReviewRecipient> = getCollisionRecipients()
|
||||
|
||||
if (collisionRecipients.size < 2 && !collisionExists(threadRecipientId)) {
|
||||
return
|
||||
}
|
||||
|
||||
val collision: NameCollision = getOrCreateCollision(resolved)
|
||||
val hash: String = calculateHash(collisionRecipients)
|
||||
|
||||
updateCollision(
|
||||
collision.copy(
|
||||
members = collisionRecipients,
|
||||
hash = hash,
|
||||
dismissed = if (!collision.dismissed) false else collision.hash == hash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun collisionExists(threadRecipientId: RecipientId): Boolean {
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(threadRecipientId) ?: return false
|
||||
return writableDatabase
|
||||
.exists(NameCollisionTable.TABLE_NAME)
|
||||
.where("${NameCollisionTable.THREAD_ID} = ?", threadId)
|
||||
.run()
|
||||
}
|
||||
|
||||
private fun getOrCreateCollision(threadRecipient: Recipient): NameCollision {
|
||||
check(writableDatabase.inTransaction())
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
|
||||
val collision = writableDatabase
|
||||
.select()
|
||||
.from(NameCollisionTable.TABLE_NAME)
|
||||
.where("${NameCollisionTable.THREAD_ID} = ?", threadId)
|
||||
.run()
|
||||
.readToSingleObject { nameCollisionCursor ->
|
||||
NameCollision(
|
||||
id = nameCollisionCursor.requireLong(ID),
|
||||
threadId = threadId,
|
||||
members = writableDatabase
|
||||
.select(NameCollisionMembershipTable.RECIPIENT_ID, NameCollisionMembershipTable.PROFILE_CHANGE_DETAILS)
|
||||
.from(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.where("${NameCollisionMembershipTable.COLLISION_ID} = ?", nameCollisionCursor.requireInt(ID))
|
||||
.run()
|
||||
.readToSet {
|
||||
val id = RecipientId.from(it.requireLong(NameCollisionMembershipTable.RECIPIENT_ID))
|
||||
val rawProfileChangeDetails = it.requireBlob(NameCollisionMembershipTable.PROFILE_CHANGE_DETAILS)
|
||||
val profileChangeDetails = if (rawProfileChangeDetails != null) {
|
||||
ProfileChangeDetails.ADAPTER.decode(rawProfileChangeDetails)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
ReviewRecipient(
|
||||
Recipient.resolved(id),
|
||||
profileChangeDetails
|
||||
)
|
||||
},
|
||||
dismissed = nameCollisionCursor.requireBoolean(NameCollisionTable.DISMISSED),
|
||||
hash = nameCollisionCursor.requireString(NameCollisionTable.HASH) ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
return if (collision == null) {
|
||||
val rowId = writableDatabase
|
||||
.insertInto(NameCollisionTable.TABLE_NAME)
|
||||
.values(
|
||||
contentValuesOf(
|
||||
NameCollisionTable.THREAD_ID to threadId,
|
||||
NameCollisionTable.DISMISSED to 0,
|
||||
NameCollisionTable.HASH to null
|
||||
)
|
||||
)
|
||||
.run()
|
||||
|
||||
NameCollision(id = rowId, threadId = threadId, members = emptySet(), dismissed = false, hash = "")
|
||||
} else {
|
||||
collision
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCollision(collision: NameCollision) {
|
||||
check(writableDatabase.inTransaction())
|
||||
|
||||
writableDatabase
|
||||
.update(NameCollisionTable.TABLE_NAME)
|
||||
.values(
|
||||
contentValuesOf(
|
||||
NameCollisionTable.DISMISSED to collision.dismissed.toInt(),
|
||||
NameCollisionTable.THREAD_ID to collision.threadId,
|
||||
NameCollisionTable.HASH to collision.hash
|
||||
)
|
||||
)
|
||||
.where("$ID = ?", collision.id)
|
||||
.run()
|
||||
|
||||
writableDatabase
|
||||
.delete(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.where("${NameCollisionMembershipTable.COLLISION_ID} = ?")
|
||||
.run()
|
||||
|
||||
if (collision.members.size < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
collision.members.forEach { member ->
|
||||
writableDatabase
|
||||
.insertInto(NameCollisionMembershipTable.TABLE_NAME)
|
||||
.values(
|
||||
NameCollisionMembershipTable.RECIPIENT_ID to member.recipient.id.toLong(),
|
||||
NameCollisionMembershipTable.COLLISION_ID to collision.id,
|
||||
NameCollisionMembershipTable.PROFILE_CHANGE_DETAILS to member.profileChangeDetails?.encode()
|
||||
)
|
||||
.run(conflictStrategy = org.thoughtcrime.securesms.database.SQLiteDatabase.CONFLICT_IGNORE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateHash(collisionRecipients: Set<ReviewRecipient>): String {
|
||||
if (collisionRecipients.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return try {
|
||||
val digest = MessageDigest.getInstance("MD5")
|
||||
val names = collisionRecipients.map { it.recipient.getDisplayName(context) }
|
||||
names.forEach { digest.update(it.encodeToByteArray()) }
|
||||
Hex.toStringCondensed(digest.digest())
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any collision for which there is only a single member.
|
||||
*/
|
||||
private fun pruneCollisions() {
|
||||
check(writableDatabase.inTransaction())
|
||||
|
||||
writableDatabase.execSQL(
|
||||
"""
|
||||
DELETE FROM ${NameCollisionTable.TABLE_NAME}
|
||||
WHERE ${NameCollisionTable.TABLE_NAME}.$ID IN (
|
||||
SELECT ${NameCollisionMembershipTable.COLLISION_ID}
|
||||
FROM ${NameCollisionMembershipTable.TABLE_NAME}
|
||||
GROUP BY ${NameCollisionMembershipTable.COLLISION_ID}
|
||||
HAVING COUNT($ID) < 2
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDuplicatedGroupRecipients(groupId: V2, toCheck: Set<RecipientId>): List<ReviewRecipient> {
|
||||
if (toCheck.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val profileChangeRecords: Map<RecipientId, MessageRecord> = getProfileChangeRecordsForGroup(groupId).associateBy { it.fromRecipient.id }
|
||||
val members: MutableList<Recipient> = SignalDatabase.groups.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_INCLUDING_SELF).toMutableList()
|
||||
val changed: List<ReviewRecipient> = Recipient.resolvedList(toCheck)
|
||||
.map { recipient -> ReviewRecipient(recipient.resolve(), profileChangeRecords[recipient.id]?.let { getProfileChangeDetails(it) }) }
|
||||
.filter { !it.recipient.isSystemContact && it.recipient.nickname.isEmpty }
|
||||
|
||||
val results = mutableListOf<ReviewRecipient>()
|
||||
|
||||
for (reviewRecipient in changed) {
|
||||
if (results.contains(reviewRecipient)) {
|
||||
continue
|
||||
}
|
||||
|
||||
members.remove(reviewRecipient.recipient)
|
||||
|
||||
for (member in members) {
|
||||
if (member.getDisplayName(context) == reviewRecipient.recipient.getDisplayName(context)) {
|
||||
results.add(reviewRecipient)
|
||||
results.add(ReviewRecipient(member))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private fun getProfileChangeRecordsForGroup(groupId: V2): List<MessageRecord> {
|
||||
val groupRecipientId = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||
val groupThreadId = SignalDatabase.threads.getThreadIdFor(groupRecipientId)
|
||||
|
||||
return if (groupThreadId == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
SignalDatabase.messages.getProfileChangeDetailsRecords(
|
||||
groupThreadId,
|
||||
System.currentTimeMillis() - PROFILE_CHANGE_TIMEOUT.inWholeMilliseconds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProfileChangeDetails(record: MessageRecord): ProfileChangeDetails {
|
||||
try {
|
||||
return ProfileChangeDetails.ADAPTER.decode(Base64.decode(record.body))
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private data class NameCollision(
|
||||
val id: Long,
|
||||
val threadId: Long,
|
||||
val members: Set<ReviewRecipient>,
|
||||
val dismissed: Boolean,
|
||||
val hash: String
|
||||
)
|
||||
}
|
||||
@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
@@ -1715,9 +1716,20 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
|
||||
fun getSimilarRecipientIds(recipient: Recipient): List<RecipientId> {
|
||||
val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name")
|
||||
val where = "checked_name = ? AND $HIDDEN = ?"
|
||||
val arguments = SqlUtil.buildArgs(recipient.profileName.toString(), 0)
|
||||
if (!recipient.nickname.isEmpty || recipient.isSystemContact) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val threadId = threads.getThreadIdFor(recipient.id)
|
||||
val isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(threadId, recipient)
|
||||
if (isMessageRequestAccepted) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val glob = SqlUtil.buildCaseInsensitiveGlobPattern(recipient.profileName.toString())
|
||||
val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($NICKNAME_JOINED_NAME, ''), NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name")
|
||||
val where = "checked_name GLOB ? AND $HIDDEN = ? AND $BLOCKED = ?"
|
||||
val arguments = SqlUtil.buildArgs(glob, 0, 0)
|
||||
|
||||
readableDatabase.query(TABLE_NAME, projection, where, arguments, null, null, null).use { cursor ->
|
||||
if (cursor == null || cursor.count == 0) {
|
||||
|
||||
@@ -73,6 +73,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val callTable: CallTable = CallTable(context, this)
|
||||
val kyberPreKeyTable: KyberPreKeyTable = KyberPreKeyTable(context, this)
|
||||
val callLinkTable: CallLinkTable = CallLinkTable(context, this)
|
||||
val nameCollisionTables: NameCollisionTables = NameCollisionTables(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
@@ -109,6 +110,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
db.execSQL(CallLinkTable.CREATE_TABLE)
|
||||
db.execSQL(CallTable.CREATE_TABLE)
|
||||
db.execSQL(KyberPreKeyTable.CREATE_TABLE)
|
||||
NameCollisionTables.createTables(db)
|
||||
executeStatements(db, SearchTable.CREATE_TABLE)
|
||||
executeStatements(db, RemappedRecordTables.CREATE_TABLE)
|
||||
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
|
||||
@@ -139,6 +141,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, SearchTable.CREATE_TRIGGERS)
|
||||
executeStatements(db, MessageSendLogTables.CREATE_TRIGGERS)
|
||||
|
||||
NameCollisionTables.createIndexes(db)
|
||||
|
||||
DistributionListTables.insertInitialDistributionListAtCreationTime(db)
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
@@ -526,5 +530,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
@get:JvmName("callLinks")
|
||||
val callLinks: CallLinkTable
|
||||
get() = instance!!.callLinkTable
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("nameCollisions")
|
||||
val nameCollisions: NameCollisionTables
|
||||
get() = instance!!.nameCollisionTables
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V224_AddAttachmentA
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V225_AddLocalUserJoinedStateAndGroupCallActiveState
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V226_AddAttachmentMediaIdIndex
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V227_AddAttachmentArchiveTransferState
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V228_AddNameCollisionTables
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -172,10 +173,11 @@ object SignalDatabaseMigrations {
|
||||
224 to V224_AddAttachmentArchiveColumns,
|
||||
225 to V225_AddLocalUserJoinedStateAndGroupCallActiveState,
|
||||
226 to V226_AddAttachmentMediaIdIndex,
|
||||
227 to V227_AddAttachmentArchiveTransferState
|
||||
227 to V227_AddAttachmentArchiveTransferState,
|
||||
228 to V228_AddNameCollisionTables
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 227
|
||||
const val DATABASE_VERSION = 228
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds the tables for managing name collisions
|
||||
*/
|
||||
object V228_AddNameCollisionTables : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE name_collision (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
thread_id INTEGER UNIQUE NOT NULL,
|
||||
dismissed INTEGER DEFAULT 0,
|
||||
hash STRING DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE name_collision_membership (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collision_id INTEGER NOT NULL REFERENCES name_collision (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
profile_change_details BLOB DEFAULT NULL,
|
||||
UNIQUE (collision_id, recipient_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
db.execSQL("CREATE INDEX name_collision_membership_collision_id_index ON name_collision_membership (collision_id)")
|
||||
db.execSQL("CREATE INDEX name_collision_membership_recipient_id_index ON name_collision_membership (recipient_id)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user