Merge branch 'fix/off-the-record-outbox' into 'main'
Keep off-the-record messages out of the outbox on disk See merge request paulhitt/flamenet-messenger-android!143
This commit was merged in pull request #170.
This commit is contained in:
@@ -1304,7 +1304,33 @@ class Vault(
|
|||||||
*/
|
*/
|
||||||
fun outbox(): JSONArray = readJson(OUTBOX)?.optJSONArray("pending") ?: JSONArray()
|
fun outbox(): JSONArray = readJson(OUTBOX)?.optJSONArray("pending") ?: JSONArray()
|
||||||
|
|
||||||
fun saveOutbox(pending: JSONArray) = writeJson(OUTBOX, JSONObject().put("pending", pending))
|
/**
|
||||||
|
* Write the queue, minus anything for an off-the-record conversation.
|
||||||
|
*
|
||||||
|
* An entry carries `to` and `text`, so this is the message and not a note
|
||||||
|
* about it. Every enqueue writes here directly, so an off-the-record send
|
||||||
|
* queued for later reached disk in plaintext the moment it was typed, for
|
||||||
|
* a conversation §6.28 says is gone when the app closes.
|
||||||
|
*
|
||||||
|
* The filter is here rather than at the four call sites for the same
|
||||||
|
* reason it is on [saveUnreadThreads]: a later caller cannot forget it.
|
||||||
|
* A group entry carries `thread` and no user-thread setting, so it passes
|
||||||
|
* through.
|
||||||
|
*
|
||||||
|
* Dropping a queued send is the promise, not the cost of it. The thread
|
||||||
|
* it belongs to is not kept either, so a message that survived would
|
||||||
|
* arrive into a conversation the app no longer has.
|
||||||
|
*/
|
||||||
|
fun saveOutbox(pending: JSONArray) {
|
||||||
|
val kept = JSONArray()
|
||||||
|
for (i in 0 until pending.length()) {
|
||||||
|
val entry = pending.optJSONObject(i) ?: continue
|
||||||
|
val to = entry.optLong("to", 0L)
|
||||||
|
if (to != 0L && offTheRecord(threadForUser(to))) continue
|
||||||
|
kept.put(entry)
|
||||||
|
}
|
||||||
|
writeJson(OUTBOX, JSONObject().put("pending", kept))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls placed and received, newest last.
|
* Calls placed and received, newest last.
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package io.flamenet.e2e
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queued send for an off-the-record conversation never reaches the vault.
|
||||||
|
*
|
||||||
|
* This is the message, not a note about it: an entry carries `to` and `text`.
|
||||||
|
* Every enqueue writes the whole queue back, so an off-the-record send queued
|
||||||
|
* for later was on disk in plaintext the moment it was typed.
|
||||||
|
*/
|
||||||
|
class OffTheRecordOutboxTest {
|
||||||
|
private class MapStorage : Vault.Storage {
|
||||||
|
val records = LinkedHashMap<String, ByteArray>()
|
||||||
|
|
||||||
|
override fun read(name: String) = records[name]
|
||||||
|
|
||||||
|
override fun write(
|
||||||
|
name: String,
|
||||||
|
bytes: ByteArray,
|
||||||
|
) {
|
||||||
|
records[name] = bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(name: String) {
|
||||||
|
records.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun names() = records.keys.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun key(): SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||||
|
|
||||||
|
private fun entry(
|
||||||
|
to: Long,
|
||||||
|
text: String,
|
||||||
|
) = JSONObject().put("to", to).put("text", text).put("at", 1L)
|
||||||
|
|
||||||
|
private fun texts(array: JSONArray): List<String> =
|
||||||
|
(0 until array.length()).mapNotNull { array.optJSONObject(it)?.optString("text") }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a send queued in an off the record thread is not written`() {
|
||||||
|
val storage = MapStorage()
|
||||||
|
val k = key()
|
||||||
|
Vault(storage, k).apply {
|
||||||
|
saveOffTheRecord(Vault.threadForUser(7L), true)
|
||||||
|
saveOutbox(JSONArray().put(entry(7L, "meet me at six")).put(entry(8L, "ordinary")))
|
||||||
|
}
|
||||||
|
|
||||||
|
val restored = texts(Vault(storage, k).outbox())
|
||||||
|
assertFalse("meet me at six" in restored, "the off-the-record message text was written to the vault")
|
||||||
|
assertTrue("ordinary" in restored)
|
||||||
|
assertEquals(1, restored.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `nothing is dropped when no thread is off the record`() {
|
||||||
|
val storage = MapStorage()
|
||||||
|
val k = key()
|
||||||
|
Vault(storage, k).saveOutbox(JSONArray().put(entry(7L, "one")).put(entry(8L, "two")))
|
||||||
|
assertEquals(listOf("one", "two"), texts(Vault(storage, k).outbox()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group entry names the group thread and carries the member it is going
|
||||||
|
* to, and a group has no off-the-record setting of its own.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `a group entry passes through`() {
|
||||||
|
val storage = MapStorage()
|
||||||
|
val k = key()
|
||||||
|
Vault(storage, k).saveOutbox(
|
||||||
|
JSONArray().put(entry(8L, "to the group").put("thread", "group:g-1")),
|
||||||
|
)
|
||||||
|
assertEquals(listOf("to the group"), texts(Vault(storage, k).outbox()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turning it on does not reach back into what was already written, but the
|
||||||
|
* next save must drop it: the queue is rewritten whole every time.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `the next save after turning it on clears the queue`() {
|
||||||
|
val storage = MapStorage()
|
||||||
|
val k = key()
|
||||||
|
val v = Vault(storage, k)
|
||||||
|
v.saveOutbox(JSONArray().put(entry(7L, "still here")))
|
||||||
|
assertEquals(listOf("still here"), texts(Vault(storage, k).outbox()))
|
||||||
|
|
||||||
|
v.saveOffTheRecord(Vault.threadForUser(7L), true)
|
||||||
|
v.saveOutbox(v.outbox())
|
||||||
|
assertTrue(texts(Vault(storage, k).outbox()).isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user