mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Clean up interceptors a bit
This commit is contained in:
		@@ -17,12 +17,13 @@ class NetworkHelper(context: Context) {
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val cacheDir = File(context.cacheDir, "network_cache")
 | 
			
		||||
 | 
			
		||||
    private val cacheSize = 5L * 1024 * 1024 // 5 MiB
 | 
			
		||||
 | 
			
		||||
    val cookieManager = AndroidCookieJar()
 | 
			
		||||
 | 
			
		||||
    private val http103Interceptor = Http103Interceptor(context)
 | 
			
		||||
    private val userAgentInterceptor by lazy { UserAgentInterceptor() }
 | 
			
		||||
    private val http103Interceptor by lazy { Http103Interceptor(context) }
 | 
			
		||||
    private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) }
 | 
			
		||||
 | 
			
		||||
    private val baseClientBuilder: OkHttpClient.Builder
 | 
			
		||||
        get() {
 | 
			
		||||
@@ -32,7 +33,7 @@ class NetworkHelper(context: Context) {
 | 
			
		||||
                .readTimeout(30, TimeUnit.SECONDS)
 | 
			
		||||
                .callTimeout(2, TimeUnit.MINUTES)
 | 
			
		||||
                // .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
 | 
			
		||||
                .addInterceptor(UserAgentInterceptor())
 | 
			
		||||
                .addInterceptor(userAgentInterceptor)
 | 
			
		||||
                .addNetworkInterceptor(http103Interceptor)
 | 
			
		||||
 | 
			
		||||
            if (preferences.verboseLogging()) {
 | 
			
		||||
@@ -64,7 +65,7 @@ class NetworkHelper(context: Context) {
 | 
			
		||||
    @Suppress("UNUSED")
 | 
			
		||||
    val cloudflareClient by lazy {
 | 
			
		||||
        client.newBuilder()
 | 
			
		||||
            .addInterceptor(CloudflareInterceptor(context))
 | 
			
		||||
            .addInterceptor(cloudflareInterceptor)
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,17 +2,12 @@ package eu.kanade.tachiyomi.network.interceptor
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchUI
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.isOutdated
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
@@ -26,56 +21,26 @@ import java.io.IOException
 | 
			
		||||
import java.util.concurrent.CountDownLatch
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) {
 | 
			
		||||
 | 
			
		||||
    private val executor = ContextCompat.getMainExecutor(context)
 | 
			
		||||
 | 
			
		||||
    private val networkHelper: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
 | 
			
		||||
     * blocking the main thread too much. If used too often we could consider moving it to the
 | 
			
		||||
     * Application class.
 | 
			
		||||
     */
 | 
			
		||||
    private val initWebView by lazy {
 | 
			
		||||
        // Crashes on some devices. We skip this in some cases since the only impact is slower
 | 
			
		||||
        // WebView init in those rare cases.
 | 
			
		||||
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
 | 
			
		||||
        if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
 | 
			
		||||
            return@lazy
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        WebSettings.getDefaultUserAgent(context)
 | 
			
		||||
    override fun shouldIntercept(response: Response): Boolean {
 | 
			
		||||
        // Check if Cloudflare anti-bot is on
 | 
			
		||||
        return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val originalRequest = chain.request()
 | 
			
		||||
 | 
			
		||||
        if (!WebViewUtil.supportsWebView(context)) {
 | 
			
		||||
            launchUI {
 | 
			
		||||
                context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
 | 
			
		||||
            }
 | 
			
		||||
            return chain.proceed(originalRequest)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        initWebView
 | 
			
		||||
 | 
			
		||||
        val response = chain.proceed(originalRequest)
 | 
			
		||||
 | 
			
		||||
        // Check if Cloudflare anti-bot is on
 | 
			
		||||
        if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
 | 
			
		||||
            return response
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
 | 
			
		||||
        try {
 | 
			
		||||
            response.close()
 | 
			
		||||
            networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
 | 
			
		||||
            val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
 | 
			
		||||
            networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0)
 | 
			
		||||
            val oldCookie = networkHelper.cookieManager.get(request.url)
 | 
			
		||||
                .firstOrNull { it.name == "cf_clearance" }
 | 
			
		||||
            resolveWithWebView(originalRequest, oldCookie)
 | 
			
		||||
            resolveWithWebView(request, oldCookie)
 | 
			
		||||
 | 
			
		||||
            return chain.proceed(originalRequest)
 | 
			
		||||
            return chain.proceed(request)
 | 
			
		||||
        }
 | 
			
		||||
        // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
 | 
			
		||||
        // we don't crash the entire app
 | 
			
		||||
@@ -87,7 +52,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("SetJavaScriptEnabled")
 | 
			
		||||
    private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
 | 
			
		||||
    private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) {
 | 
			
		||||
        // We need to lock this thread until the WebView finds the challenge solution url, because
 | 
			
		||||
        // OkHttp doesn't support asynchronous interceptors.
 | 
			
		||||
        val latch = CountDownLatch(1)
 | 
			
		||||
@@ -98,8 +63,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
        var cloudflareBypassed = false
 | 
			
		||||
        var isWebViewOutdated = false
 | 
			
		||||
 | 
			
		||||
        val origRequestUrl = request.url.toString()
 | 
			
		||||
        val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
 | 
			
		||||
        val origRequestUrl = originalRequest.url.toString()
 | 
			
		||||
        val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
 | 
			
		||||
 | 
			
		||||
        executor.execute {
 | 
			
		||||
            val webview = WebView(context)
 | 
			
		||||
@@ -107,7 +72,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
            webview.setDefaultSettings()
 | 
			
		||||
 | 
			
		||||
            // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
 | 
			
		||||
            webview.settings.userAgentString = request.header("User-Agent")
 | 
			
		||||
            webview.settings.userAgentString = originalRequest.header("User-Agent")
 | 
			
		||||
                ?: networkHelper.defaultUserAgent
 | 
			
		||||
 | 
			
		||||
            webview.webViewClient = object : WebViewClientCompat() {
 | 
			
		||||
@@ -175,12 +140,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
            throw CloudflareBypassException()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val ERROR_CODES = listOf(403, 503)
 | 
			
		||||
        private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
 | 
			
		||||
        private val COOKIE_NAMES = listOf("cf_clearance")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val ERROR_CODES = listOf(403, 503)
 | 
			
		||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
 | 
			
		||||
private val COOKIE_NAMES = listOf("cf_clearance")
 | 
			
		||||
 | 
			
		||||
private class CloudflareBypassException : Exception()
 | 
			
		||||
 
 | 
			
		||||
@@ -2,67 +2,31 @@ package eu.kanade.tachiyomi.network.interceptor
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.webkit.JavascriptInterface
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchUI
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.logcat
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.MediaType.Companion.toMediaType
 | 
			
		||||
import okhttp3.Protocol
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okhttp3.ResponseBody.Companion.toResponseBody
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.util.concurrent.CountDownLatch
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
// TODO: Remove when OkHttp can handle http 103 responses
 | 
			
		||||
class Http103Interceptor(private val context: Context) : Interceptor {
 | 
			
		||||
// TODO: Remove when OkHttp can handle HTTP 103 responses
 | 
			
		||||
class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
 | 
			
		||||
 | 
			
		||||
    private val executor = ContextCompat.getMainExecutor(context)
 | 
			
		||||
 | 
			
		||||
    private val networkHelper: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
 | 
			
		||||
     * blocking the main thread too much. If used too often we could consider moving it to the
 | 
			
		||||
     * Application class.
 | 
			
		||||
     */
 | 
			
		||||
    private val initWebView by lazy {
 | 
			
		||||
        // Crashes on some devices. We skip this in some cases since the only impact is slower
 | 
			
		||||
        // WebView init in those rare cases.
 | 
			
		||||
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
 | 
			
		||||
        if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
 | 
			
		||||
            return@lazy
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        WebSettings.getDefaultUserAgent(context)
 | 
			
		||||
    override fun shouldIntercept(response: Response): Boolean {
 | 
			
		||||
        return response.code == 103
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val request = chain.request()
 | 
			
		||||
        val response = chain.proceed(request)
 | 
			
		||||
        if (response.code != 103) return response
 | 
			
		||||
        if (!WebViewUtil.supportsWebView(context)) {
 | 
			
		||||
            launchUI {
 | 
			
		||||
                context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
 | 
			
		||||
            }
 | 
			
		||||
            return response
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        initWebView
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
 | 
			
		||||
        logcat { "Proceeding with WebView for request $request" }
 | 
			
		||||
        try {
 | 
			
		||||
            return proceedWithWebView(request, response)
 | 
			
		||||
@@ -71,23 +35,9 @@ class Http103Interceptor(private val context: Context) : Interceptor {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal class JsInterface(private val latch: CountDownLatch, var payload: String? = null) {
 | 
			
		||||
        @JavascriptInterface
 | 
			
		||||
        fun passPayload(passedPayload: String) {
 | 
			
		||||
            payload = passedPayload
 | 
			
		||||
            latch.countDown()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
 | 
			
		||||
 | 
			
		||||
        val htmlMediaType = "text/html".toMediaType()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
 | 
			
		||||
    private fun proceedWithWebView(ogRequest: Request, ogResponse: Response): Response {
 | 
			
		||||
        // We need to lock this thread until the WebView finds the challenge solution url, because
 | 
			
		||||
    private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
 | 
			
		||||
        // We need to lock this thread until the WebView loads the page, because
 | 
			
		||||
        // OkHttp doesn't support asynchronous interceptors.
 | 
			
		||||
        val latch = CountDownLatch(1)
 | 
			
		||||
 | 
			
		||||
@@ -97,16 +47,11 @@ class Http103Interceptor(private val context: Context) : Interceptor {
 | 
			
		||||
 | 
			
		||||
        var exception: Exception? = null
 | 
			
		||||
 | 
			
		||||
        val requestUrl = ogRequest.url.toString()
 | 
			
		||||
        val headers = ogRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
 | 
			
		||||
        val requestUrl = originalRequest.url.toString()
 | 
			
		||||
        val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
 | 
			
		||||
 | 
			
		||||
        executor.execute {
 | 
			
		||||
            val webview = WebView(context).also { outerWebView = it }
 | 
			
		||||
            with(webview.settings) {
 | 
			
		||||
                javaScriptEnabled = true
 | 
			
		||||
                userAgentString = ogRequest.header("User-Agent") ?: networkHelper.defaultUserAgent
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val webview = createWebView(originalRequest).also { outerWebView = it }
 | 
			
		||||
            webview.addJavascriptInterface(jsInterface, "android")
 | 
			
		||||
 | 
			
		||||
            webview.webViewClient = object : WebViewClientCompat() {
 | 
			
		||||
@@ -143,13 +88,25 @@ class Http103Interceptor(private val context: Context) : Interceptor {
 | 
			
		||||
 | 
			
		||||
        exception?.let { throw it }
 | 
			
		||||
 | 
			
		||||
        val payload = jsInterface.payload ?: throw Exception("Couldn't fetch site through webview")
 | 
			
		||||
        val responseHtml = jsInterface.responseHtml ?: throw Exception("Couldn't fetch site through webview")
 | 
			
		||||
 | 
			
		||||
        return ogResponse.newBuilder()
 | 
			
		||||
        return originalResponse.newBuilder()
 | 
			
		||||
            .code(200)
 | 
			
		||||
            .protocol(Protocol.HTTP_1_1)
 | 
			
		||||
            .message("OK")
 | 
			
		||||
            .body(payload.toResponseBody(htmlMediaType))
 | 
			
		||||
            .body(responseHtml.toResponseBody(htmlMediaType))
 | 
			
		||||
            .build()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal class JsInterface(private val latch: CountDownLatch, var responseHtml: String? = null) {
 | 
			
		||||
    @Suppress("UNUSED")
 | 
			
		||||
    @JavascriptInterface
 | 
			
		||||
    fun passPayload(passedPayload: String) {
 | 
			
		||||
        responseHtml = passedPayload
 | 
			
		||||
        latch.countDown()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
 | 
			
		||||
private val htmlMediaType = "text/html".toMediaType()
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
package eu.kanade.tachiyomi.network.interceptor
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import android.webkit.WebView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.launchUI
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.toast
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
 | 
			
		||||
 | 
			
		||||
    private val networkHelper: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
 | 
			
		||||
     * blocking the main thread too much. If used too often we could consider moving it to the
 | 
			
		||||
     * Application class.
 | 
			
		||||
     */
 | 
			
		||||
    private val initWebView by lazy {
 | 
			
		||||
        // Crashes on some devices. We skip this in some cases since the only impact is slower
 | 
			
		||||
        // WebView init in those rare cases.
 | 
			
		||||
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
 | 
			
		||||
        if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
 | 
			
		||||
            return@lazy
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        WebSettings.getDefaultUserAgent(context)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract fun shouldIntercept(response: Response): Boolean
 | 
			
		||||
 | 
			
		||||
    abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response
 | 
			
		||||
 | 
			
		||||
    override fun intercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val request = chain.request()
 | 
			
		||||
        val response = chain.proceed(request)
 | 
			
		||||
        if (!shouldIntercept(response)) {
 | 
			
		||||
            return response
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!WebViewUtil.supportsWebView(context)) {
 | 
			
		||||
            launchUI {
 | 
			
		||||
                context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
 | 
			
		||||
            }
 | 
			
		||||
            return response
 | 
			
		||||
        }
 | 
			
		||||
        initWebView
 | 
			
		||||
 | 
			
		||||
        return intercept(chain, request, response)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createWebView(request: Request): WebView {
 | 
			
		||||
        val webview = WebView(context)
 | 
			
		||||
        webview.setDefaultSettings()
 | 
			
		||||
        webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent
 | 
			
		||||
        return webview
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -11,7 +11,7 @@ import logcat.LogPriority
 | 
			
		||||
object WebViewUtil {
 | 
			
		||||
    const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
 | 
			
		||||
 | 
			
		||||
    const val MINIMUM_WEBVIEW_VERSION = 99
 | 
			
		||||
    const val MINIMUM_WEBVIEW_VERSION = 100
 | 
			
		||||
 | 
			
		||||
    fun supportsWebView(context: Context): Boolean {
 | 
			
		||||
        try {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user