That fuck shit the fascists are using
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}