Say which envelopes are worth interrupting somebody for
The relay announced every envelope as "New message" because it holds
ciphertext and cannot tell. It now takes `silent` from the sender.
Marked silent: call answers, call ends, delivery keys, key-log gossip,
reactions, avatars — everything that is a layer talking to a layer. A
call offer still announces itself, and so does anything a person would
see in the thread. `Content.isRenderable` already drew exactly this line
for the receive side; this is the same question asked on the way out.
⚠️ 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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user