diff --git a/client/src/main/kotlin/io/flamenet/client/Conversations.kt b/client/src/main/kotlin/io/flamenet/client/Conversations.kt index ea62792..1b5ee14 100644 --- a/client/src/main/kotlin/io/flamenet/client/Conversations.kt +++ b/client/src/main/kotlin/io/flamenet/client/Conversations.kt @@ -532,6 +532,28 @@ class Conversations( fun sendPlaintextForGroupInvite(toUserId: Long, text: String): Sent = sendPlaintext(toUserId, text) + /** + * Whether an envelope is worth interrupting somebody for. + * + * ๐Ÿ”ด **Everything this app sends was.** The relay holds ciphertext and + * cannot tell a call answer from a birthday message, so it announced every + * envelope as "New message" โ€” and call answers, call ends, delivery keys, + * key-log gossip, reactions and avatars are all envelopes. One call + * produced a burst of banners on the far end for things nobody sent. + * + * โš ๏ธ A plain string is a message: text from an older build carries no + * envelope at all, and the safe reading of something unrecognised is that a + * person meant to say it. + */ + private fun warrantsAlert(body: String): Boolean { + if (!body.startsWith("""{"fnc":""")) return true + val content = Content.parse(body) + // Only the OFFER announces itself. An answer and a hang-up are the far + // end's half of a conversation already on screen. + content.call?.let { return it.action == Content.CallSignal.OFFER } + return content.isRenderable + } + private fun sendPlaintext(toUserId: Long, text: String): Sent { // ๐Ÿ”ด **The DIRECTORY, not the key route.** `/e2e/keys` pops a one-time // prekey per device and is metered per contact pair โ€” calling it on @@ -696,6 +718,7 @@ class Conversations( try { relay.sendEnvelopes( toUserId, outgoing, identity.deviceId, + silent = !warrantsAlert(text), // โš ๏ธ The key only when there is no capability. Sending both // would let the relay accept on the key when the capability // had been revoked, so it would never learn it was refused diff --git a/client/src/main/kotlin/io/flamenet/client/RelayClient.kt b/client/src/main/kotlin/io/flamenet/client/RelayClient.kt index a171457..61e7ca4 100644 --- a/client/src/main/kotlin/io/flamenet/client/RelayClient.kt +++ b/client/src/main/kotlin/io/flamenet/client/RelayClient.kt @@ -217,6 +217,19 @@ class RelayClient( * revocation is the whole lever ยง21 adds. */ deliveryToken: String? = null, + /** + * Deliver without announcing: protocol traffic, not a message. + * + * ๐Ÿ”ด **The relay announced everything, because it cannot tell.** It + * holds ciphertext, so a call answer, a delivery key and a birthday + * message are the same bytes to it โ€” and every one of them raised + * "New message" on the far end. One call produced a burst of banners + * for things nobody sent. The sender is the only party that knows. + * + * โš ๏ธ Advisory and unauthenticated. A sender that lies suppresses its + * own notifications and nobody else's. + */ + silent: Boolean = false, ): JSONObject = postJson("/e2e/messages", JSONObject() .put("to", toUser) @@ -224,6 +237,7 @@ class RelayClient( if (deliveryToken != null) body.put("delivery_token", deliveryToken) else if (deliveryKey != null) body.put("delivery_key", deliveryKey) else body.put("from_device", fromDevice) + if (silent) body.put("silent", true) } .put("messages", JSONArray().also { array -> envelopes.forEach {