That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.ContentValues
4import android.content.Context
5import android.database.sqlite.SQLiteConstraintException
6import org.signal.core.util.CursorUtil
7import org.signal.core.util.SqlUtil
8import org.signal.core.util.delete
9import org.signal.core.util.logging.Log
10import org.signal.core.util.readToList
11import org.signal.core.util.requireBoolean
12import org.signal.core.util.requireLong
13import org.signal.core.util.toInt
14import org.tm.archive.database.model.MessageId
15import org.tm.archive.database.model.MessageLogEntry
16import org.tm.archive.recipients.Recipient
17import org.tm.archive.recipients.RecipientId
18import org.tm.archive.util.FeatureFlags
19import org.tm.archive.util.RecipientAccessList
20import org.whispersystems.signalservice.api.crypto.ContentHint
21import org.whispersystems.signalservice.api.messages.SendMessageResult
22import org.whispersystems.signalservice.internal.push.Content
23
24/**
25 * Stores a rolling buffer of all outgoing messages. Used for the retry logic required for sender key.
26 *
27 * General note: This class is actually three tables:
28 * - one to store the entry
29 * - one to store all the devices that were sent it, and
30 * - one to store the set of related messages.
31 *
32 * The general lifecycle of entries in the store goes something like this:
33 * - 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
34 * related message in the 'message table'
35 * - Whenever you get a delivery receipt, delete the entries in the 'recipient table'
36 * - Whenever there's no more records in the 'recipient table' for a given message, delete the entry in the 'message table'
37 * - Whenever you delete a message, delete the relevant entries from the 'payload table'
38 * - Whenever you read an entry from the table, first trim off all the entries that are too old
39 *
40 * 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
41 *
42 * 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
43 * cascading delete foreign keys between these three tables.
44 *
45 * Performance considerations:
46 * - The most common operations by far are:
47 * - Inserting into the table
48 * - Deleting a recipient (in response to a delivery receipt)
49 * - We should also optimize for when we delete messages from the sms/mms tables, since you can delete a bunch at once
50 * - 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
51 * improve retrieval time. That means we shouldn't be adding indexes that optimize for retrieval.
52 */
53class MessageSendLogTables constructor(context: Context?, databaseHelper: SignalDatabase?) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
54
55 companion object {
56 private val TAG = Log.tag(MessageSendLogTables::class.java)
57
58 @JvmField
59 val CREATE_TABLE: Array<String> = arrayOf(MslPayloadTable.CREATE_TABLE, MslRecipientTable.CREATE_TABLE, MslMessageTable.CREATE_TABLE)
60
61 @JvmField
62 val CREATE_INDEXES: Array<String> = MslPayloadTable.CREATE_INDEXES + MslRecipientTable.CREATE_INDEXES + MslMessageTable.CREATE_INDEXES
63
64 @JvmField
65 val CREATE_TRIGGERS: Array<String> = MslPayloadTable.CREATE_TRIGGERS
66 }
67
68 private object MslPayloadTable {
69 const val TABLE_NAME = "msl_payload"
70
71 const val ID = "_id"
72 const val DATE_SENT = "date_sent"
73 const val CONTENT = "content"
74 const val CONTENT_HINT = "content_hint"
75 const val URGENT = "urgent"
76
77 const val CREATE_TABLE = """
78 CREATE TABLE $TABLE_NAME (
79 $ID INTEGER PRIMARY KEY,
80 $DATE_SENT INTEGER NOT NULL,
81 $CONTENT BLOB NOT NULL,
82 $CONTENT_HINT INTEGER NOT NULL,
83 $URGENT INTEGER NOT NULL DEFAULT 1
84 )
85 """
86
87 /** Created for [deleteEntriesForRecipient] */
88 val CREATE_INDEXES = arrayOf(
89 "CREATE INDEX msl_payload_date_sent_index ON $TABLE_NAME ($DATE_SENT)"
90 )
91
92 val CREATE_TRIGGERS = arrayOf(
93 """
94 CREATE TRIGGER msl_message_delete AFTER DELETE ON ${MessageTable.TABLE_NAME}
95 BEGIN
96 DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MslMessageTable.PAYLOAD_ID} FROM ${MslMessageTable.TABLE_NAME} WHERE ${MslMessageTable.MESSAGE_ID} = old.${MessageTable.ID});
97 END
98 """,
99 """
100 CREATE TRIGGER msl_attachment_delete AFTER DELETE ON ${AttachmentTable.TABLE_NAME}
101 BEGIN
102 DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MslMessageTable.PAYLOAD_ID} FROM ${MslMessageTable.TABLE_NAME} WHERE ${MslMessageTable.TABLE_NAME}.${MslMessageTable.MESSAGE_ID} = old.${AttachmentTable.MESSAGE_ID});
103 END
104 """
105 )
106 }
107
108 private object MslRecipientTable {
109 const val TABLE_NAME = "msl_recipient"
110
111 const val ID = "_id"
112 const val PAYLOAD_ID = "payload_id"
113 const val RECIPIENT_ID = "recipient_id"
114 const val DEVICE = "device"
115
116 const val CREATE_TABLE = """
117 CREATE TABLE $TABLE_NAME (
118 $ID INTEGER PRIMARY KEY,
119 $PAYLOAD_ID INTEGER NOT NULL REFERENCES ${MslPayloadTable.TABLE_NAME} (${MslPayloadTable.ID}) ON DELETE CASCADE,
120 $RECIPIENT_ID INTEGER NOT NULL,
121 $DEVICE INTEGER NOT NULL
122 )
123 """
124
125 /** Created for [deleteEntriesForRecipient] */
126 val CREATE_INDEXES = arrayOf(
127 "CREATE INDEX msl_recipient_recipient_index ON $TABLE_NAME ($RECIPIENT_ID, $DEVICE, $PAYLOAD_ID)",
128 "CREATE INDEX msl_recipient_payload_index ON $TABLE_NAME ($PAYLOAD_ID)"
129 )
130 }
131
132 private object MslMessageTable {
133 const val TABLE_NAME = "msl_message"
134
135 const val ID = "_id"
136 const val PAYLOAD_ID = "payload_id"
137 const val MESSAGE_ID = "message_id"
138
139 const val CREATE_TABLE = """
140 CREATE TABLE $TABLE_NAME (
141 $ID INTEGER PRIMARY KEY,
142 $PAYLOAD_ID INTEGER NOT NULL REFERENCES ${MslPayloadTable.TABLE_NAME} (${MslPayloadTable.ID}) ON DELETE CASCADE,
143 $MESSAGE_ID INTEGER NOT NULL
144 )
145 """
146
147 /** Created for [MslPayloadTable.CREATE_TRIGGERS] and [deleteAllRelatedToMessage] */
148 val CREATE_INDEXES = arrayOf(
149 "CREATE INDEX msl_message_message_index ON $TABLE_NAME ($MESSAGE_ID, $PAYLOAD_ID)",
150 "CREATE INDEX msl_message_payload_index ON $TABLE_NAME ($PAYLOAD_ID)"
151 )
152 }
153
154 /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */
155 fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long {
156 if (!FeatureFlags.retryReceipts()) return -1
157
158 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) {
159 val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices))
160 return insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, listOf(messageId), urgent)
161 }
162
163 return -1
164 }
165
166 /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */
167 fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageIds: List<MessageId>, urgent: Boolean): Long {
168 if (!FeatureFlags.retryReceipts()) return -1
169
170 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) {
171 val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices))
172 return insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, messageIds, urgent)
173 }
174
175 return -1
176 }
177
178 /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */
179 fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List<Recipient>, results: List<SendMessageResult>, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long {
180 if (!FeatureFlags.retryReceipts()) return -1
181
182 val accessList = RecipientAccessList(possibleRecipients)
183
184 val recipientDevices: List<RecipientDevice> = results
185 .filter { it.isSuccess && it.success.content.isPresent }
186 .map { result ->
187 val recipient: Recipient = accessList.requireByAddress(result.address)
188 RecipientDevice(recipient.id, result.success.devices)
189 }
190
191 if (recipientDevices.isEmpty()) {
192 return -1
193 }
194
195 val content: Content = results.first { it.isSuccess && it.success.content.isPresent }.success.content.get()
196
197 return insert(recipientDevices, sentTimestamp, content, contentHint, listOf(messageId), urgent)
198 }
199
200 fun addRecipientToExistingEntryIfPossible(payloadId: Long, recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long {
201 if (!FeatureFlags.retryReceipts()) return payloadId
202
203 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) {
204 val db = databaseHelper.signalWritableDatabase
205
206 db.beginTransaction()
207 try {
208 sendMessageResult.success.devices.forEach { device ->
209 val recipientValues = ContentValues().apply {
210 put(MslRecipientTable.PAYLOAD_ID, payloadId)
211 put(MslRecipientTable.RECIPIENT_ID, recipientId.serialize())
212 put(MslRecipientTable.DEVICE, device)
213 }
214
215 db.insert(MslRecipientTable.TABLE_NAME, null, recipientValues)
216 }
217
218 db.setTransactionSuccessful()
219 } catch (e: SQLiteConstraintException) {
220 Log.w(TAG, "Failed to append to existing entry. Creating a new one.")
221 val newPayloadId = insertIfPossible(recipientId, sentTimestamp, sendMessageResult, contentHint, messageId, urgent)
222 db.setTransactionSuccessful()
223 return newPayloadId
224 } finally {
225 db.endTransaction()
226 }
227 }
228
229 return payloadId
230 }
231
232 private fun insert(recipients: List<RecipientDevice>, dateSent: Long, content: Content, contentHint: ContentHint, messageIds: List<MessageId>, urgent: Boolean): Long {
233 val db = databaseHelper.signalWritableDatabase
234
235 db.beginTransaction()
236 try {
237 val payloadValues = ContentValues().apply {
238 put(MslPayloadTable.DATE_SENT, dateSent)
239 put(MslPayloadTable.CONTENT, content.encode())
240 put(MslPayloadTable.CONTENT_HINT, contentHint.type)
241 put(MslPayloadTable.URGENT, urgent.toInt())
242 }
243
244 val payloadId: Long = db.insert(MslPayloadTable.TABLE_NAME, null, payloadValues)
245
246 val recipientValues: MutableList<ContentValues> = mutableListOf()
247 recipients.forEach { recipientDevice ->
248 recipientDevice.devices.forEach { device ->
249 recipientValues += ContentValues().apply {
250 put(MslRecipientTable.PAYLOAD_ID, payloadId)
251 put(MslRecipientTable.RECIPIENT_ID, recipientDevice.recipientId.serialize())
252 put(MslRecipientTable.DEVICE, device)
253 }
254 }
255 }
256 SqlUtil.buildBulkInsert(MslRecipientTable.TABLE_NAME, arrayOf(MslRecipientTable.PAYLOAD_ID, MslRecipientTable.RECIPIENT_ID, MslRecipientTable.DEVICE), recipientValues)
257 .forEach { query -> db.execSQL(query.where, query.whereArgs) }
258
259 val messageValues: MutableList<ContentValues> = mutableListOf()
260 messageIds.forEach { messageId ->
261 messageValues += ContentValues().apply {
262 put(MslMessageTable.PAYLOAD_ID, payloadId)
263 put(MslMessageTable.MESSAGE_ID, messageId.id)
264 }
265 }
266 SqlUtil.buildBulkInsert(MslMessageTable.TABLE_NAME, arrayOf(MslMessageTable.PAYLOAD_ID, MslMessageTable.MESSAGE_ID), messageValues)
267 .forEach { query -> db.execSQL(query.where, query.whereArgs) }
268
269 db.setTransactionSuccessful()
270 return payloadId
271 } finally {
272 db.endTransaction()
273 }
274 }
275
276 fun getLogEntry(recipientId: RecipientId, device: Int, dateSent: Long): MessageLogEntry? {
277 if (!FeatureFlags.retryReceipts()) return null
278
279 trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())
280
281 val db = databaseHelper.signalReadableDatabase
282 val table = "${MslPayloadTable.TABLE_NAME} LEFT JOIN ${MslRecipientTable.TABLE_NAME} ON ${MslPayloadTable.TABLE_NAME}.${MslPayloadTable.ID} = ${MslRecipientTable.TABLE_NAME}.${MslRecipientTable.PAYLOAD_ID}"
283 val query = "${MslPayloadTable.DATE_SENT} = ? AND ${MslRecipientTable.RECIPIENT_ID} = ? AND ${MslRecipientTable.DEVICE} = ?"
284 val args = SqlUtil.buildArgs(dateSent, recipientId, device)
285
286 db.query(table, null, query, args, null, null, null).use { entryCursor ->
287 if (entryCursor.moveToFirst()) {
288 val payloadId = CursorUtil.requireLong(entryCursor, MslRecipientTable.PAYLOAD_ID)
289
290 db.query(MslMessageTable.TABLE_NAME, null, "${MslMessageTable.PAYLOAD_ID} = ?", SqlUtil.buildArgs(payloadId), null, null, null).use { messageCursor ->
291 val messageIds: MutableList<MessageId> = mutableListOf()
292
293 while (messageCursor.moveToNext()) {
294 messageIds.add(
295 MessageId(
296 id = CursorUtil.requireLong(messageCursor, MslMessageTable.MESSAGE_ID)
297 )
298 )
299 }
300
301 return MessageLogEntry(
302 recipientId = RecipientId.from(CursorUtil.requireLong(entryCursor, MslRecipientTable.RECIPIENT_ID)),
303 dateSent = CursorUtil.requireLong(entryCursor, MslPayloadTable.DATE_SENT),
304 content = Content.ADAPTER.decode(CursorUtil.requireBlob(entryCursor, MslPayloadTable.CONTENT)),
305 contentHint = ContentHint.fromType(CursorUtil.requireInt(entryCursor, MslPayloadTable.CONTENT_HINT)),
306 urgent = entryCursor.requireBoolean(MslPayloadTable.URGENT),
307 relatedMessages = messageIds
308 )
309 }
310 }
311 }
312
313 return null
314 }
315
316 fun deleteAllRelatedToMessage(messageId: Long) {
317 val db = databaseHelper.signalWritableDatabase
318 val query = "${MslPayloadTable.ID} IN (SELECT ${MslMessageTable.PAYLOAD_ID} FROM ${MslMessageTable.TABLE_NAME} WHERE ${MslMessageTable.MESSAGE_ID} = ?)"
319 val args = SqlUtil.buildArgs(messageId)
320
321 db.delete(MslPayloadTable.TABLE_NAME, query, args)
322 }
323
324 fun deleteEntryForRecipient(dateSent: Long, recipientId: RecipientId, device: Int) {
325 deleteEntriesForRecipient(listOf(dateSent), recipientId, device)
326 }
327
328 fun deleteEntriesForRecipient(dateSent: List<Long>, recipientId: RecipientId, device: Int) {
329 val db = databaseHelper.signalWritableDatabase
330 db.beginTransaction()
331 try {
332 val query = """
333 DELETE FROM ${MslRecipientTable.TABLE_NAME} WHERE
334 ${MslRecipientTable.RECIPIENT_ID} = ? AND
335 ${MslRecipientTable.DEVICE} = ? AND
336 ${MslRecipientTable.PAYLOAD_ID} IN (
337 SELECT ${MslPayloadTable.ID}
338 FROM ${MslPayloadTable.TABLE_NAME}
339 WHERE ${MslPayloadTable.DATE_SENT} IN (${dateSent.joinToString(",")})
340 )
341 RETURNING ${MslRecipientTable.PAYLOAD_ID}"""
342 val args = SqlUtil.buildArgs(recipientId, device)
343
344 val payloadIds = db.rawQuery(query, args).readToList {
345 it.requireLong(MslRecipientTable.PAYLOAD_ID)
346 }
347
348 val queries = SqlUtil.buildCollectionQuery(MslPayloadTable.ID, payloadIds)
349 queries.forEach {
350 db.delete(MslPayloadTable.TABLE_NAME, "${it.where} AND ${MslPayloadTable.ID} NOT IN (SELECT ${MslRecipientTable.PAYLOAD_ID} FROM ${MslRecipientTable.TABLE_NAME})", it.whereArgs)
351 }
352 db.setTransactionSuccessful()
353 } finally {
354 db.endTransaction()
355 }
356 }
357
358 fun deleteAllForRecipient(recipientId: RecipientId) {
359 if (!FeatureFlags.retryReceipts()) return
360
361 writableDatabase
362 .delete(MslRecipientTable.TABLE_NAME)
363 .where("${MslRecipientTable.RECIPIENT_ID} = ?", recipientId)
364 .run()
365
366 writableDatabase
367 .delete(MslPayloadTable.TABLE_NAME)
368 .where("${MslPayloadTable.ID} NOT IN (SELECT ${MslRecipientTable.PAYLOAD_ID} FROM ${MslRecipientTable.TABLE_NAME})")
369 .run()
370 }
371
372 fun deleteAll() {
373 databaseHelper.signalWritableDatabase.delete(MslPayloadTable.TABLE_NAME, null, null)
374 }
375
376 fun trimOldMessages(currentTime: Long, maxAge: Long) {
377 val db = databaseHelper.signalWritableDatabase
378 val query = "${MslPayloadTable.DATE_SENT} < ?"
379 val args = SqlUtil.buildArgs(currentTime - maxAge)
380
381 db.delete(MslPayloadTable.TABLE_NAME, query, args)
382 }
383
384 override fun remapRecipient(oldRecipientId: RecipientId, newRecipientId: RecipientId) {
385 val values = ContentValues().apply {
386 put(MslRecipientTable.RECIPIENT_ID, newRecipientId.serialize())
387 }
388
389 val db = databaseHelper.signalWritableDatabase
390 val query = "${MslRecipientTable.RECIPIENT_ID} = ?"
391 val args = SqlUtil.buildArgs(oldRecipientId.serialize())
392
393 db.update(MslRecipientTable.TABLE_NAME, values, query, args)
394 }
395
396 private data class RecipientDevice(val recipientId: RecipientId, val devices: List<Int>)
397}