FeedbackRepository.kt

3.3 KB · raw

1package com.peachpasswords.feedback23import com.peachpasswords.BuildConfig4import kotlinx.coroutines.Dispatchers5import kotlinx.coroutines.withContext6import okhttp3.MediaType.Companion.toMediaType7import okhttp3.OkHttpClient8import okhttp3.Request9import okhttp3.RequestBody.Companion.toRequestBody10import org.json.JSONObject11import javax.inject.Inject12import javax.inject.Singleton1314/**15 * Submits user feedback through Web3Forms — the same inbox and the same access16 * key the browser extension posts to (`peach-browser/src/shared/config.ts`).17 * Android had no endpoint at all and said so on every attempt; there is no18 * reason for the two clients to reach different places.19 *20 * The access key is a client-side, write-only form identifier, not a secret.21 * It cannot read prior submissions or reach anything else, and the only abuse22 * it enables is spamming Peach's own feedback inbox — the same trade already23 * accepted for the extension bundle.24 *25 * The payload carries the category and the user-typed message only. No vault26 * data, keys, credentials, or device identifiers are included; the app version27 * rides in the subject line so a report can be placed without asking the user28 * who they are.29 */30class FeedbackUnavailableException : Exception("feedback_endpoint_not_configured")3132@Singleton33class FeedbackRepository @Inject constructor() {3435    private val client: OkHttpClient by lazy { OkHttpClient() }3637    suspend fun submit(type: String, message: String) {38        if (message.isBlank()) throw IllegalArgumentException("message_empty")39        if (WEB3FORMS_KEY.isBlank()) throw FeedbackUnavailableException()4041        withContext(Dispatchers.IO) {42            val body = buildSubmission(type, message).toRequestBody(JSON)43            val request = Request.Builder().url(ENDPOINT).post(body).build()44            client.newCall(request).execute().use { res ->45                val text = res.body?.string().orEmpty()46                if (!res.isSuccessful) throw java.io.IOException("HTTP ${res.code}")47                // Web3Forms answers 200 with {"success": false} when it rejects48                // a submission, so status alone would report a send that never49                // happened. Never tell the user it went if it did not.50                val accepted = runCatching { JSONObject(text).optBoolean("success") }51                    .getOrDefault(false)52                if (!accepted) throw java.io.IOException("feedback_rejected")53            }54        }55    }5657    companion object {58        const val ENDPOINT = "https://api.web3forms.com/submit"5960        /** Mirrors `WEB3FORMS_KEY` in the browser's `src/shared/config.ts`. */61        const val WEB3FORMS_KEY = "af0b7072-e3fa-4cb3-99b5-4ad3b87fac66"6263        private val JSON = "application/json; charset=utf-8".toMediaType()6465        internal fun buildSubmission(66            type: String,67            message: String,68            version: String = BuildConfig.VERSION_NAME,69        ): String = JSONObject().apply {70            put("access_key", WEB3FORMS_KEY)71            put("subject", "Peach Feedback - $type (Android $version)")72            put("message", message)73            put("from_name", "Peach User")74            // Honeypot — Web3Forms drops the submission if this arrives filled.75            put("botcheck", "")76        }.toString()77    }78}