diff --git a/app/build.gradle b/app/build.gradle
index a832c262..62517be7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -83,6 +83,7 @@ android {
jvmTarget = '1.8'
}
buildFeatures {
+ viewBinding true
compose true
}
composeOptions {
@@ -163,6 +164,9 @@ dependencies {
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
implementation "androidx.datastore:datastore:1.1.0-alpha01"
+ // Libraries used for legacy View-based UI
+ implementation "androidx.constraintlayout:constraintlayout:2.2.0-alpha09"
+
// JDK Desugaring - polyfill for new Java APIs
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 76545d94..189201d8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,10 +19,10 @@
+
@@ -30,6 +30,9 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/activities/WebChallengeActivity.kt b/app/src/main/java/chat/revolt/activities/WebChallengeActivity.kt
new file mode 100644
index 00000000..fdc8e30e
--- /dev/null
+++ b/app/src/main/java/chat/revolt/activities/WebChallengeActivity.kt
@@ -0,0 +1,47 @@
+package chat.revolt.activities
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.appcompat.app.AppCompatActivity
+import chat.revolt.api.REVOLT_BASE
+import chat.revolt.api.buildUserAgent
+import chat.revolt.databinding.ActivityWebchallengeBinding
+
+private class WebChallengeClient(val pageLoaded: () -> Unit) : WebViewClient() {
+ @Override
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+ pageLoaded()
+ }
+}
+
+class WebChallengeActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityWebchallengeBinding
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityWebchallengeBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.webView.settings.apply {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ userAgentString = buildUserAgent("WebChallenge")
+ }
+
+ binding.webView.webViewClient = WebChallengeClient {
+ binding.webView.evaluateJavascript(
+ "(function() { return document.getElementById('cf-wrapper') != null; })();"
+ ) { result ->
+ if (result == "false") { // No challenge
+ finish()
+ }
+ }
+ }
+
+ binding.webView.loadUrl(REVOLT_BASE)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt
index 8dafbca4..94db07b0 100644
--- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt
+++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt
@@ -41,6 +41,10 @@ fun asJanuaryProxyUrl(url: String): String {
return "$REVOLT_JANUARY/proxy?url=${url}"
}
+fun buildUserAgent(accessMethod: String = "Ktor"): String {
+ return "$accessMethod RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
+}
+
private const val BACKEND_IS_STABLE = false
val RevoltJson = Json { ignoreUnknownKeys = true }
@@ -70,10 +74,7 @@ val RevoltHttp = HttpClient(OkHttp) {
defaultRequest {
url(REVOLT_BASE)
- header(
- "User-Agent",
- "Ktor RevoltAndroid/${BuildConfig.VERSION_NAME} (Android ${android.os.Build.VERSION.SDK_INT}; ${android.os.Build.MANUFACTURER} ${android.os.Build.DEVICE})"
- )
+ header("User-Agent", buildUserAgent())
}
}
diff --git a/app/src/main/java/chat/revolt/api/internals/WebChallenge.kt b/app/src/main/java/chat/revolt/api/internals/WebChallenge.kt
new file mode 100644
index 00000000..f74f3cb1
--- /dev/null
+++ b/app/src/main/java/chat/revolt/api/internals/WebChallenge.kt
@@ -0,0 +1,15 @@
+package chat.revolt.api.internals
+
+import chat.revolt.api.REVOLT_BASE
+import chat.revolt.api.RevoltHttp
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+
+object WebChallenge {
+ suspend fun needsCloudflare(): Boolean {
+ RevoltHttp.get(REVOLT_BASE).let {
+ val text = it.bodyAsText()
+ return text.contains("window._cf_chl_opt") // FIXME Naive, prone to captcha page changing
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/SplashScreen.kt b/app/src/main/java/chat/revolt/screens/SplashScreen.kt
index 6453175c..a6c5408a 100644
--- a/app/src/main/java/chat/revolt/screens/SplashScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/SplashScreen.kt
@@ -2,8 +2,10 @@ package chat.revolt.screens
import android.annotation.SuppressLint
import android.content.Context
+import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
+import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -11,6 +13,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -18,7 +21,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import chat.revolt.R
+import chat.revolt.activities.WebChallengeActivity
import chat.revolt.api.RevoltAPI
+import chat.revolt.api.internals.WebChallenge
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.screens.splash.DisconnectedScreen
@@ -70,11 +75,19 @@ class SplashScreenViewModel @Inject constructor(
}
fun checkLoggedInState() {
+ Log.d("SplashScreenViewModel", "Checking logged in state")
viewModelScope.launch {
setIsConnected(hasInternetConnection())
if (!isConnected) return@launch
+ val needsCloudflare = WebChallenge.needsCloudflare()
+
+ if (needsCloudflare) {
+ setNavigateTo("webchallenge")
+ return@launch
+ }
+
val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login")
val valid = RevoltAPI.checkSessionToken(token)
@@ -107,6 +120,8 @@ fun SplashScreen(
navController: NavController,
viewModel: SplashScreenViewModel = hiltViewModel()
) {
+ val context = LocalContext.current
+
if (!viewModel.isConnected) {
DisconnectedScreen(
onRetry = {
@@ -143,6 +158,17 @@ fun SplashScreen(
}
}
}
+
+ "webchallenge" -> {
+ context.startActivity(
+ Intent(
+ context,
+ WebChallengeActivity::class.java
+ )
+ )
+ viewModel.checkLoggedInState()
+ }
+
"home" -> {
navController.navigate("chat") {
popUpTo("splash") {
diff --git a/app/src/main/res/layout/activity_webchallenge.xml b/app/src/main/res/layout/activity_webchallenge.xml
new file mode 100644
index 00000000..7636ffbd
--- /dev/null
+++ b/app/src/main/res/layout/activity_webchallenge.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file