That fuck shit the fascists are using
at master 461 lines 15 kB view raw
1package org.tm.archive.database 2 3import android.content.ContentValues 4import android.content.Context 5import android.database.Cursor 6import androidx.core.content.contentValuesOf 7import org.signal.core.util.Serializer 8import org.signal.core.util.SqlUtil 9import org.signal.core.util.delete 10import org.signal.core.util.insertInto 11import org.signal.core.util.logging.Log 12import org.signal.core.util.readToList 13import org.signal.core.util.readToSet 14import org.signal.core.util.readToSingleInt 15import org.signal.core.util.readToSingleLong 16import org.signal.core.util.readToSingleObject 17import org.signal.core.util.requireBlob 18import org.signal.core.util.requireBoolean 19import org.signal.core.util.requireInt 20import org.signal.core.util.requireLong 21import org.signal.core.util.requireNonNullBlob 22import org.signal.core.util.requireNonNullString 23import org.signal.core.util.select 24import org.signal.core.util.update 25import org.signal.core.util.withinTransaction 26import org.signal.ringrtc.CallLinkRootKey 27import org.signal.ringrtc.CallLinkState.Restrictions 28import org.tm.archive.calls.log.CallLogRow 29import org.tm.archive.conversation.colors.AvatarColor 30import org.tm.archive.conversation.colors.AvatarColorHash 31import org.tm.archive.dependencies.ApplicationDependencies 32import org.tm.archive.recipients.Recipient 33import org.tm.archive.recipients.RecipientId 34import org.tm.archive.service.webrtc.links.CallLinkCredentials 35import org.tm.archive.service.webrtc.links.CallLinkRoomId 36import org.tm.archive.service.webrtc.links.SignalCallLinkState 37import java.time.Instant 38import java.time.temporal.ChronoUnit 39 40/** 41 * Table containing ad-hoc call link details 42 */ 43class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference { 44 45 companion object { 46 private val TAG = Log.tag(CallLinkTable::class.java) 47 48 const val TABLE_NAME = "call_link" 49 const val ID = "_id" 50 const val ROOT_KEY = "root_key" 51 const val ROOM_ID = "room_id" 52 const val ADMIN_KEY = "admin_key" 53 const val NAME = "name" 54 const val RESTRICTIONS = "restrictions" 55 const val REVOKED = "revoked" 56 const val EXPIRATION = "expiration" 57 const val RECIPIENT_ID = "recipient_id" 58 59 //language=sql 60 const val CREATE_TABLE = """ 61 CREATE TABLE $TABLE_NAME ( 62 $ID INTEGER PRIMARY KEY, 63 $ROOT_KEY BLOB, 64 $ROOM_ID TEXT NOT NULL UNIQUE, 65 $ADMIN_KEY BLOB, 66 $NAME TEXT NOT NULL, 67 $RESTRICTIONS INTEGER NOT NULL, 68 $REVOKED INTEGER NOT NULL, 69 $EXPIRATION INTEGER NOT NULL, 70 $RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE 71 ) 72 """ 73 74 private fun SignalCallLinkState.serialize(): ContentValues { 75 return contentValuesOf( 76 NAME to name, 77 RESTRICTIONS to restrictions.mapToInt(), 78 EXPIRATION to if (expiration == Instant.MAX) -1L else expiration.toEpochMilli(), 79 REVOKED to revoked 80 ) 81 } 82 83 private fun Restrictions.mapToInt(): Int { 84 return when (this) { 85 Restrictions.NONE -> 0 86 Restrictions.ADMIN_APPROVAL -> 1 87 Restrictions.UNKNOWN -> 2 88 } 89 } 90 } 91 92 fun insertCallLink( 93 callLink: CallLink 94 ) { 95 writableDatabase.withinTransaction { db -> 96 val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId) 97 98 db 99 .insertInto(TABLE_NAME) 100 .values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId))) 101 .run() 102 } 103 104 ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(callLink.roomId) 105 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() 106 } 107 108 fun updateCallLinkCredentials( 109 roomId: CallLinkRoomId, 110 credentials: CallLinkCredentials 111 ) { 112 writableDatabase 113 .update(TABLE_NAME) 114 .values( 115 contentValuesOf( 116 ROOT_KEY to credentials.linkKeyBytes, 117 ADMIN_KEY to credentials.adminPassBytes 118 ) 119 ) 120 .where("$ROOM_ID = ?", roomId.serialize()) 121 .run() 122 123 ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId) 124 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() 125 } 126 127 fun updateCallLinkState( 128 roomId: CallLinkRoomId, 129 state: SignalCallLinkState 130 ) { 131 writableDatabase 132 .update(TABLE_NAME) 133 .values(state.serialize()) 134 .where("$ROOM_ID = ?", roomId.serialize()) 135 .run() 136 137 val recipientId = readableDatabase 138 .select(RECIPIENT_ID) 139 .from(TABLE_NAME) 140 .where("$ROOM_ID = ?", roomId.serialize()) 141 .run() 142 .readToSingleLong() 143 .let { RecipientId.from(it) } 144 145 if (state.revoked) { 146 SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() 147 } 148 149 Recipient.live(recipientId).refresh() 150 ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId) 151 ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() 152 } 153 154 fun callLinkExists( 155 callLinkRoomId: CallLinkRoomId 156 ): Boolean { 157 return writableDatabase 158 .select("COUNT(*)") 159 .from(TABLE_NAME) 160 .where("$ROOM_ID = ?", callLinkRoomId.serialize()) 161 .run() 162 .readToSingleInt() > 0 163 } 164 165 fun getCallLinkByRoomId( 166 callLinkRoomId: CallLinkRoomId 167 ): CallLink? { 168 return writableDatabase 169 .select() 170 .from(TABLE_NAME) 171 .where("$ROOM_ID = ?", callLinkRoomId.serialize()) 172 .run() 173 .readToSingleObject { CallLinkDeserializer.deserialize(it) } 174 } 175 176 fun getOrCreateCallLinkByRootKey( 177 callLinkRootKey: CallLinkRootKey 178 ): CallLink { 179 val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId()) 180 val callLink = getCallLinkByRoomId(roomId) 181 return if (callLink == null) { 182 val link = CallLink( 183 recipientId = RecipientId.UNKNOWN, 184 roomId = roomId, 185 credentials = CallLinkCredentials( 186 linkKeyBytes = callLinkRootKey.keyBytes, 187 adminPassBytes = null 188 ), 189 state = SignalCallLinkState() 190 ) 191 192 insertCallLink(link) 193 return getCallLinkByRoomId(roomId)!! 194 } else { 195 callLink 196 } 197 } 198 199 fun getOrCreateCallLinkByRoomId( 200 callLinkRoomId: CallLinkRoomId 201 ): CallLink { 202 val callLink = getCallLinkByRoomId(callLinkRoomId) 203 return if (callLink == null) { 204 val link = CallLink( 205 recipientId = RecipientId.UNKNOWN, 206 roomId = callLinkRoomId, 207 credentials = null, 208 state = SignalCallLinkState() 209 ) 210 insertCallLink(link) 211 return getCallLinkByRoomId(callLinkRoomId)!! 212 } else { 213 callLink 214 } 215 } 216 217 fun getCallLinksCount(query: String?): Int { 218 return queryCallLinks(query, -1, -1, true).readToSingleInt(0) 219 } 220 221 fun getCallLinks(query: String?, offset: Int, limit: Int): List<CallLogRow.CallLink> { 222 return queryCallLinks(query, offset, limit, false).readToList { 223 val callLink = CallLinkDeserializer.deserialize(it) 224 val peer = Recipient.resolved(callLink.recipientId) 225 CallLogRow.CallLink( 226 record = callLink, 227 recipient = peer, 228 searchQuery = query, 229 callLinkPeekInfo = ApplicationDependencies.getSignalCallManager().peekInfoSnapshot[peer.id] 230 ) 231 } 232 } 233 234 /** 235 * Puts the call link into the "revoked" state which will hide it from the UI and 236 * delete it after a few days. 237 */ 238 fun markRevoked( 239 roomId: CallLinkRoomId 240 ) { 241 writableDatabase.withinTransaction { db -> 242 db.update(TABLE_NAME) 243 .values("$REVOKED" to true) 244 .where("$ROOM_ID", roomId) 245 .run() 246 247 SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps() 248 } 249 } 250 251 /** 252 * Deletes the call link. This should only happen *after* we send out a sync message 253 * or receive a sync message which deletes the corresponding link. 254 */ 255 fun deleteCallLink( 256 roomId: CallLinkRoomId 257 ) { 258 writableDatabase.withinTransaction { db -> 259 db.delete(TABLE_NAME) 260 .where("$ROOM_ID", roomId) 261 .run() 262 } 263 } 264 265 fun deleteNonAdminCallLinks(roomIds: Set<CallLinkRoomId>) { 266 val queries = SqlUtil.buildCollectionQuery(ROOM_ID, roomIds) 267 268 queries.forEach { 269 writableDatabase.delete(TABLE_NAME) 270 .where("${it.where} AND $ADMIN_KEY IS NULL", it.whereArgs) 271 .run() 272 } 273 } 274 275 fun deleteNonAdminCallLinksOnOrBefore(timestamp: Long) { 276 writableDatabase.withinTransaction { db -> 277 db.delete(TABLE_NAME) 278 .where("EXISTS (SELECT 1 FROM ${CallTable.TABLE_NAME} WHERE ${CallTable.TIMESTAMP} <= ? AND ${CallTable.PEER} = $RECIPIENT_ID)", timestamp) 279 .run() 280 281 SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps(skipSync = true) 282 } 283 } 284 285 fun getAdminCallLinks(roomIds: Set<CallLinkRoomId>): Set<CallLink> { 286 val queries = SqlUtil.buildCollectionQuery(ROOM_ID, roomIds) 287 288 return queries.map { 289 writableDatabase 290 .select() 291 .from(TABLE_NAME) 292 .where("${it.where} AND $ADMIN_KEY IS NOT NULL", it.whereArgs) 293 .run() 294 .readToList { CallLinkDeserializer.deserialize(it) } 295 }.flatten().toSet() 296 } 297 298 fun deleteAllNonAdminCallLinksExcept(roomIds: Set<CallLinkRoomId>) { 299 if (roomIds.isEmpty()) { 300 writableDatabase.delete(TABLE_NAME) 301 .where("$ADMIN_KEY IS NULL") 302 .run() 303 } else { 304 SqlUtil.buildCollectionQuery(ROOM_ID, roomIds, collectionOperator = SqlUtil.CollectionOperator.NOT_IN).forEach { 305 writableDatabase.delete(TABLE_NAME) 306 .where("${it.where} AND $ADMIN_KEY IS NULL", it.whereArgs) 307 .run() 308 } 309 } 310 } 311 312 fun getAllAdminCallLinksExcept(roomIds: Set<CallLinkRoomId>): Set<CallLink> { 313 return if (roomIds.isEmpty()) { 314 writableDatabase 315 .select() 316 .from(TABLE_NAME) 317 .where("$ADMIN_KEY IS NOT NULL") 318 .run() 319 .readToList { CallLinkDeserializer.deserialize(it) } 320 .toSet() 321 } else { 322 SqlUtil.buildCollectionQuery(ROOM_ID, roomIds, collectionOperator = SqlUtil.CollectionOperator.NOT_IN).map { 323 writableDatabase 324 .select() 325 .from(TABLE_NAME) 326 .where("${it.where} AND $ADMIN_KEY IS NOT NULL", it.whereArgs) 327 .run() 328 .readToList { CallLinkDeserializer.deserialize(it) } 329 }.flatten().toSet() 330 } 331 } 332 333 fun getAdminCallLinkCredentialsOnOrBefore(timestamp: Long): Set<CallLinkCredentials> { 334 val query = """ 335 SELECT $ROOT_KEY, $ADMIN_KEY FROM $TABLE_NAME 336 INNER JOIN ${CallTable.TABLE_NAME} ON ${CallTable.TABLE_NAME}.${CallTable.PEER} = $TABLE_NAME.$RECIPIENT_ID 337 WHERE ${CallTable.TIMESTAMP} <= $timestamp AND $ADMIN_KEY IS NOT NULL AND $REVOKED = 0 338 """.trimIndent() 339 340 return readableDatabase.query(query).readToSet { 341 CallLinkCredentials(it.requireNonNullBlob(ROOT_KEY), it.requireNonNullBlob(ADMIN_KEY)) 342 } 343 } 344 345 private fun queryCallLinks(query: String?, offset: Int, limit: Int, asCount: Boolean): Cursor { 346 //language=sql 347 val noCallEvent = """ 348 NOT EXISTS ( 349 SELECT 1 350 FROM ${CallTable.TABLE_NAME} 351 WHERE ${CallTable.PEER} = $TABLE_NAME.$RECIPIENT_ID 352 AND ${CallTable.TYPE} = ${CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL)} 353 AND ${CallTable.EVENT} != ${CallTable.Event.serialize(CallTable.Event.DELETE)} 354 ) 355 """.trimIndent() 356 357 val searchFilter = if (!query.isNullOrEmpty()) { 358 SqlUtil.buildQuery("AND $NAME GLOB ?", SqlUtil.buildCaseInsensitiveGlobPattern(query)) 359 } else { 360 null 361 } 362 363 val limitOffset = if (limit >= 0 && offset >= 0) { 364 //language=sql 365 "LIMIT $limit OFFSET $offset" 366 } else { 367 "" 368 } 369 370 val projection = if (asCount) { 371 "COUNT(*)" 372 } else { 373 "*" 374 } 375 376 //language=sql 377 val statement = """ 378 SELECT $projection 379 FROM $TABLE_NAME 380 WHERE $noCallEvent AND NOT $REVOKED ${searchFilter?.where ?: ""} 381 ORDER BY $ID DESC 382 $limitOffset 383 """.trimIndent() 384 385 return readableDatabase.query(statement, searchFilter?.whereArgs) 386 } 387 388 private object CallLinkSerializer : Serializer<CallLink, ContentValues> { 389 override fun serialize(data: CallLink): ContentValues { 390 return contentValuesOf( 391 RECIPIENT_ID to data.recipientId.takeIf { it != RecipientId.UNKNOWN }?.toLong(), 392 ROOM_ID to data.roomId.serialize(), 393 ROOT_KEY to data.credentials?.linkKeyBytes, 394 ADMIN_KEY to data.credentials?.adminPassBytes 395 ).apply { 396 putAll(data.state.serialize()) 397 } 398 } 399 400 override fun deserialize(data: ContentValues): CallLink { 401 throw UnsupportedOperationException() 402 } 403 } 404 405 private object CallLinkDeserializer : Serializer<CallLink, Cursor> { 406 override fun serialize(data: CallLink): Cursor { 407 throw UnsupportedOperationException() 408 } 409 410 override fun deserialize(data: Cursor): CallLink { 411 return CallLink( 412 recipientId = data.requireLong(RECIPIENT_ID).let { if (it > 0) RecipientId.from(it) else RecipientId.UNKNOWN }, 413 roomId = CallLinkRoomId.DatabaseSerializer.deserialize(data.requireNonNullString(ROOM_ID)), 414 credentials = CallLinkCredentials( 415 linkKeyBytes = data.requireNonNullBlob(ROOT_KEY), 416 adminPassBytes = data.requireBlob(ADMIN_KEY) 417 ), 418 state = SignalCallLinkState( 419 name = data.requireNonNullString(NAME), 420 restrictions = data.requireInt(RESTRICTIONS).mapToRestrictions(), 421 revoked = data.requireBoolean(REVOKED), 422 expiration = data.requireLong(EXPIRATION).let { 423 if (it == -1L) { 424 Instant.MAX 425 } else { 426 Instant.ofEpochMilli(it).truncatedTo(ChronoUnit.DAYS) 427 } 428 } 429 ) 430 ) 431 } 432 433 private fun Int.mapToRestrictions(): Restrictions { 434 return when (this) { 435 0 -> Restrictions.NONE 436 1 -> Restrictions.ADMIN_APPROVAL 437 else -> Restrictions.UNKNOWN 438 } 439 } 440 } 441 442 data class CallLink( 443 val recipientId: RecipientId, 444 val roomId: CallLinkRoomId, 445 val credentials: CallLinkCredentials?, 446 val state: SignalCallLinkState 447 ) { 448 val avatarColor: AvatarColor = credentials?.let { AvatarColorHash.forCallLink(it.linkKeyBytes) } ?: AvatarColor.UNKNOWN 449 } 450 451 override fun remapRecipient(fromId: RecipientId, toId: RecipientId) { 452 writableDatabase.update(TABLE_NAME) 453 .values( 454 contentValuesOf( 455 RECIPIENT_ID to toId.toLong() 456 ) 457 ) 458 .where("$RECIPIENT_ID = ?", fromId.toLong()) 459 .run() 460 } 461}