send path uses the device directory, not the prekey route — every send burned bundle quota and popped OPKs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-28 15:18:26 -04:00
co-authored by Claude Opus 4.8
parent cf3f43fdbf
commit 7727cfa432
@@ -378,7 +378,33 @@ class Conversations(
}
private fun sendPlaintext(toUserId: Long, text: String): Sent {
val bundles = relay.fetchKeys(toUserId)
// 🔴 **The DIRECTORY, not the key route.** `/e2e/keys` pops a one-time
// prekey per device and is metered per contact pair — calling it on
// EVERY send drained both, and an hour of ordinary chatting ended in
// "Too many attempts". The directory costs nothing; full bundles are
// fetched only when some device has no session yet, which is the one
// moment X3DH actually needs them.
val directory = relay.listDevices(toUserId)
val stubs = (0 until directory.length()).map { i ->
val o = directory.getJSONObject(i)
PeerBundle(
deviceId = o.optString("device_id"),
identityDH = runCatching { Wire.unb64(o.optString("ik_dh")) }
.getOrDefault(ByteArray(0)),
identitySigning = runCatching { Wire.unb64(o.optString("ik_sig")) }
.getOrDefault(ByteArray(0)),
signedPreKeyId = 0,
signedPreKey = ByteArray(0),
signedPreKeySignature = ByteArray(0),
oneTimePreKeyId = 0,
oneTimePreKey = null,
proto = o.optInt("proto", 1),
)
}
val needsEstablish = stubs.any {
it.deviceId != identity.deviceId && existingSession(it.deviceId) == null
}
val bundles = if (needsEstablish) relay.fetchKeys(toUserId) else stubs
guardIdentity(toUserId, bundles)
val delivered = mutableListOf<String>()
val failed = LinkedHashMap<String, String>()