A suspended account is told so

The relay can now suspend an account, and it refuses one with a single
sentence at every door. That sentence has to reach the person: nothing
else they do will work until the suspension is lifted, and "that isn't
allowed on this server" sends them reinstalling. A 403 whose body names
account_suspended shows the relay's sentence; every other 403 reads as
before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-15 09:02:04 -04:00
co-authored by Claude Fable 5.1
parent 3932e43153
commit a8ec0bab63
2 changed files with 41 additions and 2 deletions
@@ -4202,8 +4202,19 @@ internal fun describeFailure(failure: Throwable): String =
// Say what the relay said.
403 -> {
failure.body
.takeIf { it.isNotBlank() && !it.trimStart().startsWith("{") && " " in it }
// A suspension is the one 403 that carries a sentence the
// person needs to read, because nothing else they do will
// work until it is lifted, and a generic refusal sends them
// reinstalling.
val suspended =
runCatching { org.json.JSONObject(failure.body) }
.getOrNull()
?.takeIf { it.optString("code") == "account_suspended" }
?.optString("message")
?.takeIf { it.isNotBlank() }
suspended
?: failure.body
.takeIf { it.isNotBlank() && !it.trimStart().startsWith("{") && " " in it }
?: "That isn't allowed on this server."
}
@@ -0,0 +1,28 @@
package io.flamenet.messenger.app
import io.flamenet.client.RelayException
import kotlin.test.Test
import kotlin.test.assertTrue
/**
* The relay refuses a suspended account with one sentence at every door. It
* has to reach the person, because nothing else they do will work until the
* suspension is lifted, and "that isn't allowed" sends them reinstalling.
*/
class ASuspendedAccountIsToldSoTest {
private val body =
"""{"code":"account_suspended","message":"This account is suspended by the people who run this server. Nothing can be sent or read with it until they lift that.","data":{"status":403}}"""
@Test
fun `the relay's sentence is shown, not a generic refusal`() {
val shown = describeFailure(RelayException.Http(403, body))
assertTrue(shown.contains("suspended"), "shown: $shown")
assertTrue(!shown.contains("isn't allowed"), "fell through to the generic 403: $shown")
}
@Test
fun `any other 403 still reads as before`() {
val shown = describeFailure(RelayException.Http(403, """{"code":"forbidden","message":"no"}"""))
assertTrue(shown.contains("isn't allowed"), "shown: $shown")
}
}