A story reaches this account's own devices, so reactions have somewhere to land
Posting sent the story to the audience and saved it locally. Nothing went to this account's other devices, so a story posted on the phone did not exist on the tablet at all. Reactions do go to every device the author has. The other device received all of them and had no story to attach them to, so they sat in a map nothing reads and the person watching saw a story drawing no response. The only way out anybody found was to stop using the second device. Each posted story is now mirrored to this account as well, carrying the same story key so one story stays one story across the devices. Best effort and outside the audience count: an account with one device has nowhere to send it and that is still a successful post. On the way in, a story from this account is filed as this account's own, and only from a proven sender, or an unproven envelope claiming our id could put a stranger's story on our own strip and show us its reactions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
13d49fed8e
commit
38e31fc5ee
@@ -345,6 +345,22 @@ class Conversations(
|
||||
return Sent(delivered, failed, attachment = ref)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a story this account just posted to its OWN other devices.
|
||||
*
|
||||
* The audience loop reaches other people. Nothing reached the tablet in
|
||||
* the next room, so a story posted here did not exist there at all, while
|
||||
* REACTIONS to it did: a reaction is addressed to every device the author
|
||||
* has. The second device received every reaction and had no story to show
|
||||
* them against, so they sat in a map nothing reads and the person saw
|
||||
* silence. Best effort, an account with one device has nowhere to send
|
||||
* and that is not a failed post.
|
||||
*/
|
||||
private fun mirrorToOwnDevices(body: String) {
|
||||
val me = myUserId ?: return
|
||||
runCatching { sendPlaintext(me, body) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a story to several people at once (§6.6).
|
||||
*/
|
||||
@@ -388,6 +404,7 @@ class Conversations(
|
||||
for (userId in toUserIds) {
|
||||
if (sendPlaintext(userId, body).reachedSomeone) reached++ else missed++
|
||||
}
|
||||
mirrorToOwnDevices(body)
|
||||
return StoryPosted(ref, reached, missed)
|
||||
}
|
||||
|
||||
@@ -418,6 +435,7 @@ class Conversations(
|
||||
for (userId in toUserIds) {
|
||||
if (sendPlaintext(userId, body).reachedSomeone) reached++ else missed++
|
||||
}
|
||||
mirrorToOwnDevices(body)
|
||||
return TextStoryPosted(key, expires, reached, missed)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package io.flamenet.client
|
||||
|
||||
import io.flamenet.e2e.DeviceIdentity
|
||||
import io.flamenet.e2e.Vault
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import javax.crypto.KeyGenerator
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A story has to reach this account's OWN other devices.
|
||||
*
|
||||
* It did not. The audience loop reached other people and the story was then
|
||||
* saved locally, so a second device never learned the story existed. Reactions
|
||||
* are addressed to every device the author has, so that device received all of
|
||||
* them and had nothing to show them against. What the person saw was a story
|
||||
* that drew no reactions, and the only way out they found was to stop using
|
||||
* the other device.
|
||||
*/
|
||||
class StoryReachesOwnDevicesTest {
|
||||
private class MapStorage : Vault.Storage {
|
||||
private 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 lateinit var server: MockWebServer
|
||||
private lateinit var vault: Vault
|
||||
private lateinit var me: DeviceIdentity
|
||||
|
||||
@BeforeTest fun start() {
|
||||
server = MockWebServer().also { it.start() }
|
||||
vault =
|
||||
Vault(
|
||||
MapStorage(),
|
||||
KeyGenerator.getInstance("AES").apply { init(256) }.generateKey(),
|
||||
)
|
||||
me = DeviceIdentity.create()
|
||||
}
|
||||
|
||||
@AfterTest fun stop() = server.shutdown()
|
||||
|
||||
private fun emptyDirectory() =
|
||||
MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody("""{"devices":[]}""")
|
||||
|
||||
@Test
|
||||
fun `posting a text story addresses this account's own devices`() {
|
||||
val conversations =
|
||||
Conversations(me, vault, RelayClient(server.url("/").toString(), "a", "b"))
|
||||
.also { it.myUserId = 4242 }
|
||||
// One for the audience, one for the mirror.
|
||||
server.enqueue(emptyDirectory())
|
||||
server.enqueue(emptyDirectory())
|
||||
|
||||
conversations.postTextStory("posted from the phone", listOf(99L))
|
||||
|
||||
val asked = generateSequence { server.takeRequest(1, java.util.concurrent.TimeUnit.SECONDS) }
|
||||
.map { it.path.orEmpty() }
|
||||
.toList()
|
||||
assertTrue(
|
||||
asked.any { it.endsWith("/e2e/devices/4242") },
|
||||
"the story never asked for this account's own devices, it reached $asked",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an account that has not said who it is mirrors nothing`() {
|
||||
val conversations =
|
||||
Conversations(me, vault, RelayClient(server.url("/").toString(), "a", "b"))
|
||||
server.enqueue(emptyDirectory())
|
||||
|
||||
conversations.postTextStory("posted from the phone", listOf(99L))
|
||||
|
||||
val asked = generateSequence { server.takeRequest(1, java.util.concurrent.TimeUnit.SECONDS) }
|
||||
.map { it.path.orEmpty() }
|
||||
.toList()
|
||||
assertTrue(asked.size == 1, "an unknown account id sent somewhere, it reached $asked")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user